cratestack-macros 0.5.0

Rust-native schema-first framework for typed HTTP APIs, generated clients, and backend services.
Documentation
//! Per-model `pub mod <model_snake> { ... }` field accessor module:
//! `FieldRef<...>` helpers for every scalar, relation path entry
//! methods, plus the [`selection_module`](super::selection_module)
//! sub-module.

use std::collections::BTreeSet;

use cratestack_core::Model;
use quote::quote;

use crate::relation::{generate_model_path_types, generate_relation_root_module};
use crate::shared::{
    ident, relation_model_fields, rust_type_tokens, scalar_model_fields, to_snake_case,
};

use super::selection_module::generate_selection_module;

/// Which schema-emission kind is asking for a field module. Server &
/// embedded both emit `*_MODEL` descriptors so they can carry the full
/// surface; client-side does not, so emissions that hard-reference those
/// descriptors must be suppressed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FieldModuleKind {
    /// Reachable from `include_server_schema!` / `include_embedded_schema!`.
    /// `*_MODEL` descriptors are in scope.
    Server,
    /// Reachable from `include_client_schema!`. `*_MODEL` descriptors are
    /// NOT emitted by the client composer, so any code that references
    /// them (today: `Path::as_include()`) must be skipped — otherwise the
    /// macro output fails to compile.
    Client,
}

pub(crate) fn generate_field_module(
    model: &Model,
    model_names: &BTreeSet<&str>,
    models: &[Model],
) -> Result<proc_macro2::TokenStream, String> {
    generate_field_module_with_kind(model, model_names, models, FieldModuleKind::Server)
}

/// Variant of [`generate_field_module`] that suppresses emissions which
/// reference `*_MODEL` descriptors. The client schema doesn't emit
/// those descriptors, so anything that hard-references them (today:
/// `as_include()` on to-one relation `Path` — see PR #47) has to be
/// skipped.
pub(crate) fn generate_client_field_module(
    model: &Model,
    model_names: &BTreeSet<&str>,
    models: &[Model],
) -> Result<proc_macro2::TokenStream, String> {
    generate_field_module_with_kind(model, model_names, models, FieldModuleKind::Client)
}

fn generate_field_module_with_kind(
    model: &Model,
    model_names: &BTreeSet<&str>,
    models: &[Model],
    kind: FieldModuleKind,
) -> Result<proc_macro2::TokenStream, String> {
    let module_ident = ident(&to_snake_case(&model.name));
    let model_ident = ident(&model.name);
    let field_fns = scalar_model_fields(model, model_names).into_iter().map(|field| {
        let function_ident = ident(&field.name);
        let field_type = rust_type_tokens(&field.ty);
        let column = to_snake_case(&field.name);
        quote! {
            #[allow(non_snake_case)]
            pub fn #function_ident() -> ::cratestack::FieldRef<super::models::#model_ident, #field_type> {
                ::cratestack::FieldRef::new(#column)
            }
        }
    });
    let relation_root_fns = relation_model_fields(model, model_names)
        .into_iter()
        .map(|field| {
            let function_ident = ident(&field.name);
            let link = crate::relation::relation_link(model, field, models)?;
            if link.is_to_many {
                // No `Root` for to-many: hand back the target model's
                // pending-quantifier type directly.
                let target_module = ident(&to_snake_case(&field.ty.name));
                let parent_table = link.parent_table.as_str();
                let parent_column = link.parent_column.as_str();
                let related_table = link.related_table.as_str();
                let related_column = link.related_column.as_str();
                Ok(quote! {
                    #[allow(non_snake_case)]
                    pub fn #function_ident() -> super::#target_module::RelToMany {
                        super::#target_module::RelToMany::__from_hops(
                            ::std::vec::Vec::from([
                                ::cratestack::RelationHop::new(
                                    #parent_table,
                                    #parent_column,
                                    #related_table,
                                    #related_column,
                                    ::cratestack::RelationQuantifier::ToOne,
                                ),
                            ]),
                        )
                    }
                })
            } else {
                let module_ident = ident(&field.name);
                Ok(quote! {
                    #[allow(non_snake_case)]
                    pub fn #function_ident() -> self::#module_ident::Root {
                        self::#module_ident::Root
                    }
                })
            }
        })
        .collect::<Result<Vec<_>, String>>()?;
    let relation_modules = relation_model_fields(model, model_names)
        .into_iter()
        .map(|relation_field| generate_relation_root_module(model, relation_field, models, kind))
        .collect::<Result<Vec<_>, String>>()?
        .into_iter()
        .flatten()
        .collect::<Vec<_>>();
    let path_types = generate_model_path_types(model, models)?;
    let selection_module = generate_selection_module(model, model_names, models)?;

    Ok(quote! {
        pub mod #module_ident {
            pub use super::*;

            #(#field_fns)*
            #(#relation_root_fns)*

            pub fn select() -> self::selection::Selection {
                self::selection::Selection::default()
            }

            pub fn include_selection() -> self::selection::IncludeSelection {
                self::selection::IncludeSelection::default()
            }

            #path_types

            #(#relation_modules)*

            #selection_module
        }
    })
}