icydb-model-macros 0.277.0

Procedural macros for IcyDB application models
Documentation
//! Module: node::field::tests
//! Responsibility: current application-field declaration regression coverage.
//! Does not own: accepted default encoding or runtime write admission.
//! Boundary: macro input validation and generated application expressions.

use super::{Field, FieldGeneration, FieldWriteManagement, Value};
use crate::{
    authoring_types::Primitive,
    node::{Arg, Item},
};
use darling::{FromMeta, ast::NestedMeta};
use quote::{format_ident, quote};
use syn::parse_quote;

fn field(ident: &str, primitive: Primitive) -> Field {
    Field {
        name: format_ident!("{ident}"),
        value: Value {
            item: Item {
                primitive: Some(primitive),
                unbounded: matches!(primitive, Primitive::Blob | Primitive::Text),
                ..Item::default()
            },
            ..Value::default()
        },
        default: None,
        generated: None,
        write_management: None,
    }
}

#[test]
fn relation_fields_require_canonical_identity_suffixes() {
    let mut relation = field("user", Primitive::Ulid);
    relation.value.item.relation = Some(parse_quote!(User));

    relation
        .validate()
        .expect_err("one relation without the identity suffix must reject");

    relation.name = format_ident!("user_id");
    relation
        .validate()
        .expect("canonical relation identity suffix should validate");
}

#[test]
fn rust_default_matching_is_application_only() {
    let mut text = field("name", Primitive::Text);
    text.default = Some(Arg::FuncPath(parse_quote!(String::new)));

    assert!(text.default_matches_implicit_default());
    assert_eq!(
        text.rust_default_expr()
            .expect("default should exist")
            .to_string(),
        quote!(String::new().into()).to_string(),
    );
}

#[test]
fn authored_enum_defaults_preserve_type_and_variant_names() {
    let mut status = field("status", Primitive::Unit);
    status.value.item.primitive = None;
    status.value.item.is = Some(parse_quote!(Status));
    status.default = Some(Arg::ConstPath(parse_quote!(Status::Active)));

    status
        .validate()
        .expect("matching unit-enum default should validate");

    status.default = Some(Arg::ConstPath(parse_quote!(OtherStatus::Active)));
    status
        .validate()
        .expect_err("a default from another enum must reject");
}

#[test]
fn generated_ulid_is_schema_owned_and_shape_checked() {
    let mut id = field("id", Primitive::Ulid);
    id.generated = Some(FieldGeneration::Insert(Arg::FuncPath(parse_quote!(
        Ulid::generate
    ))));
    id.validate()
        .expect("the admitted Ulid generator should validate");

    id.value.item.primitive = Some(Primitive::Nat64);
    id.value.item.unbounded = false;
    id.validate()
        .expect_err("a generator on the wrong primitive must reject");
}

#[test]
fn generated_identity_accepts_every_exact_unsigned_width() {
    for primitive in [
        Primitive::Nat8,
        Primitive::Nat16,
        Primitive::Nat32,
        Primitive::Nat64,
        Primitive::Nat128,
    ] {
        let mut id = field("id", primitive);
        id.generated = Some(FieldGeneration::Insert(Arg::FuncPath(parse_quote!(
            Identity::next
        ))));

        id.validate()
            .expect("exact unsigned identity generator should validate");
        assert!(id.is_identity_generated());
        assert_eq!(
            id.rust_default_expr()
                .expect("identity should have a Rust placeholder")
                .to_string(),
            quote!(Default::default()).to_string(),
        );
    }
}

#[test]
fn generated_identity_rejects_other_primitive_kinds() {
    let mut id = field("id", Primitive::Int64);
    id.generated = Some(FieldGeneration::Insert(Arg::FuncPath(parse_quote!(
        Identity::next
    ))));

    id.validate()
        .expect_err("signed identity generation must reject");
    id.value.item.primitive = Some(Primitive::Nat64);
    id.validate()
        .expect("the same generator should admit an unsigned identity");
}

