icydb-model-macros 0.301.0

Procedural macros for IcyDB application models
Documentation
//! Generated crate-path resolution.
//!
//! Macro output names package owners rather than assuming the dependency key
//! used by the consuming crate. Attribute macros may override the model path
//! explicitly; runtime paths and helper derives use Cargo package resolution.

use darling::ast::NestedMeta;
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{Expr, Lit, Path};

const ICYDB_PACKAGE: &str = "icydb";
const MODEL_PACKAGE: &str = "icydb-model";

pub(crate) fn icydb_runtime_path() -> Result<TokenStream, darling::Error> {
    resolve_path(ICYDB_PACKAGE)
}

/// Optional dependency paths consumed before node-specific argument parsing.
#[derive(Default)]
pub(crate) struct CratePathOverrides {
    model: Option<Path>,
}

impl CratePathOverrides {
    /// Remove crate-path options from one attribute's node arguments.
    pub(crate) fn extract(args: &mut Vec<NestedMeta>) -> Result<Self, darling::Error> {
        let mut overrides = Self::default();
        let mut retained = Vec::with_capacity(args.len());

        for arg in args.drain(..) {
            let NestedMeta::Meta(syn::Meta::NameValue(name_value)) = &arg else {
                retained.push(arg);
                continue;
            };

            let target = if name_value.path.is_ident("model_crate") {
                &mut overrides.model
            } else {
                retained.push(arg);
                continue;
            };

            if target.is_some() {
                return Err(darling::Error::custom(format!(
                    "{} may be specified only once",
                    name_value.path.to_token_stream()
                ))
                .with_span(&name_value.path));
            }
            let Expr::Lit(literal) = &name_value.value else {
                return Err(darling::Error::custom(
                    "crate-path overrides require a quoted Rust path",
                )
                .with_span(&name_value.value));
            };
            let Lit::Str(path) = &literal.lit else {
                return Err(darling::Error::custom(
                    "crate-path overrides require a quoted Rust path",
                )
                .with_span(&literal.lit));
            };
            *target = Some(path.parse().map_err(|_| {
                darling::Error::custom("crate-path override is not a valid Rust path")
                    .with_span(path)
            })?);
        }

        *args = retained;
        Ok(overrides)
    }
}

/// Resolved dependencies passed explicitly to generated token construction.
#[derive(Debug)]
pub struct CratePaths {
    pub(crate) model: TokenStream,
    pub(crate) runtime: TokenStream,
}

impl CratePathOverrides {
    pub(crate) fn resolve(&self, runtime: bool) -> Result<CratePaths, darling::Error> {
        Ok(CratePaths {
            model: resolve_model_path(self.model.as_ref())?,
            // The selected macro surface owns emission. Cargo manifest presence
            // cannot prove that a dependency is available to this compilation.
            runtime: if runtime {
                icydb_runtime_path()?
            } else {
                TokenStream::new()
            },
        })
    }
}

fn resolve_model_path(explicit: Option<&Path>) -> Result<TokenStream, darling::Error> {
    if let Some(path) = explicit {
        return Ok(quote!(#path));
    }

    if let Ok(found) = crate_name(MODEL_PACKAGE) {
        return Ok(found_crate_path(found));
    }

    let facade = crate_name(ICYDB_PACKAGE).map_err(|error| {
        darling::Error::custom(format!(
            "generated output requires a direct `{MODEL_PACKAGE}` dependency, a direct `{ICYDB_PACKAGE}` facade dependency, or an explicit `model_crate = \"...\"` override: {error}"
        ))
    })?;
    let facade = found_crate_path(facade);
    Ok(quote!(#facade::model))
}

fn resolve_path(package: &str) -> Result<TokenStream, darling::Error> {
    crate_name(package).map(found_crate_path).map_err(|error| {
        darling::Error::custom(format!(
            "generated output requires a direct `{package}` dependency: {error}"
        ))
    })
}

fn found_crate_path(found: FoundCrate) -> TokenStream {
    match found {
        FoundCrate::Itself => quote!(crate),
        FoundCrate::Name(name) => {
            let ident = syn::Ident::new(name.as_str(), proc_macro2::Span::call_site());
            quote!(::#ident)
        }
    }
}

#[cfg(test)]
mod tests;

#[cfg(test)]
mod helper_tests;