optics 0.3.0

A no_std-compatible optics library providing composable lenses, prisms, isomorphisms, and fallible isomorphisms.
Documentation
mod code_quality;
mod fixtures;
pub mod helpers;

use crate::HasGetter;
use crate::HasSetter;
use crate::HasTotalGetter;
use crate::optics::lens::{Lens, mapped_lens};
use crate::optics::prism::{Prism, mapped_prism};
use crate::test::fixtures::{Config, DatabaseConfig, Timespan};
use crate::{FallibleIso, HasReverseGet, Iso, mapped_fallible_iso, mapped_iso};
use alloc::string::{String, ToString};

macro_rules! assert_impl {
    ($val:ident : $trait:path) => {{
        fn _assert_impl<T: $trait>(_v: &T) {}
        _assert_impl(&$val);
    }};
}

#[test]
fn can_read_value_using_lens() {
    let config = Config::default();
    let main_lens = mapped_lens(|c: &Config| c.main.clone(), |c, v| c.main = v);

    assert_eq!(main_lens.get(&config), config.main);
}

#[test]
fn can_compose_two_lenses() {
    let mut config = Config::default();

    let main_lens = mapped_lens(|c: &Config| c.main.clone(), |c, v| c.main = v);
    let port_lens = mapped_lens(|c: &DatabaseConfig| c.port, |c, v| c.port = v);
    let composed = main_lens.compose_with_lens(port_lens);
    assert_impl!(composed: Lens<Config, Option<u16>>);

    assert_eq!(composed.get(&config), config.main.port);

    composed.set(&mut config, Some(42));
    assert_eq!(composed.get(&config), Some(42));
}

#[test]
fn can_read_value_using_prism() {
    let config = Config::default();
    let main_prism = mapped_prism(|c: &Config| Ok::<_, ()>(c.main.clone()), |c, v| c.main = v);
    assert_eq!(main_prism.try_get(&config), Ok(config.main));
}

#[test]
fn can_compose_prisms() {
    let mut config = Config::default();

    let main_prism = mapped_prism(|c: &Config| Ok::<_, ()>(c.main.clone()), |c, v| c.main = v);
    let port_prism = mapped_prism(
        |c: &DatabaseConfig| c.port.ok_or(()),
        |c, v| c.port = Some(v),
    );
    let composed = main_prism.compose_with_prism(port_prism);

    assert_impl!(composed: Prism<Config, u16>);
    assert_eq!(composed.try_get(&config).ok(), config.main.port);

    composed.set(&mut config, 42);
    assert_eq!(composed.try_get(&config), Ok::<_, ()>(42));
}

#[test]
fn can_compose_lens_with_prisms() {
    let config = Config::default();

    let main_lens = mapped_lens(|c: &Config| c.main.clone(), |c, v| c.main = v);
    let port_lens = mapped_lens(|c: &DatabaseConfig| c.port, |c, v| c.port = v);
    let main_prism = mapped_prism(|c: &Config| Ok::<_, ()>(c.main.clone()), |c, v| c.main = v);
    let port_prism = mapped_prism(|c: &DatabaseConfig| Ok::<_, ()>(c.port), |c, v| c.port = v);

    let composed1 = main_lens.compose_with_prism(port_prism);
    let composed2 = main_prism.compose_with_lens(port_lens);

    assert_impl!(composed1: Prism<Config, Option<u16>>);
    assert_impl!(composed1: Prism<Config, Option<u16>>);

    assert_eq!(composed1.try_get(&config), Ok(config.main.port));
    assert_eq!(composed2.try_get(&config), Ok(config.main.port));
}

#[test]
fn can_call_over() {
    use crate::extensions::HasOver;

    let mut config = Config::default();
    let main_lens = mapped_lens(|c: &Config| c.delay.clone(), |c, v| c.delay = v);

    main_lens.over(&mut config, |v| match v {
        Timespan::Minutes(s) => Timespan::Minutes(s + 1),
        s => s,
    });

    assert_eq!(config.delay, Timespan::Minutes(15));
}

