icydb-model-macros 0.213.38

Procedural macros for IcyDB application models
Documentation
//! Module: node::item
//! Responsibility: derive-side node parsing.
//! Does not own: runtime schema semantics.
//! Boundary: macro metadata to node models.

#[cfg(test)]
mod tests;

use crate::prelude::*;

///
/// Item
///

#[derive(Clone, Debug, Default, FromMeta)]
pub struct Item {
    #[darling(default)]
    pub(crate) is: Option<Path>,

    #[darling(default, rename = "prim")]
    pub(crate) primitive: Option<Primitive>,

    #[darling(default)]
    pub(crate) scale: Option<u32>,

    #[darling(default)]
    pub(crate) max_len: Option<u32>,

    #[darling(default)]
    pub(crate) max_bytes: Option<u32>,

    #[darling(default)]
    pub(crate) unbounded: bool,

    #[darling(default, rename = "rel")]
    pub(crate) relation: Option<Path>,

    #[darling(multiple, rename = "normalizer")]
    pub(crate) normalizers: Vec<TypeNormalizer>,

    #[darling(multiple, rename = "validator")]
    pub(crate) validators: Vec<TypeValidator>,

    #[darling(default)]
    pub(crate) indirect: bool,
}

impl Item {
    pub fn validate(&self) -> Result<(), DarlingError> {
        // Phase 1: reject incompatible option pairs.
        if self.is.is_some() && self.primitive.is_some() {
            return Err(DarlingError::custom(
                "item may not specify both is and prim",
            ));
        }

        if let Some(relation) = &self.relation
            && self.primitive.is_none()
        {
            return Err(
                DarlingError::custom(
                    "rel fields must explicitly declare prim = \"...\"; implicit identity types are forbidden",
                )
                .with_span(relation),
            );
        }

        // Phase 3: enforce relation constraints.
        if let Some(relation) = &self.relation
            && self.indirect
        {
            return Err(
                DarlingError::custom("relations cannot be set to indirect").with_span(relation)
            );
        }

        // Phase 4: validate scalar metadata owned by item directives.
        if self.scale.is_some() && !matches!(self.primitive, Some(Primitive::Decimal)) {
            return Err(DarlingError::custom(
                "scale may only be used with prim = \"Decimal\"",
            ));
        }
        if matches!(self.primitive, Some(Primitive::Decimal)) && self.scale.is_none() {
            return Err(DarlingError::custom(
                "prim = \"Decimal\" requires item(scale = N)",
            ));
        }
        if self.max_len.is_some()
            && !matches!(self.primitive, Some(Primitive::Text | Primitive::Blob))
        {
            return Err(DarlingError::custom(
                "max_len may only be used with prim = \"Text\" or prim = \"Blob\"",
            ));
        }
        if self.max_len.is_some_and(|max_len| max_len == 0) {
            return Err(DarlingError::custom(
                "item(max_len = N) requires a positive value",
            ));
        }
        if self.max_bytes.is_some()
            && !matches!(self.primitive, Some(Primitive::IntBig | Primitive::NatBig))
        {
            return Err(DarlingError::custom(
                "max_bytes may only be used with prim = \"IntBig\" or prim = \"NatBig\"",
            ));
        }
        if self.max_bytes.is_some_and(|max_bytes| max_bytes == 0) {
            return Err(DarlingError::custom(
                "item(max_bytes = N) requires a positive value",
            ));
        }
        if self.unbounded && !matches!(self.primitive, Some(Primitive::Text | Primitive::Blob)) {
            return Err(DarlingError::custom(
                "unbounded may only be used with prim = \"Text\" or prim = \"Blob\"",
            ));
        }
        if self.unbounded && self.max_len.is_some() {
            return Err(DarlingError::custom(
                "unbounded cannot be combined with max_len",
            ));
        }
        if self.max_len.is_none() && !self.unbounded {
            match self.primitive {
                Some(Primitive::Text) => {
                    return Err(DarlingError::custom(
                        "prim = \"Text\" requires either item(max_len = N) or item(unbounded)",
                    ));
                }
                Some(Primitive::Blob) => {
                    return Err(DarlingError::custom(
                        "prim = \"Blob\" requires either item(max_len = N) or item(unbounded)",
                    ));
                }
                _ => {}
            }
        }

        Ok(())
    }

    pub fn target(&self) -> ItemTarget {
        debug_assert!(
            !(self.is.is_some() && self.primitive.is_some()),
            "item 'is' cannot be combined with 'prim'",
        );

        if let Some(path) = &self.is {
            return ItemTarget::Is(path.clone());
        }
        if let Some(prim) = &self.primitive {
            return ItemTarget::Primitive(*prim);
        }

        ItemTarget::Primitive(Primitive::Unit)
    }

    pub const fn is_relation(&self) -> bool {
        self.relation.is_some()
    }
}

impl HasSchemaPart for Item {
    fn schema_part(&self) -> TokenStream {
        let target = self.target().schema_part();
        let relation = quote_option(self.relation.as_ref(), to_path);
        let scale = quote_option(self.scale.as_ref(), |scale| quote!(#scale));
        let max_len = quote_option(self.max_len.as_ref(), |max_len| quote!(#max_len));
        let max_bytes = quote_option(self.max_bytes.as_ref(), |max_bytes| quote!(#max_bytes));
        let validators = quote_slice(&self.validators, TypeValidator::schema_part);
        let normalizers = quote_slice(&self.normalizers, TypeNormalizer::schema_part);
        let indirect = self.indirect;

        quote! {
            ::icydb_model::node::Item::new(
                #target,
                #relation,
                #scale,
                #max_len,
                #max_bytes,
                #validators,
                #normalizers,
                #indirect,
            )
        }
    }
}

impl HasTypeExpr for Item {
    fn type_expr(&self) -> TokenStream {
        let ty = self.target().type_expr();

        if self.indirect {
            quote!(Box<#ty>)
        } else {
            quote!(#ty)
        }
    }
}

///
/// ItemTarget
///

pub enum ItemTarget {
    Is(Path),
    Primitive(Primitive),
}

impl HasSchemaPart for ItemTarget {
    fn schema_part(&self) -> TokenStream {
        match self {
            Self::Is(path) => {
                let path = quote_one(path, to_path);
                quote! {
                    ::icydb_model::node::ItemTarget::Is(#path)
                }
            }
            Self::Primitive(prim) => {
                quote! {
                    ::icydb_model::node::ItemTarget::Primitive(#prim)
                }
            }
        }
    }
}

impl HasTypeExpr for ItemTarget {
    fn type_expr(&self) -> TokenStream {
        match self {
            Self::Is(path) => quote!(#path),
            Self::Primitive(prim) => {
                let ty = crate::types::primitive_type_tokens(*prim);
                quote!(#ty)
            }
        }
    }
}