miden-base-macros 0.13.0

Provides proc macro support for Miden rollup SDK
Documentation
//! Helpers for rendering inline WIT documents shared across SDK proc macros.

use miden_formatting::prettier::{Document, indent, nl, text};
use semver::Version;

/// Builds inline WIT documents shared by the SDK macros.
pub(crate) struct WitBuilder {
    source: Document,
}

impl WitBuilder {
    /// Initializes a WIT document with the generator banner and package declaration.
    pub(crate) fn new(generated_by: &str, package_name: &str, package_version: &Version) -> Self {
        let package_with_version = package_with_version(package_name, package_version);
        let mut builder = Self {
            source: Document::Empty,
        };
        builder.push_line(format!("// This file is auto-generated by the `{generated_by}` macro."));
        builder.push_line("// Do not edit this file manually.");
        builder.blank_line();
        builder.push_line(format!("package {package_with_version};"));
        builder.blank_line();
        builder
    }

    /// Writes a top-level `use` directive.
    pub(crate) fn use_path(&mut self, path: &str) {
        self.push_line(format!("use {path};"));
    }

    /// Inserts a blank line at the current top-level position.
    pub(crate) fn blank_line(&mut self) {
        self.source += nl();
    }

    /// Writes an `interface` block and returns the closure result.
    pub(crate) fn interface<T>(&mut self, name: &str, build: impl FnOnce(&mut WitBody) -> T) -> T {
        let mut body = WitBody::new();
        let result = build(&mut body);
        self.push_block(format!("interface {name} {{"), body.finish());
        result
    }

    /// Writes a `world` block and returns the closure result.
    pub(crate) fn world<T>(&mut self, name: &str, build: impl FnOnce(&mut WitBody) -> T) -> T {
        let mut body = WitBody::new();
        let result = build(&mut body);
        self.push_block(format!("world {name} {{"), body.finish());
        result
    }

    /// Finishes rendering and returns the final WIT source.
    pub(crate) fn finish(self) -> String {
        self.source.to_string() + "\n"
    }

    /// Appends a top-level line to the document.
    fn push_line(&mut self, line: impl Into<String>) {
        if !self.source.is_empty() {
            self.source += nl();
        }
        self.source += text(line.into());
    }

    /// Appends a top-level block and indents its nested body using the formatter primitives.
    fn push_block(&mut self, header: impl Into<String>, body: Document) {
        self.push_line(header);
        if body.is_empty() {
            self.source += nl();
        } else {
            self.source += indent(4, nl() + body) + nl();
        }
        self.source += text("}");
    }
}

/// Builds the body of a WIT block before the enclosing formatter applies indentation.
pub(crate) struct WitBody {
    source: Document,
}

impl WitBody {
    /// Creates a new WIT block body builder.
    fn new() -> Self {
        Self {
            source: Document::Empty,
        }
    }

    /// Writes a single line to the current WIT block body.
    pub(crate) fn line(&mut self, line: &str) {
        if !self.source.is_empty() {
            self.source += nl();
        }
        self.source += text(line);
    }

    /// Inserts a blank line inside the current WIT block.
    pub(crate) fn blank_line(&mut self) {
        self.source += nl();
    }

    /// Writes a nested block whose body is indented by the formatter.
    pub(crate) fn block(&mut self, header: &str, build: impl FnOnce(&mut WitBody)) {
        if !self.source.is_empty() {
            self.source += nl();
        }
        self.source += text(header);
        let mut body = WitBody::new();
        build(&mut body);
        let body = body.finish();
        if body.is_empty() {
            self.source += nl();
        } else {
            self.source += indent(4, nl() + body) + nl();
        }
        self.source += text("}");
    }

    /// Finishes rendering the current WIT block body.
    fn finish(self) -> Document {
        self.source
    }
}

/// Formats a WIT package identifier with an explicit version suffix.
fn package_with_version(package_name: &str, package_version: &Version) -> String {
    if package_name.contains('@') {
        package_name.to_string()
    } else {
        format!("{package_name}@{package_version}")
    }
}

#[cfg(test)]
mod tests {
    use semver::Version;

    use super::WitBuilder;

    #[test]
    fn renders_nested_blocks_with_expected_indentation() {
        let mut wit = WitBuilder::new("#[test]", "miden:test", &Version::new(1, 0, 0));
        wit.use_path("miden:base/core-types@1.0.0");
        wit.blank_line();
        wit.interface("foo", |interface| {
            interface.line("use core-types.{word};");
            interface.blank_line();
            interface.block("record payload {", |record| {
                record.line("value: word,");
            });
        });
        wit.blank_line();
        wit.world("foo-world", |world| {
            world.line("export foo;");
        });

        let expected = r#"// This file is auto-generated by the `#[test]` macro.
// Do not edit this file manually.

package miden:test@1.0.0;

use miden:base/core-types@1.0.0;

interface foo {
    use core-types.{word};

    record payload {
        value: word,
    }
}

world foo-world {
    export foo;
}
"#;

        assert_eq!(wit.finish(), expected);
    }
}