statum-macros 0.7.0

Proc macros for representing legal workflow and protocol states explicitly in Rust
Documentation
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{format_ident, quote};
use syn::{Fields, Item, Type, parse_macro_input};

use crate::relation::parse_machine_reference_target;
use crate::{ItemTarget, resolved_current_module_path};

pub fn parse_machine_ref(attr: TokenStream, item: TokenStream) -> TokenStream {
    let target_type = parse_macro_input!(attr as Type);
    let item = parse_macro_input!(item as Item);
    let item_struct = match item {
        Item::Struct(item_struct) => item_struct,
        other => return invalid_machine_ref_target_error(&other).into(),
    };

    if !item_struct.generics.params.is_empty() {
        return syn::Error::new_spanned(
            &item_struct.generics,
            format!(
                "Error: `#[machine_ref(...)]` on `{}` does not support generics in v1.\nFix: declare a concrete nominal wrapper type and attach `#[machine_ref(...)]` there.",
                item_struct.ident
            ),
        )
        .to_compile_error()
        .into();
    }

    match &item_struct.fields {
        Fields::Named(_) | Fields::Unnamed(_) => {}
        Fields::Unit => {
            return syn::Error::new_spanned(
                &item_struct.fields,
                format!(
                    "Error: `#[machine_ref(...)]` on `{}` requires a nominal struct or tuple struct with stored data.\nFix: wrap the opaque reference value in a field and attach `#[machine_ref(...)]` to that struct.",
                    item_struct.ident
                ),
            )
            .to_compile_error()
            .into();
        }
    }

    let line_number = item_struct.ident.span().start().line;
    let module_path = match resolved_current_module_path(item_struct.ident.span(), "#[machine_ref]") {
        Ok(path) => path,
        Err(err) => return err,
    };
    let (machine_path, state_name) = match parse_machine_reference_target(&target_type, &module_path) {
        Ok(target) => target,
        Err(err) => return err.into(),
    };
    let rust_type_path = format!("{}::{}", module_path, item_struct.ident);
    let rust_type_path_lit = syn::LitStr::new(&rust_type_path, Span::call_site());
    let machine_path_tokens = machine_path.iter().map(|segment| {
        let segment = syn::LitStr::new(segment, Span::call_site());
        quote! { #segment }
    });
    let state_name_lit = syn::LitStr::new(&state_name, Span::call_site());
    let targets_ident = linked_reference_targets_ident(&rust_type_path, line_number);
    let type_name_ident = linked_reference_type_name_ident(&rust_type_path, line_number);
    let target_machine_type_name_ident =
        linked_reference_target_machine_type_name_ident(&rust_type_path, line_number);
    let registration_ident = linked_reference_registration_ident(&rust_type_path, line_number);
    let item_ident = &item_struct.ident;

    quote! {
        #item_struct

        #[doc(hidden)]
        static #targets_ident: &[&str] = &[#(#machine_path_tokens),*];

        #[doc(hidden)]
        fn #type_name_ident() -> &'static str {
            ::core::any::type_name::<#item_ident>()
        }

        #[doc(hidden)]
        fn #target_machine_type_name_ident() -> &'static str {
            ::core::any::type_name::<#target_type>()
        }

        impl statum::MachineReference for #item_ident {
            const TARGET: statum::MachineReferenceTarget = statum::MachineReferenceTarget {
                machine_path: #targets_ident,
                state: #state_name_lit,
            };
        }

        #[doc(hidden)]
        #[statum::__private::linkme::distributed_slice(statum::__private::__STATUM_LINKED_REFERENCE_TYPES)]
        #[linkme(crate = statum::__private::linkme)]
        static #registration_ident: statum::__private::LinkedReferenceTypeDescriptor =
            statum::__private::LinkedReferenceTypeDescriptor {
                rust_type_path: #rust_type_path_lit,
                resolved_type_name: #type_name_ident,
                to_machine_path: <#item_ident as statum::MachineReference>::TARGET.machine_path,
                resolved_target_machine_type_name: #target_machine_type_name_ident,
                to_state: <#item_ident as statum::MachineReference>::TARGET.state,
            };
    }
    .into()
}

fn invalid_machine_ref_target_error(item: &Item) -> proc_macro2::TokenStream {
    let target = ItemTarget::from(item);
    let item_name = target
        .name()
        .map(|name| format!(" `{name}`"))
        .unwrap_or_default();
    let message = format!(
        "Error: `#[machine_ref(...)]` must be applied to a nominal struct or tuple struct, but this item is {} {}{}.\nFix: apply `#[machine_ref(...)]` to a nominal wrapper struct like `struct TaskId(Uuid);`.",
        target.article(),
        target.kind(),
        item_name,
    );
    quote! { compile_error!(#message); }
}

fn linked_reference_registration_ident(rust_type_path: &str, line_number: usize) -> syn::Ident {
    format_ident!(
        "__STATUM_LINKED_REFERENCE_TYPE_{:016X}",
        stable_hash(&format!("{rust_type_path}::{line_number}::reference"))
    )
}

fn linked_reference_targets_ident(rust_type_path: &str, line_number: usize) -> syn::Ident {
    format_ident!(
        "__STATUM_LINKED_REFERENCE_TARGET_{:016X}",
        stable_hash(&format!("{rust_type_path}::{line_number}::target"))
    )
}

fn linked_reference_type_name_ident(rust_type_path: &str, line_number: usize) -> syn::Ident {
    format_ident!(
        "__statum_machine_ref_type_name_{:016x}",
        stable_hash(&format!("{rust_type_path}::{line_number}::type_name"))
    )
}

fn linked_reference_target_machine_type_name_ident(
    rust_type_path: &str,
    line_number: usize,
) -> syn::Ident {
    format_ident!(
        "__statum_machine_ref_target_machine_type_name_{:016x}",
        stable_hash(&format!(
            "{rust_type_path}::{line_number}::target_machine_type_name"
        ))
    )
}

fn stable_hash(input: &str) -> u64 {
    let mut hash = 0xcbf29ce484222325u64;
    for byte in input.as_bytes() {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(0x100000001b3);
    }
    hash
}