contrafact 0.2.0-rc.1

A trait for highly composable constraints ("facts") which can be used both to verify data and to generate arbitrary data within those constraints
Documentation
use arbitrary::Arbitrary;
use contrafact::{facts::*, *};

use either::Either;
#[cfg(feature = "optics")]
use lens_rs::{optics, Lens, Prism};

type Id = u32;

// Similar to Holochain's DhtOp
#[derive(Clone, Debug, PartialEq, Arbitrary)]
enum Omega {
    AlphaBeta { id: Id, alpha: Alpha, beta: Beta },
    Alpha { id: Id, alpha: Alpha },
}

#[allow(unused)]
impl Omega {
    fn alpha(&self) -> &Alpha {
        match self {
            Self::AlphaBeta { alpha, .. } => alpha,
            Self::Alpha { alpha, .. } => alpha,
        }
    }

    fn alpha_mut(&mut self) -> &mut Alpha {
        match self {
            Self::AlphaBeta { alpha, .. } => alpha,
            Self::Alpha { alpha, .. } => alpha,
        }
    }

    fn _beta(&self) -> Option<&Beta> {
        match self {
            Self::AlphaBeta { beta, .. } => Some(beta),
            Self::Alpha { .. } => None,
        }
    }

    fn beta_mut(&mut self) -> Option<&mut Beta> {
        match self {
            Self::AlphaBeta { beta, .. } => Some(beta),
            Self::Alpha { .. } => None,
        }
    }

    fn pi(&self) -> Pi {
        match self.clone() {
            Self::AlphaBeta { alpha, beta, .. } => Pi(alpha, Some(beta)),
            Self::Alpha { alpha, .. } => Pi(alpha, None),
        }
    }

    fn id(&self) -> &Id {
        match self {
            Self::AlphaBeta { id, .. } => id,
            Self::Alpha { id, .. } => id,
        }
    }

    fn id_mut(&mut self) -> &mut Id {
        match self {
            Self::AlphaBeta { id, .. } => id,
            Self::Alpha { id, .. } => id,
        }
    }
}

// Similar to Holochain's Action
#[derive(Clone, Debug, PartialEq, Arbitrary)]
enum Alpha {
    Beta { id: Id, beta: Beta, data: String },
    Nil { id: Id, data: String },
}

#[allow(unused)]
impl Alpha {
    fn id(&mut self) -> &mut Id {
        match self {
            Self::Beta { id, .. } => id,
            Self::Nil { id, .. } => id,
        }
    }
    fn data(&mut self) -> &mut String {
        match self {
            Self::Beta { data, .. } => data,
            Self::Nil { data, .. } => data,
        }
    }
}

// Similar to Holochain's Entry
#[derive(Clone, Debug, PartialEq, Arbitrary)]
struct Beta {
    id: u32,
    data: String,
}

#[derive(Clone, Debug, PartialEq, Arbitrary)]
/// Similar to Holochain's SignedActionHashed
struct Sigma {
    alpha: Alpha,
    id2: Id,
    sig: String,
}

#[derive(Clone, Debug, PartialEq, Arbitrary)]
#[cfg_attr(feature = "optics", derive(Lens))]
/// Similar to Holochain's Record
struct Rho {
    #[cfg_attr(feature = "optics", optic)]
    sigma: Sigma,
    #[cfg_attr(feature = "optics", optic)]
    beta: Option<Beta>,
}

/// Some struct needed to set the values of a Sigma whenever its Alpha changes.
/// Analogous to Holochain's Keystore (MetaLairClient).
#[derive(Clone)]
struct AlphaSigner;

impl AlphaSigner {
    fn sign(&self, mut alpha: Alpha) -> Sigma {
        Sigma {
            id2: alpha.id().clone() * 2,
            sig: alpha.id().to_string(),
            alpha,
        }
    }
}

#[allow(unused)]
fn alpha_fact() -> impl Fact<'static, Alpha> {
    facts![lens1("Alpha::id", |a: &mut Alpha| a.id(), id_fact(None))]
}

fn beta_fact() -> impl Fact<'static, Beta> {
    facts![lens1("Beta::id", |a: &mut Beta| &mut a.id, id_fact(None))]
}

/// Just a pair of an Alpha with optional Beta.
/// An intermediate type not used "in production" but useful for writing Fact 'static, against
#[derive(Clone, Debug, PartialEq, Arbitrary)]
struct Pi(Alpha, Option<Beta>);

fn pi_beta_match() -> impl Fact<'static, Pi> {
    facts![brute(
        "Pi alpha has matching beta iff beta is Some",
        |p: &Pi| match p {
            Pi(Alpha::Beta { beta, .. }, Some(b)) => beta == b,
            Pi(Alpha::Nil { .. }, None) => true,
            _ => false,
        }
    )]
}

fn id_fact(id: Option<Id>) -> impl Fact<'static, Id> {
    let le = brute("< u32::MAX", |id: &Id| *id < Id::MAX / 2);

    if let Some(id) = id {
        Either::Left(facts![le, eq(id)])
    } else {
        Either::Right(facts![le])
    }
}

/// - id must be set as specified
/// - All Ids should match each other. If there is a Beta, its id should match too.
fn pi_fact(id: Id) -> impl Fact<'static, Pi> {
    let alpha_fact = facts![
        lens1("Alpha::id", |a: &mut Alpha| a.id(), id_fact(Some(id))),
        // lens1("Alpha::data", |a: &mut Alpha| a.data(), eq(data)),
    ];
    let beta_fact = lens1("Beta::id", |b: &mut Beta| &mut b.id, id_fact(Some(id)));
    facts![
        pi_beta_match(),
        lens1("Pi::alpha", |o: &mut Pi| &mut o.0, alpha_fact),
        prism("Pi::beta", |o: &mut Pi| o.1.as_mut(), beta_fact),
    ]
}

