statum-macros 0.6.7

Proc macros for representing legal workflow and protocol states explicitly in Rust
Documentation
use quote::ToTokens;
use syn::{Attribute, Expr, ExprLit, Lit, LitStr, Type};

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PresentationAttr {
    pub label: Option<String>,
    pub description: Option<String>,
    pub metadata: Option<String>,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PresentationTypesAttr {
    pub machine: Option<String>,
    pub state: Option<String>,
    pub transition: Option<String>,
}

impl PresentationTypesAttr {
    pub fn parse_machine_type(&self) -> syn::Result<Option<Type>> {
        parse_optional_type(self.machine.as_deref())
    }

    pub fn parse_state_type(&self) -> syn::Result<Option<Type>> {
        parse_optional_type(self.state.as_deref())
    }

    pub fn parse_transition_type(&self) -> syn::Result<Option<Type>> {
        parse_optional_type(self.transition.as_deref())
    }
}

pub fn parse_present_attrs(attrs: &[Attribute]) -> syn::Result<Option<PresentationAttr>> {
    let mut presentation = PresentationAttr::default();
    let mut found = false;

    for attr in attrs.iter().filter(|attr| attr.path().is_ident("present")) {
        found = true;
        attr.parse_nested_meta(|meta| {
            let path = meta.path.clone();
            let Some(ident) = path.get_ident() else {
                return Err(syn::Error::new_spanned(
                    &path,
                    "Error: `#[present(...)]` keys must be simple identifiers like `label = \"...\"`.",
                ));
            };

            let value = meta.value()?;
            let value_span = value.span();
            let expr: Expr = value.parse()?;
            match ident.to_string().as_str() {
                "label" => {
                    let lit = expect_string_literal(&expr, ident, value_span)?;
                    assign_unique_string_slot(
                        &mut presentation.label,
                        ident,
                        lit.value(),
                        "present",
                    )?;
                }
                "description" => {
                    let lit = expect_string_literal(&expr, ident, value_span)?;
                    assign_unique_string_slot(
                        &mut presentation.description,
                        ident,
                        lit.value(),
                        "present",
                    )?;
                }
                "metadata" => {
                    assign_unique_string_slot(
                        &mut presentation.metadata,
                        ident,
                        expr.to_token_stream().to_string(),
                        "present",
                    )?;
                }
                _ => {
                    return Err(syn::Error::new_spanned(
                        ident,
                        format!(
                            "Error: unknown `#[present(...)]` key `{}`.\nSupported keys: `label`, `description`, `metadata`.",
                            ident
                        ),
                    ));
                }
            }
            Ok(())
        })?;
    }

    if found {
        Ok(Some(presentation))
    } else {
        Ok(None)
    }
}

pub fn parse_presentation_types_attr(
    attrs: &[Attribute],
) -> syn::Result<Option<PresentationTypesAttr>> {
    let mut presentation_types = PresentationTypesAttr::default();
    let mut found = false;

    for attr in attrs
        .iter()
        .filter(|attr| attr.path().is_ident("presentation_types"))
    {
        found = true;
        attr.parse_nested_meta(|meta| {
            let path = meta.path.clone();
            let Some(ident) = path.get_ident() else {
                return Err(syn::Error::new_spanned(
                    &path,
                    "Error: `#[presentation_types(...)]` keys must be simple identifiers like `state = MyStateMeta`.",
                ));
            };

            let value = meta.value()?;
            let ty: Type = value.parse()?;
            let ty_string = ty.to_token_stream().to_string();

            match ident.to_string().as_str() {
                "machine" => {
                    assign_unique_string_slot(
                        &mut presentation_types.machine,
                        ident,
                        ty_string,
                        "presentation_types",
                    )?;
                }
                "state" => {
                    assign_unique_string_slot(
                        &mut presentation_types.state,
                        ident,
                        ty_string,
                        "presentation_types",
                    )?;
                }
                "transition" => {
                    assign_unique_string_slot(
                        &mut presentation_types.transition,
                        ident,
                        ty_string,
                        "presentation_types",
                    )?;
                }
                _ => {
                    return Err(syn::Error::new_spanned(
                        ident,
                        format!(
                            "Error: unknown `#[presentation_types(...)]` key `{}`.\nSupported keys: `machine`, `state`, `transition`.",
                            ident
                        ),
                    ));
                }
            }

            Ok(())
        })?;
    }

    if found {
        Ok(Some(presentation_types))
    } else {
        Ok(None)
    }
}

pub fn strip_present_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
    attrs.iter()
        .filter(|attr| !attr.path().is_ident("present"))
        .cloned()
        .collect()
}

fn expect_string_literal(
    expr: &Expr,
    ident: &syn::Ident,
    span: proc_macro2::Span,
) -> Result<LitStr, syn::Error> {
    let Expr::Lit(ExprLit {
        lit: Lit::Str(lit), ..
    }) = expr
    else {
        return Err(syn::Error::new(
            span,
            format!(
                "Error: `#[present({ident} = ...)]` expects a string literal.\nFix: write `#[present({ident} = \"...\")]`."
            ),
        ));
    };

    Ok(lit.clone())
}

fn assign_unique_string_slot(
    slot: &mut Option<String>,
    ident: &syn::Ident,
    value: String,
    attr_name: &str,
) -> Result<(), syn::Error> {
    if slot.is_some() {
        let field_label = match attr_name {
            "present" => "presentation field".to_string(),
            _ => format!("{attr_name} field"),
        };
        return Err(syn::Error::new_spanned(
            ident,
            format!(
                "Error: duplicate `#[{attr_name}(...)]` key `{ident}`.\nFix: specify each {field_label} at most once per item.",
            ),
        ));
    }

    *slot = Some(value);
    Ok(())
}

fn parse_optional_type(value: Option<&str>) -> syn::Result<Option<Type>> {
    value
        .map(syn::parse_str::<Type>)
        .transpose()
}