miden-base-macros 0.11.0

Provides proc macro support for Miden rollup SDK
Documentation
use std::{
    collections::{BTreeSet, HashSet},
    fmt::Write,
    fs,
    io::ErrorKind,
};

use proc_macro::Span;
use semver::Version;
use syn::spanned::Spanned;

use crate::{
    component_macro::{CORE_TYPES_PACKAGE, ComponentMethod, MethodReturn, to_kebab_case},
    types::{ExportedTypeDef, ExportedTypeKind, ensure_custom_type_defined},
    util::generated_wit_folder,
};

/// Writes the generated component WIT to the crate's `wit` directory so that dependent targets can
/// reference it via manifest metadata.
pub fn write_component_wit_file(
    call_site_span: Span,
    wit_source: &str,
    package_name: &str,
) -> Result<(), syn::Error> {
    let sanitized_package_name = sanitize_package_name(package_name);
    let autogenerated_wit_folder = generated_wit_folder()?;
    let wit_path = autogenerated_wit_folder.join(format!("{sanitized_package_name}.wit"));

    let needs_write = match fs::read_to_string(&wit_path) {
        Ok(existing) => existing != wit_source,
        Err(err) if err.kind() == ErrorKind::NotFound => true,
        Err(err) => {
            return Err(syn::Error::new(
                call_site_span.into(),
                format!("failed to read existing WIT file '{}': {err}", wit_path.display()),
            ));
        }
    };

    if needs_write {
        fs::write(&wit_path, wit_source).map_err(|err| {
            syn::Error::new(
                call_site_span.into(),
                format!("failed to write WIT file '{}': {err}", wit_path.display()),
            )
        })?;
    }

    Ok(())
}

/// Renders the inline WIT source describing the component interface exported by the `impl` block.
pub fn build_component_wit(
    component_package: &str,
    component_version: &Version,
    interface_name: &str,
    world_name: &str,
    type_imports: &BTreeSet<String>,
    methods: &[ComponentMethod],
    exported_types: &[ExportedTypeDef],
) -> Result<String, syn::Error> {
    let package_with_version = if component_package.contains('@') {
        component_package.to_string()
    } else {
        format!("{component_package}@{component_version}")
    };

    let mut wit_source = String::new();
    let _ = writeln!(wit_source, "// This file is auto-generated by the `#[component]` macro.");
    let _ = writeln!(wit_source, "// Do not edit this file manually.");
    wit_source.push('\n');
    let _ = writeln!(wit_source, "package {package_with_version};");
    wit_source.push('\n');
    let _ = writeln!(wit_source, "use {CORE_TYPES_PACKAGE};");
    wit_source.push('\n');

    let exported_type_names: HashSet<String> =
        exported_types.iter().map(|def| def.wit_name.clone()).collect();

    let mut combined_core_imports = type_imports.clone();
    for exported in exported_types {
        match &exported.kind {
            ExportedTypeKind::Record { fields } => {
                for field in fields {
                    ensure_custom_type_defined(
                        &field.ty,
                        &exported_type_names,
                        Span::call_site().into(),
                    )?;
                    if !field.ty.is_custom {
                        combined_core_imports.insert(field.ty.wit_name.clone());
                    }
                }
            }
            ExportedTypeKind::Variant { variants } => {
                for variant in variants {
                    if let Some(payload) = &variant.payload {
                        ensure_custom_type_defined(
                            payload,
                            &exported_type_names,
                            Span::call_site().into(),
                        )?;
                        if !payload.is_custom {
                            combined_core_imports.insert(payload.wit_name.clone());
                        }
                    }
                }
            }
        }
    }

    let _ = writeln!(wit_source, "interface {interface_name} {{");

    if !combined_core_imports.is_empty() {
        let imports = combined_core_imports.iter().cloned().collect::<Vec<_>>().join(", ");
        let _ = writeln!(wit_source, "    use core-types.{{{imports}}};");
        wit_source.push('\n');
    }

    for exported in exported_types {
        match &exported.kind {
            ExportedTypeKind::Record { fields } => {
                let _ = writeln!(wit_source, "    record {} {{", exported.wit_name);
                for field in fields {
                    let field_name = to_kebab_case(&field.name);
                    let _ = writeln!(wit_source, "        {}: {},", field_name, field.ty.wit_name);
                }
                let _ = writeln!(wit_source, "    }}\n");
            }
            ExportedTypeKind::Variant { variants } => {
                let _ = writeln!(wit_source, "    variant {} {{", exported.wit_name);
                for variant in variants {
                    if let Some(payload) = &variant.payload {
                        let _ = writeln!(
                            wit_source,
                            "        {}({}),",
                            variant.wit_name, payload.wit_name
                        );
                    } else {
                        let _ = writeln!(wit_source, "        {},", variant.wit_name);
                    }
                }
                let _ = writeln!(wit_source, "    }}\n");
            }
        }
    }

    for method in methods {
        for param in &method.params {
            ensure_custom_type_defined(
                &param.type_ref,
                &exported_type_names,
                param.user_ty.span(),
            )?;
        }
        if let MethodReturn::Type { type_ref, user_ty } = &method.return_info {
            ensure_custom_type_defined(type_ref, &exported_type_names, user_ty.span())?;
        }

        let signature = if method.params.is_empty() {
            match &method.return_info {
                MethodReturn::Unit => format!("    {}: func();", method.wit_name),
                MethodReturn::Type { type_ref, .. } => {
                    format!("    {}: func() -> {};", method.wit_name, type_ref.wit_name)
                }
            }
        } else {
            let params = method
                .params
                .iter()
                .map(|param| format!("{}: {}", param.wit_param_name, param.type_ref.wit_name))
                .collect::<Vec<_>>()
                .join(", ");
            match &method.return_info {
                MethodReturn::Unit => format!("    {}: func({});", method.wit_name, params),
                MethodReturn::Type { type_ref, .. } => {
                    format!("    {}: func({}) -> {};", method.wit_name, params, type_ref.wit_name)
                }
            }
        };
        let _ = writeln!(wit_source, "{signature}");
    }

    let _ = writeln!(wit_source, "}}");
    wit_source.push('\n');
    let _ = writeln!(wit_source, "world {world_name} {{");
    let _ = writeln!(wit_source, "    export {interface_name};");
    let _ = writeln!(wit_source, "}}");

    Ok(wit_source)
}

fn sanitize_package_name(package_name: &str) -> String {
    let mut sanitized = package_name
        .chars()
        .map(|ch| match ch {
            'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
            _ => '-',
        })
        .collect::<String>();

    if sanitized.is_empty() {
        sanitized.push_str("component");
    }

    sanitized
}