#[test]
fn compose_lens_with_fallible_iso() {
    let mut config = Config::default();

    let delay_lens = mapped_lens(|c: &Config| c.delay.clone(), |c, v| c.delay = v);

    let delay_iso = mapped_fallible_iso(
        |c| match c {
            Timespan::Seconds(s) => u16::try_from(*s).map_err(|_| "Out of bounds".to_string()),
            Timespan::Minutes(m) => u16::try_from(*m * 60).map_err(|_| "Out of bounds".to_string()),
            Timespan::Hours(h) => u16::try_from(*h * 3600).map_err(|_| "Out of bounds".to_string()),
        },
        |c: &u16| match u32::from(*c) {
            c if c % 3600 == 0 => Ok::<_, ()>(Timespan::Hours(c / 3600)),
            c if c % 60 == 0 => Ok::<_, ()>(Timespan::Minutes(c / 60)),
            c => Ok::<_, ()>(Timespan::Seconds(c)),
        },
    );

    let seconds_prism = delay_lens.compose_with_fallible_iso(delay_iso);

    assert_eq!(seconds_prism.try_get(&config), Ok(14u16 * 60u16));

    seconds_prism.set(&mut config, 1800u16);
    assert_eq!(config.delay, Timespan::Minutes(30));
}

#[test]
fn test_fallible_iso() {
    let mut val: u32 = 3;

    let to_u16 = mapped_fallible_iso(
        |c| u16::try_from(*c).map_err(|_| "Too big".to_string()),
        |v| Ok::<_, String>(u32::from(*v)),
    );

    let times_2 = mapped_fallible_iso(
        |c: &u16| {
            let res = c.overflowing_mul(2u16);
            (!res.1).then_some(res.0).ok_or("Overflow".to_string())
        },
        |v| {
            let res = (v / 2, v % 2);
            (res.1 == 0).then_some(res.0).ok_or("Not Even".to_string())
        },
    );

    let u16_times_2 = to_u16.compose_with_fallible_iso(times_2);

    assert_eq!(u16_times_2.try_get(&val), Ok(6u16));
    u16_times_2.set(&mut val, 4u16);
    assert_eq!(val, 2);

    assert_eq!(u16_times_2.try_reverse_get(&3), Err("Not Even".to_string()));

    val = 40000;

    assert_eq!(u16_times_2.try_get(&val), Err("Overflow".to_string()));
}

#[test]
fn test_iso_and_fallible_iso() {
    let mut val = 5;

    let wrapping_add_one = mapped_iso(|c: &u32| c.wrapping_add(1), |v| v.wrapping_sub(1));

    let wrapping_add_two = mapped_iso(|c: &u32| c.wrapping_add(2), |v| v.wrapping_sub(2));

    assert_eq!(wrapping_add_one.try_get(&val), Ok(6));
    wrapping_add_one.set(&mut val, 0);
    assert_eq!(val, u32::MAX);

    let wrapping_add_three = wrapping_add_one.compose_with_iso(wrapping_add_two);
    assert_impl!(wrapping_add_three: Iso<u32, u32>);
    assert_eq!(wrapping_add_three.try_get(&val), Ok(2));

    let wrapping_add_one = mapped_iso(|c: &u32| c.wrapping_add(1), |v| v.wrapping_sub(1));

    let to_u16 = mapped_fallible_iso(
        |c| u16::try_from(*c).map_err(|_| "Too big".to_string()),
        |v| Ok::<_, String>(u32::from(*v)),
    );

    val = 65535;
    let wrapping_add_one_to_u16 = wrapping_add_one.compose_with_fallible_iso(to_u16);
    assert_impl!(wrapping_add_one_to_u16: FallibleIso<u32, u16>);

    assert_eq!(
        wrapping_add_one_to_u16.try_get(&val),
        Err("Too big".to_string())
    );
}