icydb-model-macros 0.284.0

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

mod number;

use crate::prelude::*;
use darling::{Error as DarlingError, FromMeta, ast::NestedMeta};
use syn::{Expr, Lit, LitStr, Path};

pub use number::*;

///
/// Arg
///
/// Quoted strings are literals; bare paths name constants or functions.
///

#[derive(Clone, Debug)]
pub enum Arg {
    Bool(bool),
    Char(char),
    Number(ArgNumber),
    ConstPath(Path), // e.g. MY_CONST, Self::CONST, crate::FOO::BAR
    FuncPath(Path),  // e.g. my_fun, path::to::fun
    String(LitStr),
}

impl FromMeta for Arg {
    fn from_value(value: &Lit) -> Result<Self, DarlingError> {
        match value {
            Lit::Bool(lit) => Ok(Self::Bool(lit.value)),
            Lit::Char(lit) => Ok(Self::Char(lit.value())),
            Lit::Int(_) | Lit::Float(_) => ArgNumber::from_value(value).map(Self::Number),
            Lit::Str(lit) => Ok(Self::String(lit.clone())),
            _ => Err(DarlingError::custom(format!(
                "Unsupported literal type: {value:?}"
            ))),
        }
    }

    fn from_expr(expr: &Expr) -> Result<Self, DarlingError> {
        match expr {
            Expr::Lit(lit) => Self::from_value(&lit.lit),
            Expr::Group(group) => Self::from_expr(&group.expr),
            Expr::Path(path) if path.qself.is_none() => {
                Self::from_nested_meta(&NestedMeta::Meta(syn::Meta::Path(path.path.clone())))
            }
            _ => Err(DarlingError::unexpected_expr_type(expr)),
        }
        .map_err(|error| error.with_span(expr))
    }

    fn from_nested_meta(item: &NestedMeta) -> Result<Self, DarlingError> {
        match item {
            NestedMeta::Lit(lit) => Self::from_value(lit),
            NestedMeta::Meta(syn::Meta::Path(path)) => {
                // bare path like CONST or my_func
                let last = path
                    .segments
                    .last()
                    .ok_or_else(|| DarlingError::custom("path has no segments"))?
                    .ident
                    .to_string();

                if last.chars().next().is_some_and(char::is_uppercase) {
                    Ok(Self::ConstPath(path.clone()))
                } else {
                    Ok(Self::FuncPath(path.clone()))
                }
            }
            NestedMeta::Meta(syn::Meta::NameValue(nv)) => Err(DarlingError::custom(format!(
                "NameValue not supported here: {nv:?}"
            ))),
            NestedMeta::Meta(syn::Meta::List(list)) => Err(DarlingError::custom(format!(
                "Nested list not supported here: {list:?}"
            ))),
            NestedMeta::NameValueInvalidExpr(value) => Self::from_invalid_expr(value),
        }
    }
}

