cratestack-macros 0.5.2

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
Documentation
//! Relation codegen: `@relation` attribute parsing + per-model relation
//! path emission for the typed query builder, plus the axum-side
//! filter-key/order-key match-arm generators.
//!
//! Submodules:
//! - [`types`] / [`parse`]: parser + shared types (`RelationLink`).
//! - [`query_guard`]: prefix-match guard for `where`-side relation filters.
//! - [`order_arms`] / [`order_targets`]: orderBy arm + SQL fragment
//!   computation through to-one relation chains.
//! - [`flat`]: the per-model `RelPath` / `RelToMany` / `Field` emitter.
//!   One arrival type per model, path carried as runtime data — see the
//!   module docs for why this replaced the per-path recursive emitter
//!   (cratestack#252).
//! - [`root`]: per-(model, relation) `Root` entry carrying `as_include()`.
//! - [`include_arm`] / [`include_validation`]: serializer / validator arms
//!   consumed by the per-model axum module.
//! - [`filter_builders`]: shared per-arity append helpers.

mod filter_builders;
mod flat;
mod include_arm;
mod include_validation;
mod order_arms;
mod order_targets;
mod parse;
mod root;
mod types;

mod query_guard;

pub(crate) use flat::generate_model_path_types;
pub(crate) use include_arm::generate_relation_include_arm;
pub(crate) use include_validation::{
    generate_relation_include_fields_validation_arm, generate_relation_include_path_validation_arm,
};
pub(crate) use order_arms::{collect_allowed_sort_keys, generate_relation_order_by_arms};
pub(crate) use parse::parse_relation_attribute;
pub(crate) use query_guard::generate_relation_query_guard;
pub(crate) use root::generate_relation_root_module;
pub(crate) use types::{RelationLink, relation_link};

#[cfg(test)]
mod tests {
    use cratestack_core::{Attribute, Field, SourceSpan, TypeRef};

    use super::parse::{parse_relation_attribute, split_top_level};

    fn span() -> SourceSpan {
        SourceSpan {
            start: 0,
            end: 0,
            line: 1,
        }
    }

    fn field_with_relation(raw: &str) -> Field {
        Field {
            docs: Vec::new(),
            name: "author".to_owned(),
            name_span: span(),
            ty: TypeRef {
                name: "User".to_owned(),
                name_span: span(),
                arity: cratestack_core::TypeArity::Required,
                generic_args: Vec::new(),
            },
            attributes: vec![Attribute {
                raw: raw.to_owned(),
                span: span(),
            }],
            span: span(),
        }
    }

    #[test]
    fn split_top_level_ignores_nested_brackets() {
        let items = split_top_level("fields:[userId], references:[id], map:[a,b(c,d)]", ',');
        assert_eq!(
            items,
            vec!["fields:[userId]", "references:[id]", "map:[a,b(c,d)]"]
        );
    }

    #[test]
    fn parse_relation_attribute_extracts_fields_and_references() {
        let field = field_with_relation("@relation(fields:[userId], references:[id])");
        let parsed = parse_relation_attribute(&field).expect("relation attribute should parse");
        assert_eq!(parsed.fields, vec!["userId".to_owned()]);
        assert_eq!(parsed.references, vec!["id".to_owned()]);
    }

    #[test]
    fn parse_relation_attribute_rejects_unknown_keys() {
        let field = field_with_relation("@relation(fields:[userId], ref:[id])");
        assert!(parse_relation_attribute(&field).is_none());
    }
}