#[test]
fn generated_clause_requires_a_quoted_function_path() {
    let generated = FieldGeneration::from_list(&[NestedMeta::Meta(syn::Meta::NameValue(
        parse_quote!(insert = "Timestamp::now"),
    ))])
    .expect("a quoted generator path should parse");

    let FieldGeneration::Insert(Arg::FuncPath(path)) = generated else {
        panic!("generated insert must retain the authored function path");
    };
    assert_eq!(
        quote!(#path).to_string(),
        quote!(Timestamp::now).to_string()
    );
}

#[test]
fn identity_like_defaults_require_explicit_persisted_literals() {
    let mut id = field("id", Primitive::Ulid);
    id.default = Some(Arg::FuncPath(parse_quote!(Ulid::default)));

    id.validate()
        .expect_err("identity-like implicit defaults must reject");
    id.default = None;
    id.validate()
        .expect("the same identity field should admit without an implicit default");
}

#[test]
fn managed_fields_do_not_accept_insert_generators() {
    let mut created_at = field("created_at", Primitive::Timestamp);
    created_at.write_management = Some(FieldWriteManagement::CreatedAt);
    created_at.generated = Some(FieldGeneration::Insert(Arg::FuncPath(parse_quote!(
        Timestamp::now
    ))));

    created_at
        .validate()
        .expect_err("managed and generated ownership must not overlap");
    created_at.generated = None;
    created_at
        .validate()
        .expect("a managed field without a competing generator should validate");
}

#[test]
fn identity_defaults_reject_invalid_text_during_field_validation() {
    for primitive in [
        Primitive::Account,
        Primitive::Principal,
        Primitive::Subaccount,
        Primitive::Ulid,
    ] {
        let mut field = field("identity", primitive);
        field.default = Some(Arg::String(parse_quote!("zz")));
        field
            .validate()
            .expect_err("malformed identity text must reject before Rust Default generation");
    }
}

#[test]
fn subaccount_default_lowering_is_fallible_for_unicode_hex() {
    let mut field = field("sub", Primitive::Subaccount);
    // Exactly 64 bytes, with the first two-byte slice splitting a UTF-8 scalar.
    let text = format!("aƩ{}", "0".repeat(61));
    field.default = Some(Arg::String(syn::LitStr::new(
        &text,
        proc_macro2::Span::call_site(),
    )));
    assert!(field.rust_default_expr().is_none());
    field.validate().expect_err("Unicode is not Subaccount hex");
}

#[test]
fn identity_defaults_share_validation_and_panic_free_construction() {
    for (primitive, text) in [
        (Primitive::Account, "2vxsx-fae".to_string()),
        (Primitive::Principal, "aaaaa-aa".to_string()),
        (Primitive::Principal, "2vxsx-fae".to_string()),
        (Primitive::Subaccount, "ab".repeat(32)),
        (Primitive::Subaccount, format!("0x{}", "AB".repeat(32))),
        (Primitive::Ulid, "00000000000000000000000000".to_string()),
        (Primitive::Ulid, "7ZZZZZZZZZZZZZZZZZZZZZZZZZ".to_string()),
    ] {
        let mut field = field("identity", primitive);
        field.default = Some(Arg::String(syn::LitStr::new(
            &text,
            proc_macro2::Span::call_site(),
        )));
        field
            .validate()
            .expect("valid identity literals must validate");
        let expression = field
            .rust_default_expr()
            .expect("valid literals construct defaults");
        syn::parse2::<syn::Expr>(expression).expect("construction must be valid Rust");
    }
    for text in [
        "",
        "0x",
        "zz",
        &"g".repeat(64),
        &"0".repeat(63),
        &"0".repeat(65),
    ] {
        let mut field = field("sub", Primitive::Subaccount);
        field.default = Some(Arg::String(syn::LitStr::new(
            text,
            proc_macro2::Span::call_site(),
        )));
        field
            .validate()
            .expect_err("invalid Subaccount literals must reject");
        assert!(field.rust_default_expr().is_none());
    }
}