impl HasSchemaPart for Arg {
    fn schema_part(&self, paths: &CratePaths) -> TokenStream {
        let model = &paths.model;
        match self {
            Self::Bool(v) => quote!(#model::node::Arg::Bool(#v)),
            Self::Char(v) => quote!(#model::node::Arg::Char(#v)),
            Self::Number(v) => {
                let num = quote_one(v, |value| ArgNumber::schema_part(value, paths));
                quote!(#model::node::Arg::Number(#num))
            }
            Self::ConstPath(p) => {
                let path = quote_one(p, to_str_lit);
                quote!(#model::node::Arg::ConstPath(#path))
            }
            Self::FuncPath(p) => {
                let path = quote_one(p, to_str_lit);
                quote!(#model::node::Arg::FuncPath(#path))
            }
            Self::String(v) => quote!(#model::node::Arg::String(#v)),
        }
    }
}

impl ToTokens for Arg {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let q = match self {
            Self::Bool(v) => quote!(#v),
            Self::Char(v) => quote!(#v),       //
            Self::Number(v) => quote!(#v),     // already prints `1u8`, `42i32`, etc.
            Self::ConstPath(p) => quote!(#p),  // plain constant
            Self::FuncPath(p) => quote!(#p()), // function call
            Self::String(v) => quote!(#v),
        };

        tokens.extend(q);
    }
}

///
/// Args
/// Generic re-useable list of arguments
///

#[derive(Clone, Debug, Default)]
pub struct Args(Vec<Arg>);

impl Args {
    #[must_use]
    pub(crate) const fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub(crate) fn iter(&self) -> std::slice::Iter<'_, Arg> {
        self.0.iter()
    }
}

impl FromMeta for Args {
    fn from_list(items: &[NestedMeta]) -> Result<Self, DarlingError> {
        let mut args = Vec::new();

        for item in items {
            args.push(Arg::from_nested_meta(item)?);
        }

        Ok(Self(args))
    }
}

impl HasSchemaPart for Args {
    fn schema_part(&self, paths: &CratePaths) -> TokenStream {
        let model = &paths.model;
        let args = quote_slice(&self.0, |value| Arg::schema_part(value, paths));

        quote! {
            #model::node::Args(#args)
        }
    }
}

///
/// TESTS
///

#[cfg(test)]
mod arg_tests {
    use super::*;
    use syn::parse_quote;

    #[test]
    fn quoted_paths_remain_literal_defaults_and_arguments() {
        for text in ["::1", "a::b", "std::string", "Self::CONST", "42", ""] {
            let literal = LitStr::new(text, proc_macro2::Span::call_site());
            let arg = Arg::from_meta(&parse_quote!(default = #literal)).unwrap();
            assert!(matches!(&arg, Arg::String(value) if value.value() == text));
            assert_eq!(
                arg.to_token_stream().to_string(),
                quote!(#literal).to_string()
            );
            let args = Args::from_list(&[NestedMeta::Lit(Lit::Str(literal.clone()))]).unwrap();
            assert!(matches!(&args.0[0], Arg::String(value) if value.value() == text));
            let schema = arg.schema_part(&CratePaths::test());
            assert!(schema.to_string().contains("Arg :: String"));
        }
    }

    #[test]
    fn bare_paths_work_in_defaults_and_argument_lists() {
        for (path, is_constant) in [
            (parse_quote!(String::new), false),
            (parse_quote!(crate::LIMIT), true),
            (parse_quote!(factory), false),
        ] {
            let default = Arg::from_meta(&parse_quote!(default = #path)).unwrap();
            let argument = Arg::from_nested_meta(&NestedMeta::Meta(syn::Meta::Path(path))).unwrap();
            assert_eq!(
                default.to_token_stream().to_string(),
                argument.to_token_stream().to_string()
            );
            assert_eq!(matches!(default, Arg::ConstPath(_)), is_constant);
        }
        assert!(Arg::from_meta(&parse_quote!(default = factory())).is_err());
        assert!(Arg::from_meta(&parse_quote!(default = 1 + 2)).is_err());
    }

    #[test]
    fn test_bool_parsing() {
        let lit = parse_quote!(true);
        if let Ok(Arg::Bool(b)) = Arg::from_value(&lit) {
            assert!(b, "Parsed boolean should be true");
        } else {
            panic!("Expected Bool variant");
        }
    }

    #[test]
    fn test_char_parsing() {
        let lit = parse_quote!('a');
        if let Ok(Arg::Char(c)) = Arg::from_value(&lit) {
            assert_eq!(c, 'a', "Parsed char should be 'a'");
        } else {
            panic!("Expected Char variant");
        }
    }

    #[test]
    fn test_number_parsing() {
        let lit = parse_quote!(42);
        if let Ok(Arg::Number(num)) = Arg::from_value(&lit) {
            assert_eq!(num, ArgNumber::Int32(42), "Parsed number does not match");
        } else {
            panic!("Expected Number variant");
        }
    }

    #[test]
    fn test_const_path_parsing() {
        let arg: Arg = Arg::from_nested_meta(&parse_quote!(MY_CONST)).unwrap();
        match arg {
            Arg::ConstPath(path) => {
                assert_eq!(path.segments.last().unwrap().ident.to_string(), "MY_CONST");
            }
            _ => panic!("Expected ConstPath variant"),
        }
    }

    #[test]
    fn test_func_path_parsing() {
        let arg: Arg = Arg::from_nested_meta(&parse_quote!(my_func)).unwrap();
        match arg {
            Arg::FuncPath(path) => {
                assert_eq!(path.segments.last().unwrap().ident.to_string(), "my_func");
            }
            _ => panic!("Expected FuncPath variant"),
        }
    }

    #[test]
    fn test_invalid_input() {
        let lit = parse_quote!(b"invalid");
        assert!(
            Arg::from_value(&lit).is_err(),
            "Expected an error for unsupported literal type."
        );
    }
}