Skip to content

Commit

Permalink
Document OptionalResult (#55)
Browse files Browse the repository at this point in the history
* feat: document OptionalResult::and_then

* feat: document OptionalResult::fail_or

* feat: document Default for OptionalResult

* feat: document OptionalResult::filter

* feat: rename OptionalResult::fail_or_* into result_or_*

* feat: document inspect and map

* feat: remove repetitive tests

* feat: add inline attributes
Jujulego authored Aug 21, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent ed7b6a3 commit bfbb1ff
Showing 2 changed files with 222 additions and 76 deletions.
2 changes: 1 addition & 1 deletion crates/ring-js/src/project_detector.rs
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ impl Detector for JsProjectDetector {
self.package_loader.load(path)
.and_then(|mnf|
self.lockfile_detector.detect_from(path)
.fail_or_default()
.result_or_default()
.map(|lck| (mnf, lck))
)
.map(|(mnf, lck)| Rc::new(JsProject::new(path.to_path_buf(), mnf, lck)))
296 changes: 221 additions & 75 deletions crates/ring-utils/src/optional_result.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,199 @@
use crate::OptionalResult::{Empty, Fail, Found};

#[derive(Debug, Eq, PartialEq)]
/// Combination of Option and Result
///
/// # Examples
///
/// `OptionalResult<T, E>` is the same as `Option<Result<T, E>>`
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// assert_eq!(Found::<&str, ()>("test"), Some(Ok("test")));
/// assert_eq!(Empty::<&str, ()>, None::<Result<_, _>>);
/// assert_eq!(Fail::<&str, ()>(()), Some(Err(())));
/// ```
///
/// `OptionalResult<T, E>` is the same as `Result<Option<T>, E>`
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// assert_eq!(Found::<&str, ()>("test"), Ok(Some("test")));
/// assert_eq!(Empty::<&str, ()>, Ok(None));
/// assert_eq!(Fail::<&str, ()>(()), Err::<Option<_>, _>(()));
/// ```
#[derive(Debug, Eq)]
#[must_use = "this `OptionalResult` may be an `Fail` variant, which should be handled"]
pub enum OptionalResult<T, E = anyhow::Error> {
Found(T),
Fail(E),
Empty,
}

impl<T, E> OptionalResult<T, E> {
pub fn and_then<R, F>(self, f: F) -> OptionalResult<R, E>
where
F: FnOnce(T) -> OptionalResult<R, E>,
{
/// Returns [`Empty`] if the optional result is [`Empty`], [`Fail`] if it is [`Fail`] otherwise
/// calls `f` with the wrapped value and return the result.
///
/// # Examples
///
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// fn euclidean_divide(a: i32, b: i32) -> OptionalResult<i32, &'static str> {
/// match (a, b) {
/// (_, 0) => Fail("Cannot divide by 0"),
/// (a, b) if a % b == 0 => Found(a / b),
/// (_, _) => Empty,
/// }
/// }
///
/// assert_eq!(Found(4).and_then(|n| euclidean_divide(n, 2)), Found(2));
/// assert_eq!(Found(4).and_then(|n| euclidean_divide(n, 3)), Empty);
/// assert_eq!(Found(4).and_then(|n| euclidean_divide(n, 0)), Fail("Cannot divide by 0"));
/// assert_eq!(Empty.and_then(|n| euclidean_divide(n, 2)), Empty);
/// assert_eq!(Fail("early").and_then(|n| euclidean_divide(n, 2)), Fail("early"));
/// ```
#[inline]
pub fn and_then<U, R: Into<OptionalResult<U, E>>>(self, f: impl FnOnce(T) -> R) -> OptionalResult<U, E> {
match self {
Found(val) => f(val),
Found(val) => f(val).into(),
Fail(err) => Fail(err),
Empty => Empty,
}
}

pub fn fail_or(self, val: T) -> OptionalResult<T, E> {
if matches!(self, Empty) { Found(val) } else { self }
/// Returns [`Ok`] if the optional result is [`Found`], [`Err`] if it is [`Fail`] otherwise
/// calls `f` and return the result wrapped in [`Ok`].
///
/// # Examples
///
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// assert_eq!(Found::<i32, ()>(2).result_or_else(|| 42), Ok(2));
/// assert_eq!(Empty::<i32, ()>.result_or_else(|| 42), Ok(42));
/// assert_eq!(Fail::<i32, ()>(()).result_or_else(|| 42), Err(()));
/// ```
#[inline]
pub fn result_or_else(self, f: impl FnOnce() -> T) -> Result<T, E> {
match self {
Found(val) => Ok(val),
Fail(err) => Err(err),
Empty => Ok(f()),
}
}

/// Returns [`Ok`] if the optional result is [`Found`], [`Err`] if it is [`Fail`] otherwise
/// returns `val` wrapped in [`Ok`].
///
/// # Examples
///
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// assert_eq!(Found::<i32, ()>(2).result_or(42), Ok(2));
/// assert_eq!(Empty::<i32, ()>.result_or(42), Ok(42));
/// assert_eq!(Fail::<i32, ()>(()).result_or(42), Err(()));
/// ```
#[inline]
pub fn result_or(self, val: T) -> Result<T, E> {
match self {
Found(val) => Ok(val),
Fail(err) => Err(err),
Empty => Ok(val),
}
}

/// Returns [`Ok`] if the optional result is [`Found`], [`Err`] if it is [`Fail`] otherwise
/// calls `Default::default` and return the result wrapped in [`Ok`].
///
/// # Examples
///
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// assert_eq!(Found::<i32, ()>(2).result_or_default(), Ok(2));
/// assert_eq!(Empty::<i32, ()>.result_or_default(), Ok(0));
/// assert_eq!(Fail::<i32, ()>(()).result_or_default(), Err(()));
/// ```
#[inline]
pub fn result_or_default(self) -> Result<T, E>
where
T: Default
{
self.result_or_else(T::default)
}

pub fn filter<F>(self, f: F) -> OptionalResult<T, E>
/// Returns [`Empty`] if the optional result is [`Empty`], [`Fail`] if it is [`Fail`] otherwise
/// calls `predicate` with the wrapped value and returns:
///
/// - [`Found(val)`] if `predicate` returns `true` (where `val` is the wrapped value), and
/// - [`Empty`] if `predicate` returns `false`.
///
/// # Examples
///
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// fn is_even(n: &i32) -> bool {
/// n % 2 == 0
/// }
///
/// assert_eq!(Found::<_, ()>(2).filter(is_even), Found(2));
/// assert_eq!(Found::<_, ()>(1).filter(is_even), Empty);
/// assert_eq!(Empty::<_, ()>.filter(is_even), Empty);
/// assert_eq!(Fail::<_, ()>(()).filter(is_even), Fail(()));
/// ```
#[inline]
pub fn filter<F>(self, predicate: F) -> OptionalResult<T, E>
where
F: FnOnce(&T) -> bool,
{
match self {
Found(val) if f(&val) => Found(val),
Found(val) if predicate(&val) => Found(val),
Found(_) | Empty => Empty,
Fail(err) => Fail(err),
}
}

pub fn inspect<F>(self, f: F) -> OptionalResult<T, E>
where
F: FnOnce(&T),
{
if let Found(val) = &self {
/// Calls a function with a reference to the contained value if [`Found`]
///
/// Returns the original optional result
///
/// # Examples
///
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// // prints "hello world"
/// let result = Found::<&str, ()>("world").inspect(|txt| println!("hello {txt}"));
///
/// // prints nothing
/// let result = Empty::<&str, ()>.inspect(|txt| println!("hello {txt}"));
/// let result = Fail::<&str, ()>(()).inspect(|txt| println!("hello {txt}"));
/// ```
#[inline]
pub fn inspect(self, f: impl FnOnce(&T)) -> OptionalResult<T, E> {
if let Found(ref val) = self {
f(val);
}

self
}

pub fn map<R, F>(self, f: F) -> OptionalResult<R, E>
where
F: FnOnce(T) -> R,
{
/// Apply a function to the contained value (if [`Found`]) mapping `OptionalResult<T, E>` to
/// `OptionalResult<U, E>`.
///
/// # Examples
///
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// assert_eq!(Found::<&str, ()>("test").map(|s| s.len()), Found(4));
/// assert_eq!(Empty::<&str, ()>.map(|s| s.len()), Empty);
/// assert_eq!(Fail::<&str, ()>(()).map(|s| s.len()), Fail(()));
/// ```
#[inline]
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> OptionalResult<U, E> {
match self {
Found(data) => Found(f(data)),
Fail(err) => Fail(err),
@@ -57,13 +202,25 @@ impl<T, E> OptionalResult<T, E> {
}
}

impl<T : Default, E> OptionalResult<T, E> {
pub fn fail_or_default(self) -> OptionalResult<T, E> {
self.fail_or(T::default())
/// Default value for OptionalResult is [`Empty`]
///
/// # Examples
///
/// ```
/// use ring_utils::OptionalResult::{self, *};
///
/// let result: OptionalResult<(), ()> = Default::default();
/// assert_eq!(result, Empty);
/// ```
impl<T, E> Default for OptionalResult<T, E> {
#[inline]
fn default() -> Self {
Empty
}
}

impl<T, E> From<Result<T, E>> for OptionalResult<T, E> {
#[inline]
fn from(res: Result<T, E>) -> Self {
match res {
Ok(val) => Found(val),
@@ -73,6 +230,7 @@ impl<T, E> From<Result<T, E>> for OptionalResult<T, E> {
}

impl<T, E> From<OptionalResult<T, E>> for Result<Option<T>, E> {
#[inline]
fn from(res: OptionalResult<T, E>) -> Self {
match res {
Found(val) => Ok(Some(val)),
@@ -83,6 +241,7 @@ impl<T, E> From<OptionalResult<T, E>> for Result<Option<T>, E> {
}

impl<T, E> From<Option<T>> for OptionalResult<T, E> {
#[inline]
fn from(opt: Option<T>) -> Self {
match opt {
Some(val) => Found(val),
@@ -92,6 +251,7 @@ impl<T, E> From<Option<T>> for OptionalResult<T, E> {
}

impl<T, E> From<OptionalResult<T, E>> for Option<Result<T, E>> {
#[inline]
fn from(res: OptionalResult<T, E>) -> Self {
match res {
Found(val) => Some(Ok(val)),
@@ -101,6 +261,40 @@ impl<T, E> From<OptionalResult<T, E>> for Option<Result<T, E>> {
}
}

impl<T: PartialEq, E: PartialEq> PartialEq for OptionalResult<T, E> {
#[inline]
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Found(s), Found(o)) => *s == *o,
(Fail(s), Fail(o)) => *s == *o,
(Empty, Empty) => true,
(_, _) => false
}
}
}

impl<T: PartialEq, E: PartialEq> PartialEq<Option<Result<T, E>>> for OptionalResult<T, E> {
fn eq(&self, other: &Option<Result<T, E>>) -> bool {
match (self, other) {
(Found(s), Some(Ok(o))) => *s == *o,
(Fail(s), Some(Err(o))) => *s == *o,
(Empty, None) => true,
(_, _) => false
}
}
}

impl<T: PartialEq, E: PartialEq> PartialEq<Result<Option<T>, E>> for OptionalResult<T, E> {
fn eq(&self, other: &Result<Option<T>, E>) -> bool {
match (self, other) {
(Found(s), Ok(Some(o))) => *s == *o,
(Fail(s), Err(o)) => *s == *o,
(Empty, Ok(None)) => true,
(_, _) => false
}
}
}

#[cfg(test)]
mod tests {
use mockall::mock;
@@ -137,71 +331,23 @@ mod tests {
}

#[test]
fn it_should_apply_cb_on_optional_result() {
assert_eq!(OR::Found("test").and_then(|_| Found(4)), Found(4));
assert_eq!(OR::Found("test").and_then(|_| OR::Fail("failed")), Fail("failed"));
assert_eq!(OR::Found("test").and_then(|_| OR::Empty), Empty);

assert_eq!(OR::Fail("test").and_then(|_| Found(4)), Fail("test"));
assert_eq!(OR::Fail("test").and_then(|_| OR::Fail("failed")), Fail("test"));
assert_eq!(OR::Fail("test").and_then(|_| OR::Empty), Fail("test"));

assert_eq!(OR::Empty.and_then(|_| Found(4)), Empty);
assert_eq!(OR::Empty.and_then(|_| OR::Fail("failed")), Empty);
assert_eq!(OR::Empty.and_then(|_| OR::Empty), Empty);
}

#[test]
fn it_should_replace_empty_with_given_value() {
assert_eq!(OR::Found("test").fail_or("was empty"), Found("test"));
assert_eq!(OR::Fail("test").fail_or("was empty"), Fail("test"));
assert_eq!(OR::Empty.fail_or("was empty"), Found("was empty"));
}

#[test]
fn it_should_replace_empty_with_default_value() {
assert_eq!(OR::Found("test").fail_or_default(), Found("test"));
assert_eq!(OR::Fail("test").fail_or_default(), Fail("test"));
assert_eq!(OR::Empty.fail_or_default(), Found(""));
}

#[test]
fn it_should_filter_optional_result() {
assert_eq!(OR::Found("test").filter(|_| true), Found("test"));
assert_eq!(OR::Found("test").filter(|_| false), Empty);

assert_eq!(OR::Fail("test").filter(|_| true), Fail("test"));
assert_eq!(OR::Fail("test").filter(|_| false), Fail("test"));

assert_eq!(OR::Empty.filter(|_| true), Empty);
assert_eq!(OR::Empty.filter(|_| false), Empty);
}

#[test]
fn it_should_map_optional_result() {
assert_eq!(OR::Found("test").map(|s| s.len()), Found(4));
assert_eq!(OR::Fail("test").map(|s| s.len()), Fail("test"));
assert_eq!(OR::Empty.map(|s| s.len()), Empty);
}

#[test]
fn it_should_inspect_optional_result() {
fn inspect_should_call_f_only_on_found() {
mock!(
Inspector {
fn view(&self, val: &str) -> ();
}
);

let mut inspector = MockInspector::new();
inspector.expect_view()
.with(eq("test"))
.times(1)
.return_const(());

assert_eq!(OR::Found("test").inspect(|&s| inspector.view(s)), Found("test"));

inspector.checkpoint();

assert_eq!(OR::Fail("test").inspect(|&s| inspector.view(s)), Fail("test"));
assert_eq!(OR::Empty.inspect(|&s| inspector.view(s)), Empty);
}

0 comments on commit bfbb1ff

Please sign in to comment.