icydb-model-macros 0.299.0

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

use crate::{
    node::field_list_arg::{
        field_or_fields_duplicate_message, parse_field_list_arg, parse_scalar_field_arg,
        reject_duplicate_fields,
    },
    prelude::*,
};
use darling::ast::NestedMeta;

///
/// Relation
///
/// Derive-side relation-edge declaration. This is proposal metadata only; the
/// generated schema node performs graph-aware validation against accepted
/// source/target field metadata.
///

#[derive(Clone, Debug)]
pub struct Relation {
    pub(crate) name: LitStr,
    pub(crate) target: Path,
    pub(crate) fields: Vec<LitStr>,
}

impl FromMeta for Relation {
    fn from_list(items: &[NestedMeta]) -> Result<Self, DarlingError> {
        let mut name = None;
        let mut target = None;
        let mut fields = None;

        for item in items {
            let NestedMeta::Meta(syn::Meta::NameValue(name_value)) = item else {
                return Err(DarlingError::custom(
                    "relation(...) supports name = \"...\", rel = \"...\", field = \"...\", and fields = [...]",
                ));
            };

            if name_value.path.is_ident("name") {
                set_relation_arg_once(
                    &mut name,
                    parse_relation_name(&name_value.value)?,
                    "relation(...) accepts only one name = \"...\" argument",
                    &name_value.path,
                )?;
                continue;
            }

            if name_value.path.is_ident("rel") {
                set_relation_arg_once(
                    &mut target,
                    parse_relation_target(&name_value.value)?,
                    "relation(...) accepts only one rel = \"...\" argument",
                    &name_value.path,
                )?;
                continue;
            }

            if name_value.path.is_ident("field") {
                let field = parse_scalar_field_arg("relation", &name_value.value)?;
                if fields.replace(vec![field]).is_some() {
                    return Err(DarlingError::custom(field_or_fields_duplicate_message(
                        "relation",
                    ))
                    .with_span(&name_value.path));
                }
                continue;
            }

            if name_value.path.is_ident("fields") {
                if fields
                    .replace(parse_field_list_arg("relation", &name_value.value)?)
                    .is_some()
                {
                    return Err(DarlingError::custom(field_or_fields_duplicate_message(
                        "relation",
                    ))
                    .with_span(&name_value.path));
                }
                continue;
            }

            return Err(DarlingError::custom(
                "relation(...) supports name = \"...\", rel = \"...\", field = \"...\", and fields = [...]",
            )
            .with_span(&name_value.path));
        }

        let Some(name) = name else {
            return Err(DarlingError::custom(
                "relation(...) requires name = \"...\"",
            ));
        };
        let Some(target) = target else {
            return Err(DarlingError::custom("relation(...) requires rel = \"...\""));
        };
        let Some(fields) = fields else {
            return Err(DarlingError::custom(
                "relation(...) requires field = \"...\" or fields = [...]",
            ));
        };

        if fields.is_empty() {
            return Err(DarlingError::custom(
                "relation(fields = []) must contain at least one field",
            ));
        }
        reject_duplicate_fields(fields.as_slice(), LitStr::value, "relation")?;

        Ok(Self {
            name,
            target,
            fields,
        })
    }
}

fn set_relation_arg_once<T>(
    target: &mut Option<T>,
    value: T,
    duplicate_message: &str,
    span: &syn::Path,
) -> Result<(), DarlingError> {
    if target.replace(value).is_some() {
        return Err(DarlingError::custom(duplicate_message).with_span(span));
    }
    Ok(())
}

impl Relation {
    pub(crate) fn validate(
        &self,
        fields: &FieldList,
        __paths: &CratePaths,
    ) -> Result<(), DarlingError> {
        let mut local_component_cardinality = None;
        for field in &self.fields {
            let field_ident = relation_field_ident(field)?;
            let Some(local_field) = fields.get(&field_ident) else {
                return Err(DarlingError::custom(format!(
                    "relation field '{}' not found",
                    field.value()
                ))
                .with_span(field));
            };

            let local_cardinality = local_field.value.cardinality();
            if local_cardinality == Cardinality::Many {
                return Err(DarlingError::custom(
                    "relation tuple component fields cannot have many cardinality",
                )
                .with_span(field));
            }
            match local_component_cardinality {
                Some(expected) if expected != local_cardinality => {
                    return Err(DarlingError::custom(
                        "relation tuple component fields must be all required or all optional",
                    )
                    .with_span(field));
                }
                Some(_) => {}
                None => local_component_cardinality = Some(local_cardinality),
            }
            if local_field.generated.is_some() {
                return Err(DarlingError::custom(
                    "relation tuple component fields cannot be generated",
                )
                .with_span(field));
            }
        }

        Ok(())
    }
}

impl HasSchemaPart for Relation {
    fn schema_part(&self, paths: &CratePaths) -> TokenStream {
        let model = &paths.model;
        let name = quote_one(&self.name, to_str_lit);
        let target = quote_one(&self.target, |value| to_path(value, paths));
        let fields = quote_slice(&self.fields, to_str_lit);

        quote! {
            #model::node::RelationEdge::new(#name, #target, #fields)
        }
    }
}

fn parse_relation_name(expr: &syn::Expr) -> Result<LitStr, DarlingError> {
    let literal = parse_relation_string_arg("name", expr)?;
    if literal.value().is_empty() {
        return Err(DarlingError::custom("relation name cannot be empty").with_span(&literal));
    }

    Ok(literal)
}

fn parse_relation_target(expr: &syn::Expr) -> Result<Path, DarlingError> {
    let literal = parse_relation_string_arg("rel", expr)?;
    syn::parse_str::<Path>(literal.value().as_str()).map_err(|_| {
        DarlingError::custom(format!(
            "relation target '{}' is not a valid Rust path",
            literal.value()
        ))
        .with_span(&literal)
    })
}

fn parse_relation_string_arg(name: &str, expr: &syn::Expr) -> Result<LitStr, DarlingError> {
    let syn::Expr::Lit(expr_lit) = expr else {
        return Err(DarlingError::custom(format!(
            "relation({name} = ...) requires a string literal"
        ))
        .with_span(expr));
    };
    let syn::Lit::Str(literal) = &expr_lit.lit else {
        return Err(DarlingError::custom(format!(
            "relation({name} = ...) requires a string literal"
        ))
        .with_span(expr));
    };

    Ok(literal.clone())
}

fn relation_field_ident(field: &LitStr) -> Result<Ident, DarlingError> {
    syn::parse_str::<Ident>(field.value().as_str()).map_err(|_| {
        DarlingError::custom(format!(
            "relation field '{}' is not a valid Rust field identifier",
            field.value()
        ))
        .with_span(field)
    })
}

#[cfg(test)]
mod tests;