icydb-model 0.299.0

IcyDB application-model authoring, validation, and code generation
Documentation
use super::*;
use crate::{
    normalize::normalize,
    validate::validate,
    visitor::{ApplicationOperation, CallbackKind, Issue, VisitorError},
};
use std::{
    cell::{Cell, RefCell},
    rc::Rc,
};

const AUTO_NORMALIZE_ISSUE: &str = "automatic normalize";
const CUSTOM_NORMALIZE_ISSUE: &str = "custom normalize";
const AUTO_VALIDATE_ISSUE: &str = "automatic validate";
const CUSTOM_VALIDATE_ISSUE: &str = "custom validate";

#[derive(Default)]
struct HookProbe {
    auto_normalize: u32,
    custom_normalize: u32,
    auto_validate: Cell<u32>,
    custom_validate: Cell<u32>,
}

struct OrderedLeaf {
    events: Rc<RefCell<Vec<&'static str>>>,
}

impl Visitable for OrderedLeaf {}

impl NormalizeAuto for OrderedLeaf {
    fn normalize_self(&mut self, _ctx: &mut dyn VisitorContext) {
        self.events.borrow_mut().push("leaf normalize auto");
    }
}

impl NormalizeCustom for OrderedLeaf {
    fn normalize_custom(&mut self, _ctx: &mut dyn VisitorContext) {
        self.events.borrow_mut().push("leaf normalize custom");
    }
}

impl ValidateAuto for OrderedLeaf {
    fn validate_self(&self, _ctx: &mut dyn VisitorContext) {
        self.events.borrow_mut().push("leaf validate auto");
    }
}

impl ValidateCustom for OrderedLeaf {
    fn validate_custom(&self, _ctx: &mut dyn VisitorContext) {
        self.events.borrow_mut().push("leaf validate custom");
    }
}

struct OrderedParent {
    events: Rc<RefCell<Vec<&'static str>>>,
    child: OrderedLeaf,
}

impl Visitable for OrderedParent {
    fn drive(&self, visitor: &mut dyn VisitorCore) {
        perform_visit(visitor, &self.child, "child");
    }

    fn drive_mut(&mut self, visitor: &mut dyn VisitorMutCore) {
        perform_visit_mut(visitor, &mut self.child, "child");
    }
}

impl NormalizeAuto for OrderedParent {
    fn normalize_self(&mut self, _ctx: &mut dyn VisitorContext) {
        self.events.borrow_mut().push("parent normalize auto");
    }
}

impl NormalizeCustom for OrderedParent {
    fn normalize_custom(&mut self, _ctx: &mut dyn VisitorContext) {
        self.events.borrow_mut().push("parent normalize custom");
    }
}

impl ValidateAuto for OrderedParent {
    fn validate_self(&self, _ctx: &mut dyn VisitorContext) {
        self.events.borrow_mut().push("parent validate auto");
    }
}

impl ValidateCustom for OrderedParent {
    fn validate_custom(&self, _ctx: &mut dyn VisitorContext) {
        self.events.borrow_mut().push("parent validate custom");
    }
}

fn ordered_parent() -> (OrderedParent, Rc<RefCell<Vec<&'static str>>>) {
    let events = Rc::new(RefCell::new(Vec::new()));
    (
        OrderedParent {
            events: Rc::clone(&events),
            child: OrderedLeaf {
                events: Rc::clone(&events),
            },
        },
        events,
    )
}

impl Visitable for HookProbe {}

impl NormalizeAuto for HookProbe {
    fn normalize_self(&mut self, ctx: &mut dyn VisitorContext) {
        self.auto_normalize += 1;
        ctx.issue(AUTO_NORMALIZE_ISSUE);
    }
}

impl NormalizeCustom for HookProbe {
    fn normalize_custom(&mut self, ctx: &mut dyn VisitorContext) {
        self.custom_normalize += 1;
        ctx.issue(CUSTOM_NORMALIZE_ISSUE);
    }
}

impl ValidateAuto for HookProbe {
    fn validate_self(&self, ctx: &mut dyn VisitorContext) {
        self.auto_validate.set(self.auto_validate.get() + 1);
        ctx.issue(AUTO_VALIDATE_ISSUE);
    }
}

impl ValidateCustom for HookProbe {
    fn validate_custom(&self, ctx: &mut dyn VisitorContext) {
        self.custom_validate.set(self.custom_validate.get() + 1);
        ctx.issue(CUSTOM_VALIDATE_ISSUE);
    }
}