/// - All Ids should match each other. If there is a Beta, its id should match too
/// - If Omega::Alpha,     then Alpha::Nil.
/// - If Omega::AlphaBeta, then Alpha::Beta,
///     - and, the the Betas of the Alpha and the Omega should match.
/// - all data must be set as specified
fn omega_fact(id: Id) -> impl Fact<'static, Omega> {
    let omega_pi = lens2(
        "Omega -> Pi",
        |o| match o {
            Omega::AlphaBeta { alpha, beta, .. } => Pi(alpha, Some(beta)),
            Omega::Alpha { alpha, .. } => Pi(alpha, None),
        },
        |o, pi| {
            let id = o.id().clone();
            match pi {
                Pi(alpha, Some(beta)) => Omega::AlphaBeta { id, alpha, beta },
                Pi(alpha, None) => Omega::Alpha { id, alpha },
            }
        },
        pi_fact(id),
    );

    facts![
        omega_pi,
        lens1("Omega::id", |o: &mut Omega| o.id_mut(), id_fact(Some(id))),
    ]
}

#[allow(unused)]
fn sigma_fact() -> impl Fact<'static, Sigma> {
    let id2_fact = lens2(
        "Sigma::id is correct",
        |mut s: Sigma| (s.id2, *(s.alpha.id()) * 2),
        |mut s, (_, id2)| {
            s.id2 = id2;
            s
        },
        same(),
    );
    let sig_fact = lens2(
        "Sigma::sig is correct",
        |mut s: Sigma| (s.sig, s.alpha.id().to_string()),
        |mut s, (_, sig)| {
            s.sig = sig;
            s
        },
        same(),
    );
    facts![
        lens1("Sigma::id", |o: &mut Sigma| o.alpha.id(), id_fact(None)),
        id2_fact
    ]
}

/// The inner Sigma is correct wrt to signature
/// XXX: this is a little wonky, probably room for improvement.
fn rho_fact(id: Id, signer: AlphaSigner) -> impl Fact<'static, Rho> {
    let rho_pi = lens2(
        "Rho -> Pi",
        |rho: Rho| Pi(rho.sigma.alpha, rho.beta),
        move |mut rho, Pi(a, b)| {
            rho.sigma = signer.sign(a);
            rho.beta = b;
            rho
        },
        pi_fact(id),
    );

    #[cfg(not(feature = "optics"))]
    {
        facts![
            lens1("Rho -> Sigma", |rho: &mut Rho| &mut rho.sigma, sigma_fact()),
            rho_pi
        ]
    }

    #[cfg(feature = "optics")]
    {
        facts![
            optical("Rho -> Sigma", optics!(sigma), sigma_fact()),
            rho_pi
        ]
    }
}

#[test]
fn test_rho_fact() {
    observability::test_run().ok();
    let mut g = utils::random_generator();

    let fact = rho_fact(5, AlphaSigner);
    let mut rho = fact.clone().build(&mut g);
    assert!(fact.clone().check(&rho).is_ok());
    assert_eq!(rho.sigma.id2, 10);
    assert_eq!(rho.sigma.sig, "5".to_string());

    rho.sigma.id2 = 9;
    assert!(fact.clone().check(&rho).is_err());

    dbg!(rho);
}

#[test]
fn test_omega_fact() {
    observability::test_run().ok();
    let mut g = utils::random_generator();

    let mut fact = omega_fact(11);

    let beta = beta_fact().build(&mut g);

    let mut valid1 = Omega::Alpha {
        id: 8,
        alpha: Alpha::Nil {
            id: 3,
            data: "cheese".into(),
        },
    };

    let mut valid2 = Omega::AlphaBeta {
        id: 8,
        alpha: Alpha::Nil {
            id: 3,
            data: "cheese".into(),
        },
        beta: beta.clone(),
    };

    valid1 = fact.mutate(&mut g, valid1).unwrap();
    fact.clone().check(dbg!(&valid1)).unwrap();

    valid2 = fact.mutate(&mut g, valid2).unwrap();
    fact.clone().check(dbg!(&valid2)).unwrap();

    let mut invalid1 = Omega::Alpha {
        id: 8,
        alpha: Alpha::Beta {
            id: 3,
            data: "cheese".into(),
            beta: beta.clone(),
        },
    };

    let mut invalid2 = Omega::AlphaBeta {
        id: 8,
        alpha: Alpha::Nil {
            id: 3,
            data: "cheese".into(),
        },
        beta: beta.clone(),
    };

    // Ensure that check fails for invalid data
    assert!(
        dbg!(fact
            .clone()
            .check(dbg!(&invalid1))
            .result()
            .unwrap()
            .unwrap_err())
        .len()
            > 0
    );
    invalid1 = fact.mutate(&mut g, invalid1).unwrap();
    fact.clone().check(dbg!(&invalid1)).unwrap();

    // Ensure that check fails for invalid data
    assert!(
        dbg!(fact
            .clone()
            .check(dbg!(&invalid2))
            .result()
            .unwrap()
            .unwrap_err())
        .len()
            > 0
    );
    invalid2 = fact.mutate(&mut g, invalid2).unwrap();
    fact.clone().check(dbg!(&invalid2)).unwrap();
}