fn assert_issues(error: &VisitorError, path: &str, expected: [&str; 2]) {
    let issues = error
        .issues()
        .get(path)
        .unwrap_or_else(|| panic!("expected visitor issues at {path}"));
    let messages = issues.iter().map(Issue::message).collect::<Vec<_>>();
    assert_eq!(messages, expected);
}

fn assert_callbacks(error: &VisitorError, path: &str, expected: [CallbackKind; 2]) {
    let issues = error
        .issues()
        .get(path)
        .unwrap_or_else(|| panic!("expected visitor issues at {path}"));
    let callbacks = issues
        .iter()
        .map(|issue| {
            issue
                .callback()
                .expect("top-level application traversal must type every callback")
        })
        .collect::<Vec<_>>();
    assert_eq!(
        callbacks
            .iter()
            .map(|callback| callback.kind())
            .collect::<Vec<_>>(),
        expected
    );
    assert!(
        callbacks
            .iter()
            .all(|callback| callback.type_path() == std::any::type_name::<HookProbe>())
    );
}

#[test]
fn option_vec_normalize_hooks_run_once_at_each_indexed_path() {
    let mut value = Some(vec![HookProbe::default(), HookProbe::default()]);

    let error = normalize(&mut value).expect_err("probe normalizers should report issues");
    assert_eq!(error.operation(), ApplicationOperation::Normalize);

    let Some(probes) = value.as_ref() else {
        panic!("normalize should preserve the populated option");
    };
    for probe in probes {
        assert_eq!(probe.auto_normalize, 1);
        assert_eq!(probe.custom_normalize, 1);
    }
    assert!(error.issues().get("").is_none());
    assert_issues(
        &error,
        "[0]",
        [AUTO_NORMALIZE_ISSUE, CUSTOM_NORMALIZE_ISSUE],
    );
    assert_callbacks(
        &error,
        "[0]",
        [CallbackKind::NormalizeAuto, CallbackKind::NormalizeCustom],
    );
    assert_issues(
        &error,
        "[1]",
        [AUTO_NORMALIZE_ISSUE, CUSTOM_NORMALIZE_ISSUE],
    );
}

#[test]
fn option_vec_validate_hooks_run_once_at_each_indexed_path() {
    let value = Some(vec![HookProbe::default(), HookProbe::default()]);

    let error = validate(&value).expect_err("probe validators should report issues");
    assert_eq!(error.operation(), ApplicationOperation::Validate);

    let Some(probes) = value.as_ref() else {
        panic!("validate should preserve the populated option");
    };
    for probe in probes {
        assert_eq!(probe.auto_validate.get(), 1);
        assert_eq!(probe.custom_validate.get(), 1);
    }
    assert!(error.issues().get("").is_none());
    assert_issues(&error, "[0]", [AUTO_VALIDATE_ISSUE, CUSTOM_VALIDATE_ISSUE]);
    assert_callbacks(
        &error,
        "[0]",
        [CallbackKind::ValidateAuto, CallbackKind::ValidateCustom],
    );
    assert_issues(&error, "[1]", [AUTO_VALIDATE_ISSUE, CUSTOM_VALIDATE_ISSUE]);
}

#[test]
fn box_transparency_keeps_one_forwarded_hook_call() {
    let mut normalized = Box::new(HookProbe::default());
    let normalize_error =
        normalize(&mut normalized).expect_err("probe normalizers should report issues");
    assert_eq!(normalized.auto_normalize, 1);
    assert_eq!(normalized.custom_normalize, 1);
    assert_callbacks(
        &normalize_error,
        "",
        [CallbackKind::NormalizeAuto, CallbackKind::NormalizeCustom],
    );

    let validated = Box::new(HookProbe::default());
    let validate_error = validate(&validated).expect_err("probe validators should report issues");
    assert_eq!(validated.auto_validate.get(), 1);
    assert_eq!(validated.custom_validate.get(), 1);
    assert_callbacks(
        &validate_error,
        "",
        [CallbackKind::ValidateAuto, CallbackKind::ValidateCustom],
    );
}

#[test]
fn normalize_and_validate_traversals_are_preorder_and_declaration_ordered() {
    let (mut normalized, normalize_events) = ordered_parent();
    normalize(&mut normalized).expect("ordered normalizers should succeed");
    assert_eq!(
        normalize_events.borrow().as_slice(),
        [
            "parent normalize auto",
            "parent normalize custom",
            "leaf normalize auto",
            "leaf normalize custom",
        ]
    );

    let (validated, validate_events) = ordered_parent();
    validate(&validated).expect("ordered validators should succeed");
    assert_eq!(
        validate_events.borrow().as_slice(),
        [
            "parent validate auto",
            "parent validate custom",
            "leaf validate auto",
            "leaf validate custom",
        ]
    );
}