Introduction
sigil-stitch is a Rust library for type-safe, import-aware, width-aware code generation across multiple languages. It combines two ideas: JavaPoet’s builder model for constructing structured code, and the Wadler-Lindig algorithm for width-aware formatting. You describe code with builders and format specifiers, and the library handles imports, name conflicts, indentation, and line breaking.
Where the ideas come from
JavaPoet’s builder model. JavaPoet (by Square) introduced the idea of building code
with CodeBlock format strings and structural Spec types (TypeSpec, FunSpec, etc.).
You write a format string like "const user: %T = getUser()", pass a TypeName for
the %T slot, and the library renders the type reference and tracks the import.
sigil-stitch adopts this model directly, extending it from Java-only to multiple languages.
Wadler-Lindig pretty printing. The pretty crate implements the Wadler-Lindig
algorithm, which decides where to break lines based on a target width. sigil-stitch
uses this via the %W (soft line break) specifier – you mark where breaks can
happen, and the algorithm decides where they should happen. Without %W, output
is rendered with direct string concatenation (no pretty-printer overhead).
Four key properties
Ergonomic multi-language. CodeBlock, TypeName, and all spec types are
language-agnostic — no generic parameter. The language enters at render time when
you call FileSpec::render() or pass &dyn CodeLang to a renderer. You can build
code blocks once and render them for different languages.
Import-aware. When you use %T with a TypeName::Importable, the library records
that import. At render time, FileSpec collects all imports from every code block,
deduplicates them, and resolves naming conflicts automatically. If two modules export a
type named User, the first one encountered keeps the simple name User and the second
gets an aliased name (e.g., OtherUser). You never write import statements by hand.
Width-aware. Place %W in a format string to mark a soft line break. When the
output fits within the target width, %W produces a space. When it doesn’t fit, %W
produces a newline with proper indentation. This is the Wadler-Lindig algorithm at
work, via the pretty crate. You pass the target width to FileSpec::render(width),
and the same code blocks produce different layouts for different widths.
Multi-language. The CodeLang trait abstracts everything that varies between
languages: string delimiters, statement terminators, import syntax, visibility keywords,
type formatting, annotation style, and more. sigil-stitch ships with implementations
for TypeScript, JavaScript, Rust, Go, Python, Java, Kotlin, Swift, Dart, Scala,
Haskell, OCaml, C, C++, Bash, and Zsh.
The same CodeBlock, TypeName, and Spec types work across all of them – only the
language passed to render() changes.
Design philosophy
Specs emit CodeBlocks, never raw strings. A FunSpec produces a CodeBlock via
its .emit() method. A TypeSpec produces one or two CodeBlocks (depending on
whether the language places methods inside or outside the type body). The renderer
and import system only ever see CodeBlock trees. This means you can add new spec
types – or build your own – without touching the renderer or import collector.
The format-specifier system and the spec system are fully decoupled.
Minimal dependencies. The runtime dependencies are pretty (v0.12) for
Wadler-Lindig formatting, serde (v1, with derive) so every spec can round-trip
to JSON or YAML, and snafu for structured errors. Everything else – parsing
format strings, collecting imports, resolving conflicts, rendering output – is
implemented in sigil-stitch itself.
Two builder flavours. Spec builders (TypeSpec, FunSpec, FieldSpec,
FileSpec, EnumVariantSpec, PropertySpec, AnnotationSpec, ProjectSpec) use an
owning chain pattern – every setter takes mut self and returns Self, so you
chain calls fluently:
let fun = FunSpec::builder("greet")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
CodeBlockBuilder is different: its methods take &mut self and return
&mut Self, so you keep the builder in a let mut binding and call methods
on it:
let mut cb = CodeBlock::builder();
cb.add_statement("return user", ());
let block = cb.build().unwrap();
Quick orientation
There are three levels of abstraction, and you can use whichever fits:
- CodeBlock for code fragments. Use format specifiers (
%T,%S,%L,%W) to interpolate values. Good for function bodies, one-off statements, and anything that doesn’t need structural metadata. - Specs (FunSpec, TypeSpec, FieldSpec, ParameterSpec, etc.) for structured declarations. They produce CodeBlocks internally but carry metadata like visibility, annotations, type parameters, and modifiers that the language trait uses to emit correct syntax.
- FileSpec to render a complete file. It orchestrates the three-pass pipeline:
materialize specs into code blocks, collect and resolve imports, then render
everything with proper formatting. Pass a target width to
file.render(80)and get aStringback.
For multi-file output, ProjectSpec collects multiple FileSpecs and can render
them all at once or write them to disk.
What’s next
Continue to Getting Started for a hands-on walkthrough, or jump to Architecture for the full technical picture.
Getting Started
Installation
Add sigil-stitch to your project:
cargo add sigil-stitch
Or add it directly to your Cargo.toml:
[dependencies]
sigil-stitch = "0.3"
sigil-stitch requires Rust edition 2024 and MSRV 1.88.0. Runtime dependencies (pretty, serde with derive, and snafu) are pulled in automatically. No feature flags are needed – all spec types implement serde::Serialize and serde::Deserialize out of the box.
Your First CodeBlock
A CodeBlock is a composable code fragment built from format strings and typed arguments. Here’s a complete example that generates a TypeScript file with an automatic import:
use sigil_stitch::prelude::*;
use sigil_stitch::code_block::StringLitArg;
let user_type = TypeName::importable_type("./models", "User");
let mut cb = CodeBlock::builder();
cb.add_statement(
"const user: %T = await getUser(%S)",
(user_type.clone(), StringLitArg("id".into())),
);
cb.add_statement("return user", ());
let body = cb.build().unwrap();
let file = FileSpec::builder("user.ts")
.add_code(body)
.build()
.unwrap();
let output = file.render(80).unwrap();
println!("{output}");
This produces:
import type { User } from './models'
const user: User = await getUser('id');
return user;
Two things happened automatically:
%Twithuser_typerendered asUserin the code and addedimport type { User } from './models'at the top of the file.%SwithStringLitArgrendered the string"id"as a single-quoted TypeScript string literal'id'.
The () in cb.add_statement("return user", ()) means “no arguments” – the format string has no specifiers, so none are needed.
The Macro Alternative
The sigil_quote! macro lets you write target-language code inline, with less ceremony than the builder API. Here’s the same example:
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
let user_type = TypeName::importable_type("./models", "User");
let body = sigil_quote!(TypeScript {
const user: $T(user_type) = await getUser($S("id"));
return user;
}).unwrap();
This produces the same CodeBlock as the builder version above. The macro uses $T instead of %T and $S instead of %S, but the result is identical – same import tracking, same rendering, same output when passed to FileSpec.
The macro is a good fit when you’re writing a block of target-language code with a few interpolations. The builder is better when you’re constructing code programmatically (loops, conditionals on what to emit).
Building Structured Declarations
For functions, types, and other declarations, use the Spec layer. Specs carry structural metadata (name, return type, visibility, modifiers) and emit CodeBlocks internally.
Here’s a function declaration:
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
let user_type = TypeName::importable_type("./models", "User");
let fun = FunSpec::builder("getActiveUsers")
.returns(TypeName::array(user_type.clone()))
.is_async()
.body(sigil_quote!(TypeScript {
const users = await fetchAll();
return users.filter(u => u.active);
}).unwrap())
.build()
.unwrap();
let file = FileSpec::builder("users.ts")
.add_function(fun)
.build()
.unwrap();
let output = file.render(80).unwrap();
println!("{output}");
This produces a complete TypeScript file with the function declaration, including the async keyword, the User[] return type annotation, and the import for User.
Notice the builder pattern: spec builders like FunSpec::builder() and FileSpec::builder() use an owning chain pattern – setter methods like .returns(), .is_async(), and .body() take mut self and return Self, so you chain them fluently. The .build() call at the end consumes the builder and returns Result<FunSpec>. (CodeBlockBuilder is different: it uses &mut self, so you keep it in a let mut binding.)
Specs Emit CodeBlocks
Every spec type follows the same pattern: you configure it with a builder, call .build(), and eventually FileSpec calls .emit() on it to get a CodeBlock. This means:
- You never write raw import statements.
%Thandles it. - You never manually format function signatures.
FunSpechandles it. - You can mix specs and raw CodeBlocks freely in a
FileSpec.
The renderer and import collector only see CodeBlock trees. They don’t know or care whether a block came from a FunSpec, a TypeSpec, or a hand-written CodeBlock::builder() call.
Configuring a Language
Each language type (TypeScript, JavaScript, Python, JavaLang, and so on)
is a struct with public fields. The ones you usually want to tweak are exposed
as fluent with_* builders:
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::lang::config::QuoteStyle;
// Prettier-style: double quotes, no semicolons, .tsx extension.
let ts = TypeScript::new()
.with_quote_style(QuoteStyle::Double)
.with_semicolons(false)
.with_extension("tsx")
.with_indent(" ");
| Language | with_quote_style | with_indent | with_semicolons | with_extension |
|---|---|---|---|---|
TypeScript | yes | yes | yes | yes |
JavaScript | yes | yes | yes | yes |
Python | yes | yes | n/a | yes (e.g. pyi) |
JavaLang | n/a | yes | n/a | yes |
RustLang | n/a | yes | n/a | yes |
GoLang | n/a | yes | n/a | yes |
Kotlin | n/a | yes | n/a | yes (e.g. kts) |
Swift | n/a | yes | n/a | yes |
DartLang | n/a | yes | n/a | yes |
CLang | n/a | yes | n/a | yes (e.g. h) |
CppLang | n/a | yes | n/a | yes (e.g. hpp, cxx) |
Bash | n/a | yes | n/a | yes (e.g. sh) |
Zsh | n/a | yes | n/a | yes |
Language configuration is per-instance, not global: pass the configured language
into the FileSpec / ProjectSpec you want rendered with those settings.
What’s Next
Now that you’ve seen the basics:
- Format Specifiers explains every
%specifier in depth. - TypeName covers type references, import tracking, and cross-language rendering.
- Building Functions & Fields covers ParameterSpec, FieldSpec, and FunSpec.
- Building Types & Enums covers TypeSpec, PropertySpec, AnnotationSpec, and EnumVariantSpec.
- Files & Projects covers ImportSpec, FileSpec, and ProjectSpec.
- sigil_quote! Macro has the full guide for the macro syntax.
- Code Templates covers reusable named-parameter templates.
- Language Cookbook has idiomatic recipes for each supported language.
Format Specifiers
CodeBlock format strings use %-prefixed specifiers to interpolate arguments. Each specifier consumes one argument from the args list (except %W, %>, %<, %[, %], and %%, which consume none).
Quick Reference
| Specifier | Name | Argument | Purpose |
|---|---|---|---|
%T | Type | TypeName | Emit type reference, track import |
%N | Name | NameArg | Emit identifier name |
%S | String | StringLitArg | Emit escaped string literal |
%L | Literal | &str, String, CodeBlock | Emit raw value or nested block |
%W | Wrap | (none) | Soft line break point |
%> | Indent | (none) | Increase indent level |
%< | Dedent | (none) | Decrease indent level |
%[ | Begin | (none) | Start of statement |
%] | End | (none) | End of statement |
%% | Escape | (none) | Literal % character |
%T – Type Reference
The most powerful specifier. Takes a TypeName and does two things: emits the type name in the output AND registers the import so FileSpec::render() can collect, deduplicate, and emit import headers automatically.
use sigil_stitch::prelude::*;
use sigil_stitch::type_name::TypeName;
let user = TypeName::importable("./models", "User");
let block = CodeBlock::of("const u: %T = getUser()", (user,)).unwrap();
// Value import (not `import type`):
// import { User } from './models';
// const u: User = getUser();
For type-only imports (TypeScript’s import type), use importable_type:
let user = TypeName::importable_type("./models", "User");
// import type { User } from './models';
Generic types track imports recursively. Every TypeName nested inside the generic’s parameters is collected:
let promise = TypeName::generic(
TypeName::primitive("Promise"),
vec![TypeName::importable("./models", "User")],
);
let block = CodeBlock::of("function load(): %T", (promise,)).unwrap();
// Promise<User> -- the User import is still tracked
%N – Name
Emits an identifier. Bare &str and String values map to Arg::Literal (for %L) by default, so you must use the NameArg wrapper when your format string contains %N.
use sigil_stitch::code_block::{CodeBlock, NameArg};
let method_name = "getData";
let mut cb = CodeBlock::builder();
cb.add_statement("this.%N()", (NameArg(method_name.to_string()),));
let block = cb.build().unwrap();
// Output: this.getData();
%S – String Literal
Emits a language-aware quoted string. The CodeLang::render_string_literal() method on each language controls the quoting style and escape rules. TypeScript and JavaScript default to single quotes; Rust, Java, Go, C, C++, Swift, and Kotlin use double quotes; Dart uses single quotes; Python uses single quotes.
Requires the StringLitArg wrapper.
use sigil_stitch::code_block::{CodeBlock, StringLitArg};
let mut cb = CodeBlock::builder();
cb.add_statement("const msg = %S", (StringLitArg("hello world".to_string()),));
let block = cb.build().unwrap();
// TypeScript output: const msg = 'hello world';
// Java output: const msg = "hello world";
Special characters are escaped according to each language’s rules. For example, Kotlin and Dart escape $ to prevent string interpolation.
%L – Literal
Emits a raw value with no transformation. This is the default for bare &str and String arguments, so no wrapper is needed. Also accepts CodeBlock for embedding nested blocks.
use sigil_stitch::code_block::CodeBlock;
let mut cb = CodeBlock::builder();
// Bare string -> Arg::Literal -> used by %L
cb.add_statement("const count = %L", "42");
// Nested CodeBlock -> Arg::Code -> also used by %L
let inner = CodeBlock::of("getValue()", ()).unwrap();
cb.add_statement("const x = %L", inner);
let block = cb.build().unwrap();
// const count = 42;
// const x = getValue();
%W – Soft Line Break
No argument consumed. Marks a point where the Wadler-Lindig pretty printer (via the pretty crate) MAY insert a line break if the line exceeds the target width passed to FileSpec::render(width). If the line fits within the width, %W renders as a space.
use sigil_stitch::code_block::CodeBlock;
let mut cb = CodeBlock::builder();
cb.add_statement("const result = someFunction(arg1,%Warg2,%Warg3,%Warg4)", ());
let block = cb.build().unwrap();
// At width 80 (fits on one line):
// const result = someFunction(arg1, arg2, arg3, arg4);
//
// At width 40 (wraps):
// const result = someFunction(arg1,
// arg2,
// arg3,
// arg4);
Without any %W in a CodeBlock, the renderer does direct string concatenation with indent tracking. When %W is present, it builds a pretty::BoxDoc tree for width-aware layout. BoxDoc (not RcDoc) is used so rendered documents are Send + Sync.
%> and %< – Indent / Dedent
No argument consumed. Manually increase (%>) or decrease (%<) the indent level. Rarely needed directly because begin_control_flow(), next_control_flow(), and end_control_flow() manage indentation automatically. Useful when building custom block structures.
use sigil_stitch::code_block::CodeBlock;
let mut cb = CodeBlock::builder();
cb.add("items: [%>\n", ());
cb.add("'first',\n", ());
cb.add("'second',\n", ());
cb.add("%<]", ());
let block = cb.build().unwrap();
// items: [
// 'first',
// 'second',
// ]
Indent depth must balance to zero by the time build() is called. An unbalanced depth produces an UnbalancedIndent error.
%[ and %] – Statement Boundaries
No argument consumed. %[ marks the start of a statement. %] marks the end and appends the language’s statement terminator – ; for TypeScript, Rust, Java, C, C++, Dart; nothing for Python, Go, Kotlin, Swift.
You almost never write these directly. add_statement() wraps your format string in %[...%] and appends a newline automatically:
use sigil_stitch::code_block::CodeBlock;
let mut cb = CodeBlock::builder();
// These produce the same output:
cb.add_statement("const x = 1", ());
cb.add("%[const x = 1%]\n", ());
let block = cb.build().unwrap();
// const x = 1;
// const x = 1;
%% – Literal Percent
Emits a literal % character in the output. No argument consumed.
use sigil_stitch::code_block::CodeBlock;
let block = CodeBlock::of("progress: 100%%", ()).unwrap();
// progress: 100%
Arguments and the IntoArgs Trait
Every method that accepts a format string (add, add_statement, begin_control_flow, next_control_flow, CodeBlock::of) takes args: impl IntoArgs. This trait converts Rust values into Vec<Arg> for the format engine.
The critical rule: bare strings map to Arg::Literal (consumed by %L), not to Arg::Name or Arg::StringLit. To target %N or %S, use the NameArg and StringLitArg wrappers from sigil_stitch::code_block.
Type-to-Arg Mapping
| Rust Type | Maps To | Consumed By |
|---|---|---|
() | empty vec | (no specifiers) |
TypeName | Arg::TypeName | %T |
&str | Arg::Literal | %L |
String | Arg::Literal | %L |
CodeBlock | Arg::Code | %L |
NameArg(String) | Arg::Name | %N |
StringLitArg(String) | Arg::StringLit | %S |
Vec<Arg> | passthrough | any |
Single Argument
When a format string has exactly one specifier, pass the value directly (no tuple needed):
use sigil_stitch::type_name::TypeName;
use sigil_stitch::code_block::CodeBlock;
let user = TypeName::importable("./models", "User");
let block = CodeBlock::of("let u: %T", user).unwrap();
Multiple Arguments with Tuples
For two or more specifiers, use a tuple. Tuples are supported up to 8 elements. Each element must implement Into<Arg>.
use sigil_stitch::code_block::{CodeBlock, StringLitArg};
use sigil_stitch::type_name::TypeName;
let user_type = TypeName::importable("./models", "User");
// Two args: a TypeName and a StringLitArg
let mut cb = CodeBlock::builder();
cb.add_statement("const u: %T = getUser(%S)", (user_type, StringLitArg("admin".into())));
let block = cb.build().unwrap();
// const u: User = getUser('admin');
No Arguments
Pass () when the format string has no specifiers:
use sigil_stitch::code_block::CodeBlock;
let mut cb = CodeBlock::builder();
cb.add_statement("return null", ());
let block = cb.build().unwrap();
Argument Count Validation
The builder checks that the number of argument-consuming specifiers (%T, %N, %S, %L) matches the number of arguments provided. A mismatch records a FormatArgCount error, surfaced when build() is called. The error carries the expected specifier list and the actual argument kinds so you can see exactly which slot is wrong.
// This will fail: format has 2 specifiers but only 1 argument
let mut cb = CodeBlock::builder();
cb.add_statement("const %N: %T = null", "x"); // &str gives one Arg::Literal
let result = cb.build();
// Err(FormatArgCount {
// format: "const %N: %T = null",
// expected_specifiers: vec!["%N", "%T"],
// actual_arg_kinds: vec!["Literal"],
// })
An unrecognised specifier character (anything after % that isn’t T, N, S, L, W, >, <, [, ], or %) produces Err(SigilStitchError::InvalidFormatSpecifier { format, specifier }) instead.
TypeName
TypeName is the type reference enum at the heart of sigil-stitch’s import tracking. When you use a TypeName with the %T format specifier in a CodeBlock, the library renders the type name in the output and records the import. At render time, FileSpec collects all recorded imports, deduplicates them, resolves naming conflicts, and emits the import header automatically.
TypeName is language-agnostic — it carries no generic parameter. The same TypeName value can be rendered for any target language at FileSpec::render() time.
Import tracking
The two Importable constructors are the primary way to create types that generate import statements:
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
// Value import: import { User } from './models'
let user = TypeName::importable("./models", "User");
// Type-only import: import type { User } from './models'
let user = TypeName::importable_type("./models", "User");
When these types appear in a CodeBlock via %T, the import is tracked automatically. At file render time, all imports are collected, deduplicated, and emitted. If two modules export the same name, the first keeps the simple name and the second gets an auto-generated alias.
You can also set an explicit alias:
let user = TypeName::importable("./other", "User")
.with_alias("OtherUser");
// import { User as OtherUser } from './other'
// Rendered as: OtherUser
Primitives
Types that don’t need imports – built-in language types, type parameters, or any name that’s already in scope:
let s = TypeName::primitive("string");
let n = TypeName::primitive("number");
let t = TypeName::primitive("T"); // type parameter
Qualified types
For types that should render with their full module path inline without generating an import statement:
// Rust: serde_json::Value (no `use serde_json::Value;`)
let val = TypeName::qualified("serde_json", "Value");
// Rust: super::Foo
let foo = TypeName::qualified("super", "Foo");
// Java: java.util.HashMap
let map = TypeName::qualified("java.util", "HashMap");
The separator between module and name comes from CodeLang::module_separator() — "::" for Rust/C++, "." for Go/Python/Java/Kotlin/Scala/Swift/Dart/Haskell/OCaml. Languages without module-qualified paths (TypeScript, JavaScript, C, Bash, Zsh) silently fall back to rendering just the name.
Qualified types work anywhere a TypeName is accepted, including inside generics:
// Rust: std::collections::HashMap<String, serde_json::Value>
let map = TypeName::generic(
TypeName::qualified("std::collections", "HashMap"),
vec![
TypeName::primitive("String"),
TypeName::qualified("serde_json", "Value"),
],
);
You can also convert an existing importable type to qualified rendering with .qualify():
// Equivalent to TypeName::qualified("serde_json", "Value")
let val = TypeName::importable("serde_json", "Value").qualify();
Collections
Arrays
// TypeScript: string[]
// Rust: Vec<String> (via type_presentation().array)
// Go: []string
let arr = TypeName::array(TypeName::primitive("string"));
// TypeScript: readonly number[]
let ro = TypeName::readonly_array(TypeName::primitive("number"));
Maps
// Go: map[string]User
// TypeScript: Record<string, User> (via type_presentation().map)
let m = TypeName::map(
TypeName::primitive("string"),
TypeName::importable("./models", "User"),
);
Tuples
// Rust: (String, i32)
// TS: [string, number]
// Python: tuple[str, int]
// C++: std::tuple<string, int>
let t = TypeName::tuple(vec![
TypeName::primitive("string"),
TypeName::primitive("number"),
]);
// Unit type (empty tuple): Rust ()
let unit = TypeName::unit();
Slices
// Go: []User
let s = TypeName::slice(TypeName::primitive("User"));
Generics
Wrap a base type with type parameters:
// TypeScript: Promise<User>
let promise = TypeName::generic(
TypeName::primitive("Promise"),
vec![TypeName::importable("./models", "User")],
);
// Rust: HashMap<String, Vec<User>>
let map = TypeName::generic(
TypeName::primitive("HashMap"),
vec![
TypeName::primitive("String"),
TypeName::generic(
TypeName::primitive("Vec"),
vec![TypeName::primitive("User")],
),
],
);
Nesting works to any depth. Imports are collected recursively – every Importable type anywhere in the tree gets tracked.
Union and intersection types
// TypeScript: string | number | boolean
let u = TypeName::union(vec![
TypeName::primitive("string"),
TypeName::primitive("number"),
TypeName::primitive("boolean"),
]);
// TypeScript: Serializable & Loggable
let i = TypeName::intersection(vec![
TypeName::primitive("Serializable"),
TypeName::primitive("Loggable"),
]);
These are primarily useful for TypeScript. Other languages render them using their closest equivalent (e.g., Python uses X | Y for unions).
Optional types
// TypeScript: string | null
// Rust: Option<String>
// Go: *string
// Kotlin: String?
// Swift: String?
let opt = TypeName::optional(TypeName::primitive("string"));
The rendering adapts per language through the optional field in lang.type_presentation().
Pointer and reference types
// Go: *User
let ptr = TypeName::pointer(TypeName::primitive("User"));
// Rust: &str
let r = TypeName::reference(TypeName::primitive("str"));
// Rust: &mut Vec<i32>
let rm = TypeName::reference_mut(TypeName::primitive("Vec<i32>"));
Reference rendering is language-aware:
- Rust:
&T/&mut T - C++:
const T&/T& - C:
const T*/T* - Go: shared reference is a no-op, mutable reference renders as
*T - TypeScript: references are a no-op (everything is by reference)
Function types
// TypeScript: (string, number) => boolean
// Rust: fn(String, i32) -> bool
// Python: Callable[[str, int], bool]
// C++: std::function<bool(string, int)>
// Dart: bool Function(String, int)
let f = TypeName::function(
vec![TypeName::primitive("string"), TypeName::primitive("number")],
TypeName::primitive("boolean"),
);
Function type rendering varies significantly across languages. The function field in lang.type_presentation() returns a FunctionPresentation struct that controls keyword, delimiters, arrow syntax, parameter order, and optional outer wrappers.
Raw escape hatch
For type expressions not covered by the built-in variants:
let t = TypeName::raw("keyof User");
Raw emits the string verbatim with no import tracking. Use it sparingly – prefer the structured variants when possible.
Cross-language rendering
The same TypeName variant renders differently per language. This is powered by the TypePresentation system – each language returns a rendering pattern (prefix, postfix, surround, delimited, generic-wrap, or infix) for each type construct, and the rendering engine in type_name.rs interprets the pattern into formatted output. Language implementations never build BoxDoc directly.
| TypeName | TypeScript | Rust | Go | C++ |
|---|---|---|---|---|
array(T) | T[] | Vec<T> | []T | std::vector<T> |
optional(T) | T | null | Option<T> | *T | std::optional<T> |
tuple(A, B) | [A, B] | (A, B) | n/a | std::tuple<A, B> |
reference(T) | T | &T | T | const T& |
reference_mut(T) | T | &mut T | *T | T& |
map(K, V) | Record<K, V> | HashMap<K, V> | map[K]V | std::map<K, V> |
function(A) -> R | (A) => R | fn(A) -> R | func(A) R | std::function<R(A)> |
See Type Presentation for the full technical details of how this rendering system works.
Inspection methods
// Check if a type renders to empty string (used internally by ParameterSpec)
let empty = TypeName::primitive("");
assert!(empty.is_empty());
// Get the simple name (for import resolution lookups)
let t = TypeName::importable("./models", "User");
assert_eq!(t.simple_name(), Some("User"));
Building Functions & Fields
Specs are structural builders that produce Vec<CodeBlock>. They encapsulate common declaration patterns – classes, functions, fields, enums – so you work with named concepts instead of raw format strings. Every spec takes a &dyn CodeLang language reference at emit time, which means the same builder definition renders correctly for any target language.
All spec types live in src/spec/. They follow a consistent builder pattern:
mut selffor setters – owning chainable configuration methods that returnSelfselffor.build()– consumes the builder and returnsResult<Spec, SigilStitchError>- Chain calls fluently –
Builder::new(...).method().method().build()
// Correct:
let fun = FunSpec::builder("greet")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
(CodeBlockBuilder is different: it uses &mut self, so you keep it in a let mut binding and call methods on it.)
Every spec type (including CodeBlock, TypeName, FileSpec, and ProjectSpec) derives serde::Serialize and serde::Deserialize, so you can round-trip specs through JSON, YAML, or any other serde format. This is useful for caching materialized specs, shipping them across process boundaries, or diffing them in tests.
ParameterSpec
A single function parameter: name, type, optional default value, and variadic flag.
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
// Simple parameter
let p = ParameterSpec::new("name", TypeName::primitive("string")).unwrap();
// Parameter with default value
let p = ParameterSpec::builder("count", TypeName::primitive("number"))
.default_value(CodeBlock::of("0", ()).unwrap())
.build()
.unwrap();
// Output: count: number = 0
// Variadic parameter
let p = ParameterSpec::builder("args", TypeName::primitive("string"))
.variadic()
.build()
.unwrap();
// Output: ...args: string
ParameterSpec adapts to the target language. TypeScript emits name: type, C emits type name, and Python omits the type annotation when the type is empty.
FieldSpec
A struct field or class property: name, type, visibility, static/readonly flags, initializer, annotations, and doc comments.
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::lang::rust_lang::RustLang;
let field = FieldSpec::builder("name", TypeName::primitive("string"))
.visibility(Visibility::Private)
.is_readonly()
.build()
.unwrap();
// TypeScript: private readonly name: string;
let field = FieldSpec::builder("name", TypeName::primitive("String"))
.visibility(Visibility::Public)
.build()
.unwrap();
// Rust: pub name: String,
Fields support initializers for default values:
let field = FieldSpec::builder("count", TypeName::primitive("number"))
.initializer(CodeBlock::of("0", ()).unwrap())
.build()
.unwrap();
// TypeScript: count: number = 0;
For Go, use .tag() to attach struct tags:
let field = FieldSpec::builder("Name", TypeName::primitive("string"))
.tag("json:\"name\" db:\"name\"")
.build()
.unwrap();
// Go: Name string `json:"name" db:"name"`
Optional fields
is_optional() marks a field whose key may be absent (distinct from a value that
can be null). Rendering is language-specific, delegated to
CodeLang::optional_field_style():
let field = FieldSpec::builder("email", TypeName::primitive("string"))
.is_optional()
.build()
.unwrap();
// TypeScript: email?: string;
// JavaScript: email; (marker stripped — no optionality in JS)
// Rust: email: Option<String>,
// Go: Email *string
// Python: email: str | None
// Java: Optional<String> email; (caller must import java.util.Optional)
// Kotlin: name: String?
// Swift: name: String?
// Dart: String? name;
// C: string *email;
// C++: std::optional<string> email; (caller must #include <optional>)
Use is_optional() for “the key might not be there” (e.g., an OpenAPI property
not listed in required). Use TypeName::optional(...) for “the value might be
null” at the type level.
FunSpec
A function or method: parameters, return type, body, modifiers (async, static, abstract, constructor, override), type parameters, annotations, and doc comments.
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
let body = CodeBlock::of("return this.name", ()).unwrap();
let fun = FunSpec::builder("getName")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
// function getName(): string {
// return this.name
// }
Async methods
let body = CodeBlock::of("return await db.find(id)", ()).unwrap();
let fun = FunSpec::builder("fetchUser")
.is_async()
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::generic(
TypeName::primitive("Promise"),
vec![TypeName::primitive("User")],
))
.body(body)
.build()
.unwrap();
// public async fetchUser(id: string): Promise<User> {
// return await db.find(id)
// }
Type parameters
let tp = TypeParamSpec::new("T")
.with_bound(TypeName::primitive("Serializable"));
let body = CodeBlock::of("return JSON.stringify(value)", ()).unwrap();
let fun = FunSpec::builder("serialize")
.add_type_param(tp)
.add_param(ParameterSpec::new("value", TypeName::primitive("T")).unwrap())
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap();
// function serialize<T extends Serializable>(value: T): string {
// return JSON.stringify(value)
// }
Abstract methods
When no body is provided, the function renders as a declaration. Combined with is_abstract(), this produces abstract method signatures:
let fun = FunSpec::builder("validate")
.is_abstract()
.returns(TypeName::primitive("boolean"))
.build()
.unwrap();
// abstract validate(): boolean;
Constructor delegation
Use .delegation() to emit super(...) or this(...) calls. The placement is language-dependent: body-style (TS, Java, Dart, Swift) emits it as the first statement; signature-style (Kotlin) emits it after the parameter list.
let body = CodeBlock::of("this.name = name", ()).unwrap();
let fun = FunSpec::builder("constructor")
.is_constructor()
.add_param(ParameterSpec::new("name", TypeName::primitive("string")).unwrap())
.delegation(CodeBlock::of("super(name)", ()).unwrap())
.body(body)
.build()
.unwrap();
// constructor(name: string) {
// super(name);
// this.name = name
// }
Building Types & Enums
This chapter covers type declarations (classes, structs, interfaces, enums, type aliases, newtypes), computed properties, annotations, and enum variants. These specs follow the same builder pattern described in Building Functions & Fields: mut self for setters that return Self, self for .build(), and fluent chaining: Builder::new(...).method().method().build().
TypeSpec
The largest spec. Models type declarations: struct, class, interface, trait, enum, type alias, or newtype wrapper. Takes a TypeKind to select the declaration form.
.build() returns Err(SigilStitchError::DuplicateFieldName { type_name, field_name }) when two fields in the same type share a name.
Single-block output (TypeScript class)
When lang.methods_inside_type_body(kind) returns true, TypeSpec emits a single CodeBlock with fields and methods inside the body:
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
let body = CodeBlock::of("return this.name", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("name", TypeName::primitive("string"))
.visibility(Visibility::Private)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("getName")
.returns(TypeName::primitive("string"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let blocks = type_spec.emit(&TypeScript::new()).unwrap();
// blocks.len() == 1
//
// export class UserService {
// private name: string;
//
// getName(): string {
// return this.name
// }
// }
Two-block output (Rust struct + impl)
When methods_inside_type_body(kind) returns false (Rust structs and enums), TypeSpec emits two separate CodeBlocks: one for the data definition, one for the impl block:
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust_lang::RustLang;
let body = CodeBlock::of("Self { name: name.to_string() }", ()).unwrap();
let type_spec = TypeSpec::builder("Config", TypeKind::Struct)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("name", TypeName::primitive("String"))
.visibility(Visibility::Public)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("new")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("name", TypeName::primitive("&str")).unwrap())
.returns(TypeName::primitive("Self"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let blocks = type_spec.emit(&RustLang::new()).unwrap();
// blocks.len() == 2
//
// Block 0:
// pub struct Config {
// pub name: String,
// }
//
// Block 1:
// impl Config {
// pub fn new(name: &str) -> Self {
// Self { name: name.to_string() }
// }
// }
This split is the key structural decision. It is fully automatic – you build one TypeSpec, and the language’s methods_inside_type_body() determines whether the output is one block or two.
Extends and implements
let type_spec = TypeSpec::builder("AdminService", TypeKind::Class)
.visibility(Visibility::Public)
.extends(TypeName::primitive("BaseService"))
.implements(TypeName::primitive("Serializable"))
.build()
.unwrap();
// export class AdminService extends BaseService implements Serializable {
// }
Type aliases
TypeKind::TypeAlias emits a single-line type alias declaration with no body. The aliased target is set via .extends() (exactly one required). No fields, methods, or variants are allowed.
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::lang::rust_lang::RustLang;
// TypeScript: export type UserId = string;
let type_spec = TypeSpec::builder("UserId", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.extends(TypeName::primitive("string"))
.build()
.unwrap();
// Rust: pub type Meters = f64;
let type_spec = TypeSpec::builder("Meters", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.extends(TypeName::primitive("f64"))
.build()
.unwrap();
Per-language rendering is controlled by type_keyword(TypeKind::TypeAlias):
- TypeScript/Rust:
type Foo = Bar; - C++:
using Foo = Bar; - C:
typedef Bar Foo;(target-first, viatype_decl_syntax().type_alias_target_first) - Go:
type Foo = Bar - Kotlin:
typealias Foo = Bar - Python:
type Foo = Bar
Type aliases support type parameters:
// Rust: pub type Result<T> = std::result::Result<T, MyError>;
let type_spec = TypeSpec::builder("Result", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.add_type_param(TypeParamSpec::new("T"))
.extends(TypeName::generic(
TypeName::primitive("std::result::Result"),
vec![TypeName::primitive("T"), TypeName::primitive("MyError")],
))
.build()
.unwrap();
Newtype wrappers
TypeKind::Newtype emits a single-line newtype wrapper. Like type aliases, the inner type is set via .extends() (exactly one required).
use sigil_stitch::prelude::*;
use sigil_stitch::lang::rust_lang::RustLang;
use sigil_stitch::lang::go_lang::GoLang;
// Rust: pub struct Meters(f64);
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.visibility(Visibility::Public)
.extends(TypeName::primitive("f64"))
.build()
.unwrap();
// Go: type Meters float64
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("float64"))
.build()
.unwrap();
Newtype syntax varies across languages and is controlled by render_newtype_line():
- Rust:
struct Meters(f64);(tuple struct) - Go:
type Meters float64(distinct type) - Kotlin:
value class Meters(val value: f64)(inline class) - Python:
Meters = NewType("Meters", float)(typing.NewType) - C:
typedef float Meters;(typedef)
Enums with EnumVariantSpec
TypeSpec with TypeKind::Enum uses add_variant() instead of add_field(). See the EnumVariantSpec section below for variant forms.
use sigil_stitch::prelude::*;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::typescript::TypeScript;
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.add_variant(
EnumVariantSpec::builder("Up")
.value(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Down")
.value(CodeBlock::of("'DOWN'", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
// enum Direction {
// Up = 'UP',
// Down = 'DOWN',
// }
PropertySpec
Computed properties with getter and/or setter. Rendering depends on lang.property_style():
- Accessor (TypeScript, JavaScript): emits separate
get name(): T { ... }andset name(v: T) { ... }methods - Field (Swift, Kotlin): emits a field with inline
get/setblocks
use sigil_stitch::prelude::*;
use sigil_stitch::spec::property_spec::PropertySpec;
use sigil_stitch::lang::typescript::TypeScript;
let getter_body = CodeBlock::of("return this._name", ()).unwrap();
let setter_body = CodeBlock::of("this._name = value", ()).unwrap();
let prop = PropertySpec::builder("name", TypeName::primitive("string"))
.getter(getter_body)
.setter("value", setter_body)
.build()
.unwrap();
// TypeScript (Accessor style):
// get name(): string {
// return this._name
// }
// set name(value: string) {
// this._name = value
// }
For Swift and Kotlin, the same PropertySpec renders as a field with inline body blocks instead.
AnnotationSpec
Structured annotations that render with language-appropriate syntax. The prefix and suffix adapt automatically:
| Language | Syntax |
|---|---|
| Java, Kotlin, TS | @Name(args) |
| Rust | #[name(args)] |
| C++ | [[name(args)]] |
| C | __attribute__((name(args))) |
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
use sigil_stitch::lang::rust_lang::RustLang;
// Simple annotation: #[allow(dead_code)]
let ann = AnnotationSpec::new("allow").arg("dead_code");
// Multiple arguments: #[cfg(test, feature = "nightly")]
let ann = AnnotationSpec::new("cfg")
.arg("test")
.arg("feature = \"nightly\"");
For import-tracked annotations, use importable() with a TypeName:
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::type_name::TypeName;
let type_name = TypeName::importable("./decorators", "Component");
let ann = AnnotationSpec::importable(type_name);
// TS: @Component (with import { Component } from './decorators')
If AnnotationSpec does not cover your annotation format, every builder also has an .annotation(CodeBlock) escape hatch that accepts a raw CodeBlock.
EnumVariantSpec
Individual enum variants. Four forms are supported:
Simple variant
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::rust_lang::RustLang;
let v = EnumVariantSpec::new("Red").unwrap();
// Rust: Red,
Valued variant
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::typescript::TypeScript;
let variant = EnumVariantSpec::builder("Up")
.value(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap();
// TypeScript: Up = 'UP',
Tuple variant (Rust, Swift)
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::lang::rust_lang::RustLang;
let variant = EnumVariantSpec::builder("Literal")
.associated_type(TypeName::primitive("i64"))
.build()
.unwrap();
// Rust: Literal(i64),
// Multi-element tuple
let variant = EnumVariantSpec::builder("Pair")
.associated_type(TypeName::primitive("String"))
.associated_type(TypeName::primitive("i32"))
.build()
.unwrap();
// Rust: Pair(String, i32),
Struct variant (Rust)
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::spec::field_spec::FieldSpec;
use sigil_stitch::lang::rust_lang::RustLang;
let variant = EnumVariantSpec::builder("Move")
.add_field(
FieldSpec::builder("x", TypeName::primitive("i32")).build().unwrap(),
)
.add_field(
FieldSpec::builder("y", TypeName::primitive("i32")).build().unwrap(),
)
.build()
.unwrap();
// Rust:
// Move {
// x: i32,
// y: i32,
// },
Variants are added to a TypeSpec via add_variant(). The language controls separators (enum_and_annotation().variant_separator), trailing separators (enum_and_annotation().variant_trailing_separator), and prefixes (Swift’s case).
Files & Projects
This chapter covers the import system, file rendering, and multi-file project generation. These specs follow the same builder pattern described in Building Functions & Fields.
ImportSpec
Explicit import control for cases where %T / TypeName::Importable is not sufficient. Add to a FileSpec via add_import().
use sigil_stitch::spec::import_spec::ImportSpec;
use sigil_stitch::lang::typescript::TypeScript;
// Forced named import (even without %T usage in code)
let spec = ImportSpec::named("./models", "User");
// Aliased import: import { User as MyUser } from './models'
let spec = ImportSpec::named_as("./models", "User", "MyUser");
// Type-only import: import type { User } from './models'
let spec = ImportSpec::named_type("./models", "User");
// Side-effect import: import './polyfill'
let spec = ImportSpec::side_effect("./polyfill");
// Wildcard import: import * from './utils'
let spec = ImportSpec::wildcard("./utils");
Most of the time you do not need ImportSpec – imports driven by %T and TypeName::importable() handle the common case. Use ImportSpec for forced imports, side-effect imports, and wildcard imports.
FileSpec
The top-level file orchestrator. Combines code blocks, type declarations, and functions, then drives the three-pass render pipeline:
- Materialize – Specs (
TypeSpec,FunSpec) emit CodeBlocks - Collect imports – Walk all blocks, extract import references from
%Ttypes - Render – Emit the import header, then the body with resolved names and pretty printing
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
let user = TypeName::importable_type("./models", "User");
let mut cb = CodeBlock::builder();
cb.add_statement("const u: %T = getUser()", (user,));
let block = cb.build().unwrap();
let file = FileSpec::builder("user.ts")
.add_code(block)
.build()
.unwrap();
let output = file.render(80).unwrap();
// import type { User } from './models'
//
// const u: User = getUser();
You can mix member types freely: add_code() for raw CodeBlocks, add_type() for TypeSpec, add_function() for FunSpec, add_raw() for escape-hatch strings with no import tracking.
A file header (license comment, package declaration) can be set with .header():
let mut header_b = CodeBlock::builder();
header_b.add("// License: MIT", ());
let header = header_b.build().unwrap();
let file = FileSpec::builder("service.ts")
.header(header)
.add_type(service_type)
.build()
.unwrap();
ProjectSpec
Multi-file generation. Wraps multiple FileSpecs, renders them all, and can optionally write to the filesystem.
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
// Build individual files
let models = FileSpec::builder("src/models.ts")
.add_type(
TypeSpec::builder("User", TypeKind::Interface).build().unwrap(),
)
.build()
.unwrap();
let index = FileSpec::builder("src/index.ts")
.add_code(CodeBlock::of("export {}", ()).unwrap())
.build()
.unwrap();
// Combine into a project
let project = ProjectSpec::builder()
.add_file(models)
.add_file(index)
.build();
// Render all files in memory
let rendered = project.render(80).unwrap();
for file in &rendered {
println!("--- {} ---\n{}", file.path, file.content);
}
// Or write directly to disk
// project.write_to(Path::new("./output"), 80).unwrap();
Each file resolves imports independently. render() returns Vec<RenderedFile> with path and content fields. write_to() creates parent directories as needed.
End-to-End Example
A complete TypeScript class with imports, fields, a constructor, and a method – from builder calls to rendered output.
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
// Define an imported type
let repo_type = TypeName::importable_type("./repository", "UserRepository");
// Build the class
let user_type = TypeName::importable_type("./models", "User");
let ctor_body = CodeBlock::of("this.repo = repo", ()).unwrap();
let method_body = CodeBlock::of("return this.repo.findById(id)", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
// Field: private readonly repo: UserRepository;
.add_field(
FieldSpec::builder("repo", repo_type.clone())
.visibility(Visibility::Private)
.is_readonly()
.build()
.unwrap(),
)
// Constructor
.add_method(
FunSpec::builder("constructor")
.is_constructor()
.add_param(ParameterSpec::new("repo", repo_type.clone()).unwrap())
.body(ctor_body)
.build()
.unwrap(),
)
// Method: async getUser(id: string): Promise<User>
.add_method(
FunSpec::builder("getUser")
.is_async()
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::generic(
TypeName::primitive("Promise"),
vec![user_type],
))
.body(method_body)
.build()
.unwrap(),
)
.build()
.unwrap();
// Build the file
let file = FileSpec::builder("user_service.ts")
.add_type(type_spec)
.build()
.unwrap();
let output = file.render(80).unwrap();
Rendered output:
import type { User } from './models'
import { UserRepository } from './repository'
export class UserService {
private readonly repo: UserRepository;
constructor(repo: UserRepository) {
this.repo = repo
}
async getUser(id: string): Promise<User> {
return this.repo.findById(id)
}
}
The import header is fully automatic. UserRepository and User are collected from the %T references inside the emitted CodeBlocks, deduplicated, and rendered as import statements. No manual import management required.
sigil_quote! Macro
sigil_quote! lets you write target-language code inline and have it expand to
CodeBlockBuilder method calls at compile time. It’s the recommended way to build
CodeBlocks when the structure is known ahead of time.
For background on the % format specifiers that sigil_quote! expands to, see
Format Specifiers. For a hands-on introduction, see
Getting Started.
Basic Usage
use sigil_stitch::prelude::*;
use sigil_stitch::lang::typescript::TypeScript;
let user_type = TypeName::importable_type("./models", "User");
let block = sigil_quote!(TypeScript {
const user: $T(user_type) = await getUser($S("id"));
if (!user) {
throw new Error($S("not found"));
}
return user;
}).unwrap();
The macro takes a language type followed by a braced body of target-language code.
It returns Result<CodeBlock, SigilStitchError>.
Interpolation Markers
| Syntax | Specifier | Argument Type | Purpose |
|---|---|---|---|
$T(expr) | %T | TypeName | Type reference, tracks imports |
$N(expr) | %N | impl ToString | Name identifier |
$S(expr) | %S | impl ToString | String literal (quoted in output) |
$L(expr) | %L | impl Into<Arg> | Literal value or nested code |
$C(expr) | %L | CodeBlock | Nested code block |
$W | %W | (none) | Soft line-break point |
$open("text") | — | (none) | Custom block opener override |
$> | %> | (none) | Increase indent level |
$< | %< | (none) | Decrease indent level |
$$ | $ | (none) | Literal dollar sign |
Types ($T)
let user_type = TypeName::importable_type("./models", "User");
let block = sigil_quote!(TypeScript {
const user: $T(user_type) = getUser();
}).unwrap();
// Expands to: __sigil_builder.add_statement("const user: %T = getUser()", (user_type,));
// The import collector picks up User and generates: import type { User } from './models'
Names ($N)
let var_name = "myVariable";
let block = sigil_quote!(TypeScript {
const $N(var_name) = 42;
}).unwrap();
// Output: const myVariable = 42;
String Literals ($S)
let block = sigil_quote!(TypeScript {
console.log($S("hello world"));
}).unwrap();
// Output: console.log('hello world'); (TypeScript uses single quotes)
Literals ($L)
let default_val = "0";
let block = sigil_quote!(TypeScript {
const count = $L(default_val);
}).unwrap();
// Output: const count = 0;
Nested Code Blocks ($C)
let inner = CodeBlock::of("doSomething()", ()).unwrap();
let block = sigil_quote!(TypeScript {
$C(inner);
}).unwrap();
// Output: doSomething();
Dollar Escape ($$)
let block = sigil_quote!(TypeScript {
const price = $$100;
}).unwrap();
// Output contains: $ 100
// Note: the tokenizer inserts a space between $ and 100
Statement Rules
The macro classifies each line based on how it ends:
Semicolons: add_statement()
Lines ending with ; become statement calls (the renderer adds the language’s
statement terminator):
sigil_quote!(TypeScript {
const x = 1; // -> add_statement("const x = 1", ())
const y = x + 1; // -> add_statement("const y = x + 1", ())
})
Brace Groups: Control Flow
Lines ending with { ... } (without a trailing ;) become control flow:
sigil_quote!(TypeScript {
if (x > 0) { // -> begin_control_flow("if(x > 0)", ())
return true; // -> add_statement("return true", ())
} // -> end_control_flow()
})
Object Literals vs Control Flow
A { ... } followed by ; is treated as part of a statement, not control flow.
This is how the macro distinguishes object literals:
sigil_quote!(TypeScript {
const config = { timeout: 5000 }; // statement (has trailing ;)
if (ready) { // control flow (no trailing ;)
start();
}
})
Blank Lines: add_line()
Blank lines in the macro body insert visual separators:
sigil_quote!(TypeScript {
const a = 1;
const b = 2; // blank line above becomes add_line()
})
Comments: $comment("text")
Rust’s proc macro tokenizer strips // comments, so they’re invisible to the macro.
Use $comment() instead:
sigil_quote!(TypeScript {
$comment("Initialize the connection pool");
const pool = createPool();
})
// Output:
// // Initialize the connection pool
// const pool = createPool();
Control Flow
if / else / else if
The macro detects else and else if chains after closing braces:
sigil_quote!(TypeScript {
if (x > 0) {
return 1;
} else if (x < 0) {
return -1;
} else {
return 0;
}
})
This expands to:
__sigil_builder.begin_control_flow("if(x > 0)", ());
__sigil_builder.add_statement("return 1", ());
__sigil_builder.next_control_flow("else if(x < 0)", ());
__sigil_builder.add_statement("return - 1", ());
__sigil_builder.next_control_flow("else", ());
__sigil_builder.add_statement("return 0", ());
__sigil_builder.end_control_flow();
for / while / try-catch
Any tokens followed by { ... } are treated as control flow:
sigil_quote!(TypeScript {
for (const item of items) {
process(item);
}
})
sigil_quote!(TypeScript {
try {
riskyOperation();
} catch (e) {
handleError(e);
}
})
Nested Control Flow
sigil_quote!(TypeScript {
if (users.length > 0) {
for (const user of users) {
if (user.active) {
process(user);
}
}
}
})
Interpolation in Conditions
let error_type = TypeName::importable_type("./errors", "NotFoundError");
sigil_quote!(TypeScript {
if (!user) {
throw new $T(error_type)($S("not found"));
}
})
Custom Block Openers ($open)
By default, { ... } in sigil_quote! uses the language’s block_syntax().block_open:
- Brace languages (TypeScript, Go, etc.):
" {" - Python:
":" - Haskell:
" ="
Use $open("text") immediately before { to override the opener for that block:
use sigil_stitch::lang::haskell::Haskell;
// Haskell type class needs " where" instead of the default " ="
sigil_quote!(Haskell {
class Functor f $open(" where") {
fmap :: (a -> b) -> f a -> f b;
}
})
// Output: class Functor f where
// fmap :: (a -> b) -> f a -> f b
use sigil_stitch::lang::ocaml::OCaml;
// OCaml module block needs " = struct" opener
sigil_quote!(OCaml {
module Foo $open(" = struct") {
let x = 42;
}
})
// Output: module Foo = struct
// let x = 42
Pass $open("") to suppress the block opener entirely.
Manual Indent / Dedent ($> / $<)
Use $> and $< as standalone directives to control indent level without
control flow blocks:
use sigil_stitch::lang::typescript::TypeScript;
sigil_quote!(TypeScript {
namespace Foo {
$>
const x = 1;
const y = 2;
$<
}
})
// Output:
// namespace Foo {
// const x = 1;
// const y = 2;
// }
These map to the %> and %< format specifiers in CodeBlockBuilder.
Multi-Language Support
The same syntax works with any language type:
use sigil_stitch::lang::python::Python;
sigil_quote!(Python {
if x > 0:
return True
})
use sigil_stitch::lang::go_lang::GoLang;
sigil_quote!(GoLang {
x := 42;
})
use sigil_stitch::lang::rust_lang::RustLang;
sigil_quote!(RustLang {
let x: i32 = 42;
})
Known Limitations and Quirks
Tokenization
sigil_quote! uses Rust’s proc_macro2 tokenizer, which means the input is tokenized
as Rust tokens, not as the target language’s tokens. This creates some edge cases:
-
Single-quoted strings don’t work.
'hello'is tokenized as a Rust lifetime. Use$S("hello")instead. -
Spacing around operators. Multi-character operators like
:=,::,===are tokenized as separate punctuation characters. The macro reconstructs them but spacing may differ slightly:x := 42may render asx:= 42(:suppresses leading space)std::mem::size_ofworks but<u32>may get extra spaces around<and>
-
No space before
(after identifiers. The macro can’t distinguish keywords from function calls, soif(x)andfn(x)are treated the same. Both are valid in all supported languages. -
Negative number literals.
-1tokenizes as-then1, so it renders as- 1with a space. Functionally identical in all target languages. -
Template literals. Backtick strings (
`${expr}`) aren’t representable. Use$L(expr)for dynamic content. -
Percent signs. Literal
%in your code is auto-escaped to%%in the format string, so it renders correctly.
Comments
// comments are stripped by the Rust tokenizer before the proc macro sees them.
Use $comment("text") for comments in generated code.
Expressions in Interpolation
The expression inside $T(...), $S(...), etc. is passed through as an opaque
token stream. Any valid Rust expression works:
sigil_quote!(TypeScript {
const x: $T(TypeName::primitive("string")) = $S("hello".to_uppercase());
})
Blank Line Detection
Blank line detection uses proc_macro2 span locations. It requires the
span-locations feature (enabled by the macros crate). If spans aren’t available,
blank lines may not be detected.
Code Templates
CodeTemplate provides named parameters on top of CodeBlock’s positional format strings. Templates are language-agnostic: you define the pattern once, then apply it with concrete arguments for any target language.
Syntax
Templates use #{name:K} for named parameters, where K specifies the kind:
| Kind | Specifier | Argument Type |
|---|---|---|
T | %T | TypeName |
N | %N | NameArg |
S | %S | StringLitArg |
L | %L | &str, String, or CodeBlock |
Use ## to emit a literal # character.
Bare positional specifiers (%T, %N, etc.) are rejected in templates. You must use the named #{...} syntax.
Basic Usage
use sigil_stitch::code_template::CodeTemplate;
use sigil_stitch::code_block::NameArg;
use sigil_stitch::lang::typescript::TypeScript;
use sigil_stitch::type_name::TypeName;
let tmpl = CodeTemplate::new("const #{var:N}: #{type:T} = #{init:L}").unwrap();
let block = tmpl.apply()
.set("var", NameArg("user".into()))
.set("type", TypeName::primitive("string"))
.set("init", "null")
.build()
.unwrap();
// Output: const user: string = null
The template is parsed once by CodeTemplate::new(). Arguments are supplied via .apply().set(...).build(), producing a language-agnostic CodeBlock.
Reuse Across Types
The same template works for different types and values:
let field_tmpl = CodeTemplate::new("#{name:N}: #{type:T}").unwrap();
// Apply for a string field
let string_field = field_tmpl.apply()
.set("name", NameArg("username".into()))
.set("type", TypeName::primitive("string"))
.build()
.unwrap();
// Apply for a number field
let number_field = field_tmpl.apply()
.set("name", NameArg("age".into()))
.set("type", TypeName::primitive("number"))
.build()
.unwrap();
Reuse Across Languages
Since templates are language-agnostic, the same template can target different languages:
use sigil_stitch::lang::rust_lang::RustLang;
let decl = CodeTemplate::new("#{name:N}: #{type:T} = #{value:L}").unwrap();
// TypeScript
let ts_block = decl.apply()
.set("name", NameArg("count".into()))
.set("type", TypeName::primitive("number"))
.set("value", "0")
.build()
.unwrap();
// Rust
let rs_block = decl.apply()
.set("name", NameArg("count".into()))
.set("type", TypeName::primitive("i32"))
.set("value", "0")
.build()
.unwrap();
Duplicate Parameters
The same parameter name can appear multiple times. The value you set is used at each occurrence:
let tmpl = CodeTemplate::new("#{type:T} -> #{type:T}").unwrap();
let block = tmpl.apply()
.set("type", TypeName::primitive("string"))
.build()
.unwrap();
// Output: string -> string
Import Tracking
Templates using #{name:T} track imports just like %T in CodeBlocks. When the resulting CodeBlock is rendered inside a FileSpec, all type references are collected for the import header:
let tmpl = CodeTemplate::new("const #{var:N}: #{type:T} = new #{type:T}()").unwrap();
let user = TypeName::importable_type("./models", "User");
let block = tmpl.apply()
.set("var", NameArg("user".into()))
.set("type", user)
.build()
.unwrap();
// When rendered: import type { User } from './models'
// Output: const user: User = new User()
Validation
.build() validates that:
- All parameters have been set (missing parameters produce an error)
- Argument kinds match the parameter kind (
#{name:T}must receive aTypeName, not a string)
let tmpl = CodeTemplate::new("#{name:N}: #{type:T}").unwrap();
// Missing parameter
let result = tmpl.apply()
.set("name", NameArg("x".into()))
// forgot to set "type"
.build();
assert!(result.is_err());
Introspection
Use param_names() to inspect a template’s parameters:
let tmpl = CodeTemplate::new("#{name:N}: #{type:T} = #{init:L}").unwrap();
let params = tmpl.param_names();
// [("name", ParamKind::Name), ("type", ParamKind::Type), ("init", ParamKind::Literal)]
When to Use Templates vs CodeBlock
- CodeBlock: When you’re building code imperatively and the structure varies at runtime.
- CodeTemplate: When you have a fixed pattern that gets reused with different values. Templates make the pattern explicit and prevent positional argument errors.
- sigil_quote!: When you can write the target code inline at compile time.
Language Cookbook
This chapter collects practical, copy-paste-ready recipes for each supported language. Each example shows the builder calls and the rendered output. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Languages
- TypeScript – class with imports, interface with generics, type alias, enum, abstract class
- Rust – struct with impl, enum with variants, newtype, trait, type alias
- Go – struct with tags, newtype, interface, generic function
- Python – function with type hints, type alias, class with bases, dataclass, enum
- Java – class with annotations, interface, enum, abstract class
- Kotlin – data class, enum, interface, suspend function
- Swift – struct with protocol conformance, enum, enum with associated values, protocol
- C++ – class with template, using alias, enum class, virtual method, namespace wrapping
- C – typedef, struct with fields, function declaration, enum
- Scala – case class, trait with type parameter, enum, bounded type parameter, newtype
- Haskell – data record with deriving, type class, function with split signature, newtype, type alias
- OCaml – record type, function with curried params, module block, type alias, pattern match
Cross-language comparison
The same logical concept – a simple data type with two fields – rendered across four languages from the same builder structure:
| Language | Output |
|---|---|
| TypeScript | export class Point { x: number; y: number; } |
| Rust | pub struct Point { pub x: f64, pub y: f64, } + separate impl block |
| Go | type Point struct { X float64; Y float64 } |
| Python | class Point: x: float; y: float |
| Scala | case class Point(x: Double, y: Double) |
| Haskell | data Point = Point { pointX :: Double, pointY :: Double } |
| OCaml | type point = { x : float; y : float } |
The language’s CodeLang trait controls every syntax detail: keywords, delimiters, field ordering, visibility rendering, and whether methods live inside the type body or in a separate impl block. You build the spec once and the language passed to render() does the rest.
TypeScript Cookbook
Practical, copy-paste-ready recipes for TypeScript code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Class with imports
use sigil_stitch::prelude::*;
let user_type = TypeName::importable_type("./models", "User");
let repo_type = TypeName::importable("./repository", "UserRepository");
let body = CodeBlock::of("return this.repo.findById(id)", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("repo", repo_type.clone())
.visibility(Visibility::Private)
.is_readonly()
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("getUser")
.is_async()
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::generic(TypeName::primitive("Promise"), vec![user_type]))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
let output = FileSpec::builder("user_service.ts")
.add_type(type_spec)
.build()
.unwrap()
.render(80)
.unwrap();
import type { User } from './models'
import { UserRepository } from './repository'
export class UserService {
private readonly repo: UserRepository;
async getUser(id: string): Promise<User> {
return this.repo.findById(id)
}
}
Interface with generics
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.visibility(Visibility::Public)
.add_type_param(TypeParamSpec::new("T"))
.add_method(
FunSpec::builder("findById")
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::generic(TypeName::primitive("Promise"), vec![TypeName::primitive("T")]))
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.returns(TypeName::generic(TypeName::primitive("Promise"), vec![TypeName::primitive("void")]))
.build()
.unwrap(),
)
.build()
.unwrap();
export interface Repository<T> {
findById(id: string): Promise<T>;
save(entity: T): Promise<void>;
}
Type alias
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("UserId", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.extends(TypeName::primitive("string"))
.build()
.unwrap();
export type UserId = string;
Enum
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.visibility(Visibility::Public)
.add_variant(
EnumVariantSpec::builder("Up")
.value(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Down")
.value(CodeBlock::of("'DOWN'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Left")
.value(CodeBlock::of("'LEFT'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Right")
.value(CodeBlock::of("'RIGHT'", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
export enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
Abstract class
use sigil_stitch::prelude::*;
let body = CodeBlock::of("console.log('handled')", ()).unwrap();
let type_spec = TypeSpec::builder("BaseController", TypeKind::Class)
.visibility(Visibility::Public)
.is_abstract()
.add_method(
FunSpec::builder("handleRequest")
.is_abstract()
.add_param(ParameterSpec::new("req", TypeName::primitive("Request")).unwrap())
.returns(TypeName::primitive("Response"))
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("log")
.visibility(Visibility::Protected)
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
export abstract class BaseController {
abstract handleRequest(req: Request): Response;
protected log() {
console.log('handled')
}
}
Rust Cookbook
Practical, copy-paste-ready recipes for Rust code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Struct with impl
use sigil_stitch::prelude::*;
let body = CodeBlock::of("Self { name: name.into(), port }", ()).unwrap();
let type_spec = TypeSpec::builder("Config", TypeKind::Struct)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("name", TypeName::primitive("String"))
.visibility(Visibility::Public)
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("port", TypeName::primitive("u16"))
.visibility(Visibility::Public)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("new")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("name", TypeName::primitive("&str")).unwrap())
.add_param(ParameterSpec::new("port", TypeName::primitive("u16")).unwrap())
.returns(TypeName::primitive("Self"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
#![allow(unused)]
fn main() {
pub struct Config {
pub name: String,
pub port: u16,
}
impl Config {
pub fn new(name: &str, port: u16) -> Self {
Self { name: name.into(), port }
}
}
}
Enum with variants
use sigil_stitch::prelude::*;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
let type_spec = TypeSpec::builder("Expr", TypeKind::Enum)
.visibility(Visibility::Public)
.add_variant(EnumVariantSpec::new("Nil").unwrap())
.add_variant(
EnumVariantSpec::builder("Literal")
.associated_type(TypeName::primitive("i64"))
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("Binary")
.add_field(FieldSpec::builder("left", TypeName::primitive("Box<Expr>")).build().unwrap())
.add_field(FieldSpec::builder("op", TypeName::primitive("Op")).build().unwrap())
.add_field(FieldSpec::builder("right", TypeName::primitive("Box<Expr>")).build().unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
#![allow(unused)]
fn main() {
pub enum Expr {
Nil,
Literal(i64),
Binary {
left: Box<Expr>,
op: Op,
right: Box<Expr>,
},
}
}
Newtype
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.visibility(Visibility::Public)
.extends(TypeName::primitive("f64"))
.build()
.unwrap();
#![allow(unused)]
fn main() {
pub struct Meters(f64);
}
Trait
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Summary", TypeKind::Trait)
.visibility(Visibility::Public)
.add_method(
FunSpec::builder("summarize")
.add_param(ParameterSpec::new("&self", TypeName::primitive("")).unwrap())
.returns(TypeName::primitive("String"))
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("preview")
.add_param(ParameterSpec::new("&self", TypeName::primitive("")).unwrap())
.returns(TypeName::primitive("String"))
.body(CodeBlock::of("self.summarize()[..50].to_string()", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
#![allow(unused)]
fn main() {
pub trait Summary {
fn summarize(&self) -> String;
fn preview(&self) -> String {
self.summarize()[..50].to_string()
}
}
}
Type alias
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Result", TypeKind::TypeAlias)
.visibility(Visibility::Public)
.add_type_param(TypeParamSpec::new("T"))
.extends(TypeName::generic(
TypeName::primitive("std::result::Result"),
vec![TypeName::primitive("T"), TypeName::primitive("MyError")],
))
.build()
.unwrap();
#![allow(unused)]
fn main() {
pub type Result<T> = std::result::Result<T, MyError>;
}
Qualified paths (no import)
Use TypeName::qualified() to render types with their full module path inline without generating a use statement:
use sigil_stitch::prelude::*;
let field = FieldSpec::builder("data", TypeName::qualified("serde_json", "Value"))
.visibility(Visibility::Public)
.build()
.unwrap();
// In a generic:
let map_type = TypeName::generic(
TypeName::qualified("std::collections", "HashMap"),
vec![
TypeName::primitive("String"),
TypeName::qualified("serde_json", "Value"),
],
);
#![allow(unused)]
fn main() {
pub data: serde_json::Value
std::collections::HashMap<String, serde_json::Value>
}
Go Cookbook
Practical, copy-paste-ready recipes for Go code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Struct with tags
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("User", TypeKind::Struct)
.add_field(
FieldSpec::builder("Name", TypeName::primitive("string"))
.tag("json:\"name\" db:\"name\"")
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("Email", TypeName::primitive("string"))
.tag("json:\"email\" db:\"email\"")
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("Age", TypeName::primitive("int"))
.tag("json:\"age,omitempty\"")
.build()
.unwrap(),
)
.build()
.unwrap();
type User struct {
Name string `json:"name" db:"name"`
Email string `json:"email" db:"email"`
Age int `json:"age,omitempty"`
}
Newtype (distinct type)
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("float64"))
.build()
.unwrap();
type Meters float64
Interface
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.doc("Repository defines data access methods.")
.add_method(
FunSpec::builder("FindByID")
.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap())
.returns(TypeName::raw("(Entity, error)"))
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("Save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("Entity")).unwrap())
.returns(TypeName::primitive("error"))
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder("repo.go")
.header(CodeBlock::of("package repo", ()).unwrap())
.add_type(type_spec)
.build()
.unwrap();
let output = file.render(80).unwrap();
package repo
// Repository defines data access methods.
type Repository interface {
FindByID(id string) (Entity, error)
Save(entity Entity) error
}
Generic function
use sigil_stitch::prelude::*;
let tp = TypeParamSpec::new("T").with_bound(TypeName::primitive("comparable"));
let mut body_b = CodeBlock::builder();
body_b.begin_control_flow("if a > b", ());
body_b.add_statement("return a", ());
body_b.end_control_flow();
body_b.add_statement("return b", ());
let body = body_b.build().unwrap();
let fun = FunSpec::builder("Max")
.add_type_param(tp)
.add_param(ParameterSpec::new("a", TypeName::primitive("T")).unwrap())
.add_param(ParameterSpec::new("b", TypeName::primitive("T")).unwrap())
.returns(TypeName::primitive("T"))
.body(body)
.build()
.unwrap();
let file = FileSpec::builder("max.go")
.header(CodeBlock::of("package main", ()).unwrap())
.add_function(fun)
.build()
.unwrap();
let output = file.render(80).unwrap();
package main
func Max[T comparable](a T, b T) T {
if a > b {
return a
}
return b
}
Python Cookbook
Practical, copy-paste-ready recipes for Python code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Function with type hints
use sigil_stitch::prelude::*;
let user_type = TypeName::importable("models", "User");
let body = CodeBlock::of("return await db.query(User).filter(active=True)", ()).unwrap();
let fun = FunSpec::builder("get_active_users")
.is_async()
.add_param(ParameterSpec::new("db", TypeName::primitive("Database")).unwrap())
.returns(TypeName::generic(
TypeName::primitive("list"),
vec![user_type],
))
.body(body)
.build()
.unwrap();
async def get_active_users(db: Database) -> list[User]:
return await db.query(User).filter(active=True)
Type alias
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("UserId", TypeKind::TypeAlias)
.extends(TypeName::primitive("str"))
.build()
.unwrap();
type UserId = str
Class with bases
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("AdminService", TypeKind::Class)
.extends(TypeName::primitive("BaseService"))
.implements(TypeName::primitive("Authenticatable"))
.add_method(
FunSpec::builder("is_admin")
.add_param(ParameterSpec::new("self", TypeName::primitive("")).unwrap())
.returns(TypeName::primitive("bool"))
.body(CodeBlock::of("return True", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
class AdminService(BaseService, Authenticatable):
def is_admin(self) -> bool:
return True
Dataclass
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Config", TypeKind::Class)
.doc("Application configuration.")
.annotation(CodeBlock::of("@dataclass", ()).unwrap())
.add_field(
FieldSpec::builder("name", TypeName::primitive("str"))
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("port", TypeName::primitive("int"))
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("debug", TypeName::primitive("bool"))
.initializer(CodeBlock::of("False", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
@dataclass
class Config:
"""Application configuration."""
name: str
port: int
debug: bool = False
Enum
use sigil_stitch::prelude::*;
let enum_base = TypeName::importable("enum", "Enum");
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.extends(enum_base)
.add_variant(
EnumVariantSpec::builder("UP")
.value(CodeBlock::of("'UP'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("DOWN")
.value(CodeBlock::of("'DOWN'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("LEFT")
.value(CodeBlock::of("'LEFT'", ()).unwrap())
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("RIGHT")
.value(CodeBlock::of("'RIGHT'", ()).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder("direction.py")
.add_type(type_spec)
.build()
.unwrap();
let output = file.render(80).unwrap();
from enum import Enum
class Direction(Enum):
UP = 'UP'
DOWN = 'DOWN'
LEFT = 'LEFT'
RIGHT = 'RIGHT'
Java Cookbook
Practical, copy-paste-ready recipes for Java code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Class with annotations
use sigil_stitch::prelude::*;
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
let inject = AnnotationSpec::new("Inject");
let body = CodeBlock::of("return repository.findById(id)", ()).unwrap();
let type_spec = TypeSpec::builder("UserService", TypeKind::Class)
.visibility(Visibility::Public)
.add_field(
FieldSpec::builder("repository", TypeName::primitive("UserRepository"))
.visibility(Visibility::Private)
.annotate(inject)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("getUser")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.returns(TypeName::primitive("User"))
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
public class UserService {
@Inject
private UserRepository repository;
public User getUser(String id) {
return repository.findById(id);
}
}
Interface
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.visibility(Visibility::Public)
.add_type_param(TypeParamSpec::new("T"))
.doc("Generic data repository.")
.add_method(
FunSpec::builder("findById")
.returns(TypeName::primitive("T"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.returns(TypeName::primitive("void"))
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("delete")
.returns(TypeName::primitive("void"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
/**
* Generic data repository.
*/
public interface Repository<T> {
T findById(String id);
void save(T entity);
void delete(String id);
}
Enum
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.visibility(Visibility::Public)
.doc("Supported colors.")
.add_variant(EnumVariantSpec::new("RED").unwrap())
.add_variant(EnumVariantSpec::new("GREEN").unwrap())
.add_variant(EnumVariantSpec::new("BLUE").unwrap())
.build()
.unwrap();
/**
* Supported colors.
*/
public enum Color {
RED,
GREEN,
BLUE
}
Abstract class
use sigil_stitch::prelude::*;
let desc_body = CodeBlock::of("return this.getClass().getSimpleName();", ()).unwrap();
let type_spec = TypeSpec::builder("Shape", TypeKind::Class)
.visibility(Visibility::Public)
.doc("Abstract shape.")
.add_method(
FunSpec::builder("describe")
.visibility(Visibility::Public)
.returns(TypeName::primitive("String"))
.body(desc_body)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("area")
.visibility(Visibility::Public)
.is_abstract()
.returns(TypeName::primitive("double"))
.build()
.unwrap(),
)
.is_abstract()
.build()
.unwrap();
/**
* Abstract shape.
*/
public abstract class Shape {
public String describe() {
return this.getClass().getSimpleName();
}
public abstract double area();
}
Kotlin Cookbook
Practical, copy-paste-ready recipes for Kotlin code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Data class
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("User", TypeKind::Class)
.visibility(Visibility::Public)
.add_modifier("data")
.add_field(FieldSpec::builder("name", TypeName::primitive("String")).build().unwrap())
.add_field(FieldSpec::builder("email", TypeName::primitive("String")).build().unwrap())
.build()
.unwrap();
data class User(
val name: String,
val email: String,
)
Enum
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.doc("Supported colors.")
.add_variant(EnumVariantSpec::new("RED").unwrap())
.add_variant(EnumVariantSpec::new("GREEN").unwrap())
.add_variant(EnumVariantSpec::new("BLUE").unwrap())
.build()
.unwrap();
/**
* Supported colors.
*/
internal enum class Color {
RED,
GREEN,
BLUE
}
Interface
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.add_type_param(TypeParamSpec::new("T"))
.doc("Generic data repository.")
.add_method(
FunSpec::builder("findById")
.returns(TypeName::primitive("T?"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("delete")
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
/**
* Generic data repository.
*/
internal interface Repository<T> {
internal fun findById(id: String): T?
internal fun save(entity: T)
internal fun delete(id: String)
}
Suspend function
use sigil_stitch::prelude::*;
let user = TypeName::importable("com.example.model", "User");
let body = CodeBlock::of("return api.fetchUser(id)", ()).unwrap();
let fun = FunSpec::builder("fetchUser")
.is_async()
.returns(user)
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.body(body)
.build()
.unwrap();
let file = FileSpec::builder("Api.kt")
.add_function(fun)
.build()
.unwrap();
let output = file.render(80).unwrap();
import com.example.model.User
internal suspend fun fetchUser(id: String): User {
return api.fetchUser(id)
}
Swift Cookbook
Practical, copy-paste-ready recipes for Swift code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Struct with protocol conformance
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Point", TypeKind::Struct)
.implements(TypeName::primitive("Codable"))
.add_field(FieldSpec::builder("x", TypeName::primitive("Double")).build().unwrap())
.add_field(FieldSpec::builder("y", TypeName::primitive("Double")).build().unwrap())
.build()
.unwrap();
struct Point: Codable {
var x: Double
var y: Double
}
Enum
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.visibility(Visibility::Public)
.doc("Supported colors.")
.add_variant(EnumVariantSpec::new("red").unwrap())
.add_variant(EnumVariantSpec::new("green").unwrap())
.add_variant(EnumVariantSpec::new("blue").unwrap())
.build()
.unwrap();
/// Supported colors.
public enum Color {
case red
case green
case blue
}
Enum with associated values
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("NetworkResult", TypeKind::Enum)
.visibility(Visibility::Public)
.doc("Result of a network request.")
.add_variant(
EnumVariantSpec::builder("success")
.associated_type(TypeName::primitive("Data"))
.build()
.unwrap(),
)
.add_variant(
EnumVariantSpec::builder("failure")
.associated_type(TypeName::primitive("Error"))
.associated_type(TypeName::primitive("Int"))
.build()
.unwrap(),
)
.add_variant(EnumVariantSpec::new("loading").unwrap())
.build()
.unwrap();
/// Result of a network request.
public enum NetworkResult {
case success(Data)
case failure(Error, Int)
case loading
}
Protocol
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Repository", TypeKind::Interface)
.add_type_param(TypeParamSpec::new("T"))
.doc("Generic data repository.")
.add_method(
FunSpec::builder("findById")
.returns(TypeName::primitive("T?"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("delete")
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
/// Generic data repository.
protocol Repository<T> {
func findById(id: String) -> T?
func save(entity: T)
func delete(id: String)
}
C++ Cookbook
Practical, copy-paste-ready recipes for C++ code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Class with template
use sigil_stitch::prelude::*;
let body = CodeBlock::of("data_.push_back(value)", ()).unwrap();
let type_spec = TypeSpec::builder("Stack", TypeKind::Class)
.add_type_param(TypeParamSpec::new("T"))
.add_field(
FieldSpec::builder("data_", TypeName::generic(
TypeName::primitive("std::vector"),
vec![TypeName::primitive("T")],
))
.visibility(Visibility::Private)
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("push")
.visibility(Visibility::Public)
.add_param(ParameterSpec::new("value", TypeName::reference(TypeName::primitive("T"))).unwrap())
.body(body)
.build()
.unwrap(),
)
.build()
.unwrap();
template <typename T>
class Stack {
private:
std::vector<T> data_;
public:
void push(const T& value) {
data_.push_back(value);
}
};
Using alias (C++ type alias)
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("StringVec", TypeKind::TypeAlias)
.extends(TypeName::generic(
TypeName::primitive("std::vector"),
vec![TypeName::primitive("std::string")],
))
.build()
.unwrap();
using StringVec = std::vector<std::string>;
Enum class
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.doc("Available colors.")
.add_variant(EnumVariantSpec::new("Red").unwrap())
.add_variant(EnumVariantSpec::new("Green").unwrap())
.add_variant(EnumVariantSpec::new("Blue").unwrap())
.build()
.unwrap();
/// Available colors.
enum class Color {
Red,
Green,
Blue
};
Virtual method
C++ abstract classes with pure virtual methods require the extra_member escape hatch. Use FunSpec::emit() to render each method signature as a CodeBlock, then attach it to the type via extra_member.
use sigil_stitch::prelude::*;
use sigil_stitch::lang::cpp_lang::CppLang;
fn emit_fun(fun: &FunSpec) -> CodeBlock {
let lang = CppLang::new();
fun.emit(&lang, DeclarationContext::Member).unwrap()
}
let mut pub_section = CodeBlock::builder();
pub_section.add("%<", ());
pub_section.add("public:", ());
pub_section.add_line();
pub_section.add("%>", ());
pub_section.add_code(emit_fun(
&FunSpec::builder("area")
.is_abstract()
.returns(TypeName::primitive("double"))
.suffix("const")
.suffix("= 0")
.build()
.unwrap(),
));
pub_section.add_line();
pub_section.add_code(emit_fun(
&FunSpec::builder("~Shape")
.is_abstract()
.suffix("= default")
.build()
.unwrap(),
));
let type_spec = TypeSpec::builder("Shape", TypeKind::Class)
.doc("Abstract shape base class.")
.extra_member(pub_section.build().unwrap())
.build()
.unwrap();
/// Abstract shape base class.
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
Namespace wrapping
Use FileSpec::add_raw to wrap generated code in a namespace block.
use sigil_stitch::prelude::*;
let mut b = CodeBlock::builder();
b.add("int square(int x) {", ());
b.add_line();
b.add("%>", ());
b.add("return x * x;", ());
b.add_line();
b.add("%<", ());
b.add("}", ());
b.add_line();
let block = b.build().unwrap();
let file = FileSpec::builder("math.hpp")
.header(CodeBlock::of("#pragma once", ()).unwrap())
.add_raw("namespace math {\n")
.add_code(block)
.add_raw("\n} // namespace math\n")
.build()
.unwrap();
let output = file.render(80).unwrap();
#pragma once
namespace math {
int square(int x) {
return x * x;
}
} // namespace math
C Cookbook
Practical, copy-paste-ready recipes for C code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Typedef
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Meters", TypeKind::TypeAlias)
.extends(TypeName::primitive("double"))
.build()
.unwrap();
typedef double Meters;
Struct with fields
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Config", TypeKind::Struct)
.doc("Application configuration.")
.add_field(
FieldSpec::builder("timeout", TypeName::primitive("int"))
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("name", TypeName::primitive("char*"))
.build()
.unwrap(),
)
.add_field(
FieldSpec::builder("verbose", TypeName::primitive("int"))
.build()
.unwrap(),
)
.build()
.unwrap();
let file = FileSpec::builder("config.h")
.header(CodeBlock::of("#pragma once", ()).unwrap())
.add_type(type_spec)
.build()
.unwrap();
let output = file.render(80).unwrap();
#pragma once
/* Application configuration. */
struct Config {
int timeout;
char* name;
int verbose;
};
Function declaration
use sigil_stitch::prelude::*;
let fun = FunSpec::builder("process")
.add_param(ParameterSpec::new("data", TypeName::primitive("const char*")).unwrap())
.add_param(ParameterSpec::new("len", TypeName::primitive("size_t")).unwrap())
.returns(TypeName::primitive("int"))
.build()
.unwrap();
let file = FileSpec::builder("api.h")
.add_function(fun)
.build()
.unwrap();
let output = file.render(80).unwrap();
int process(const char* data, size_t len);
Enum
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Direction", TypeKind::Enum)
.doc("Cardinal directions.")
.add_variant(EnumVariantSpec::new("UP").unwrap())
.add_variant(EnumVariantSpec::new("DOWN").unwrap())
.add_variant(EnumVariantSpec::new("LEFT").unwrap())
.add_variant(EnumVariantSpec::new("RIGHT").unwrap())
.build()
.unwrap();
/* Cardinal directions. */
enum Direction {
UP,
DOWN,
LEFT,
RIGHT
};
Scala Cookbook
Practical, copy-paste-ready recipes for Scala code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Case class
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("User", TypeKind::Struct)
.doc("A user case class.")
.add_primary_constructor_param(
ParameterSpec::new("name", TypeName::primitive("String")).unwrap(),
)
.add_primary_constructor_param(
ParameterSpec::new("age", TypeName::primitive("Int")).unwrap(),
)
.add_primary_constructor_param(
ParameterSpec::new("email", TypeName::primitive("String")).unwrap(),
)
.build()
.unwrap();
/**
* A user case class.
*/
case class User(name: String, age: Int, email: String) {
}
Trait with type parameter
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Repository", TypeKind::Trait)
.add_type_param(TypeParamSpec::new("T"))
.doc("Generic data repository.")
.add_method(
FunSpec::builder("findById")
.returns(TypeName::primitive("Option[T]"))
.add_param(ParameterSpec::new("id", TypeName::primitive("String")).unwrap())
.build()
.unwrap(),
)
.add_method(
FunSpec::builder("save")
.add_param(ParameterSpec::new("entity", TypeName::primitive("T")).unwrap())
.build()
.unwrap(),
)
.build()
.unwrap();
/**
* Generic data repository.
*/
trait Repository[T] {
def findById(id: String): Option[T]
def save(entity: T)
}
Enum
use sigil_stitch::prelude::*;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
let type_spec = TypeSpec::builder("Color", TypeKind::Enum)
.doc("Supported colors.")
.add_variant(EnumVariantSpec::new("Red").unwrap())
.add_variant(EnumVariantSpec::new("Green").unwrap())
.add_variant(EnumVariantSpec::new("Blue").unwrap())
.build()
.unwrap();
/**
* Supported colors.
*/
enum Color {
Red,
Green,
Blue
}
Bounded type parameter
use sigil_stitch::prelude::*;
let body = CodeBlock::of("if (a.compareTo(b) >= 0) a else b", ()).unwrap();
let fun = FunSpec::builder("max")
.add_type_param(
TypeParamSpec::new("T").with_bound(TypeName::primitive("Comparable[T]")),
)
.returns(TypeName::primitive("T"))
.add_param(ParameterSpec::new("a", TypeName::primitive("T")).unwrap())
.add_param(ParameterSpec::new("b", TypeName::primitive("T")).unwrap())
.body(body)
.build()
.unwrap();
def max[T <: Comparable[T]](a: T, b: T): T = {
if (a.compareTo(b) >= 0) a else b
}
Newtype
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("Double"))
.build()
.unwrap();
class Meters(val value: Double)
Haskell Cookbook
Practical, copy-paste-ready recipes for Haskell code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Data record with deriving
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Person", TypeKind::Struct)
.add_field(
FieldSpec::builder("personName", TypeName::primitive("String")).build().unwrap(),
)
.add_field(
FieldSpec::builder("personAge", TypeName::primitive("Int")).build().unwrap(),
)
.implements(TypeName::primitive("Show"))
.implements(TypeName::primitive("Eq"))
.build()
.unwrap();
data Person =
Person {
personName :: String,
personAge :: Int,
}
deriving (Show, Eq)
Type class
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Printable", TypeKind::Trait)
.doc("Things that can be printed.")
.add_method(
FunSpec::builder("prettyPrint")
.add_param(ParameterSpec::new("a", TypeName::primitive("a")).unwrap())
.returns(TypeName::primitive("String"))
.build()
.unwrap(),
)
.build()
.unwrap();
-- | Things that can be printed.
class Printable where
prettyPrint :: a -> String
Function with split signature
use sigil_stitch::prelude::*;
let body = CodeBlock::of("x + y", ()).unwrap();
let fun = FunSpec::builder("add")
.add_param(ParameterSpec::new("x", TypeName::primitive("Int")).unwrap())
.add_param(ParameterSpec::new("y", TypeName::primitive("Int")).unwrap())
.returns(TypeName::primitive("Int"))
.body(body)
.build()
.unwrap();
add :: Int -> Int -> Int
add x y =
x + y
Newtype
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Meters", TypeKind::Newtype)
.extends(TypeName::primitive("Int"))
.build()
.unwrap();
newtype Meters = Meters Int
Type alias
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("Name", TypeKind::TypeAlias)
.extends(TypeName::primitive("String"))
.build()
.unwrap();
type Name = String
OCaml Cookbook
Practical, copy-paste-ready recipes for OCaml code generation. For the full API of each spec type, see Building Functions & Fields, Building Types & Enums, and Files & Projects.
Record type
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("person", TypeKind::Struct)
.doc("A person record.")
.add_field(
FieldSpec::builder("name", TypeName::primitive("string")).build().unwrap(),
)
.add_field(
FieldSpec::builder("age", TypeName::primitive("int")).build().unwrap(),
)
.add_field(
FieldSpec::builder("email", TypeName::primitive("string")).build().unwrap(),
)
.build()
.unwrap();
(** A person record. *)
type person =
{
name : string;
age : int;
email : string;
}
Function with curried params
use sigil_stitch::prelude::*;
let body = CodeBlock::of("List.map f xs", ()).unwrap();
let fun = FunSpec::builder("transform")
.add_param(ParameterSpec::new("f", TypeName::primitive("'a -> 'b")).unwrap())
.add_param(ParameterSpec::new("xs", TypeName::primitive("'a list")).unwrap())
.returns(TypeName::primitive("'b list"))
.body(body)
.build()
.unwrap();
let transform (f : 'a -> 'b) (xs : 'a list) : 'b list =
List.map f xs
Module block
OCaml modules are structurally different from types – they can contain multiple types and values. Use the OCaml::module_block helper to build a module Name = struct ... end block as a raw CodeBlock.
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::lang::ocaml::OCaml;
let mut inner = CodeBlock::builder();
inner.add_statement("let greeting = \"hello\"", ());
inner.add_statement("let farewell = \"goodbye\"", ());
let body = inner.build().unwrap();
let module = OCaml::module_block("MyModule", body).unwrap();
module MyModule = struct
let greeting = "hello"
let farewell = "goodbye"
end
Type alias
use sigil_stitch::prelude::*;
let type_spec = TypeSpec::builder("string_list", TypeKind::TypeAlias)
.extends(TypeName::primitive("string list"))
.build()
.unwrap();
type string_list = string list
Pattern match
Pattern matching is built using CodeBlock control-flow methods. Use begin_control_flow for the outer binding, then begin_control_flow_with_open to open the match expression with no trailing brace.
use sigil_stitch::code_block::CodeBlock;
let mut b = CodeBlock::builder();
b.begin_control_flow("let describe color", ());
b.begin_control_flow_with_open("match color with", (), "");
b.add("| Red -> \"red\"", ());
b.add_line();
b.add("| Green -> \"green\"", ());
b.add_line();
b.add("| Blue -> \"blue\"", ());
b.add_line();
b.end_control_flow();
b.end_control_flow();
let block = b.build().unwrap();
let describe color =
match color with
| Red -> "red"
| Green -> "green"
| Blue -> "blue"
Architecture
This chapter describes how sigil-stitch works internally. It covers the four-layer design, the three-pass rendering pipeline, and the import resolution system.
Four Layers
The library is organized in four layers, each building on the one below:
┌─────────────────────────────────────┐
│ Spec Layer (TypeSpec, FunSpec, ...) │ Structural builders
├─────────────────────────────────────┤
│ CodeBlock + Format Specifiers │ Composable code fragments
├─────────────────────────────────────┤
│ TypeName │ Type references with import tracking
├─────────────────────────────────────┤
│ CodeLang Trait │ Language abstraction
└─────────────────────────────────────┘
Layer 1: CodeLang
src/lang/mod.rs defines the CodeLang trait with 33 methods, including 6 config struct accessors (type_presentation(), generic_syntax(), block_syntax(), function_syntax(), type_decl_syntax(), enum_and_annotation()) that return data structs with sensible defaults. Each supported language implements this trait in its own module (src/lang/typescript.rs, etc.).
Public types are language-agnostic — no generic parameter. The language enters as &dyn CodeLang at render time. FileSpec stores a Box<dyn CodeLang> internally; all other types (CodeBlock, TypeName, specs) are language-independent.
Layer 2: TypeName
src/type_name.rs defines type references. Key variants:
| Variant | Example | Import Tracked? |
|---|---|---|
Primitive | string, i32 | No |
Importable | User from ./models | Yes |
Generic | Promise<User> | Recursively |
Array | User[], Vec<User> | Inner type tracked |
ReadonlyArray | readonly User[] | Inner type tracked |
Optional | User?, Option<User> | Inner type tracked |
Union | string | number | All members tracked |
Intersection | A & B, A + B | All members tracked |
Tuple | [A, B], (A, B) | All members tracked |
Reference | &T, const T& | Inner type tracked |
Function | (x: string) => void | Params + return tracked |
Map | Map<string, User> | Key + value tracked |
Pointer / Slice | *const T, &[T] | Inner type tracked |
Raw | any string | No |
Every variant that contains other types recursively collects imports via collect_imports(). This means Generic(Promise, [Importable(User)]) tracks the User import even though Promise is a primitive.
TypeName also renders to pretty::BoxDoc for width-aware output of complex type signatures. BoxDoc is used (rather than RcDoc) so rendered documents are Send + Sync and can cross thread boundaries.
Type Presentation Layer
TypeName variants are semantic — Array(T) means “array of T” regardless of language. Cross-language rendering is handled by a data-driven presentation layer:
- Each
TypeNamevariant asks the language for aTypePresentation— a data enum describing the syntactic pattern (e.g.,GenericWrap,Prefix,Postfix,Surround,Delimited,Infix). - A single rendering engine in
type_name.rsinterprets the pattern intoBoxDocoutput.
BoxDoc never appears in the CodeLang trait. Languages return pure data; the engine does all rendering. See Type Presentation for the full design.
Layer 3: CodeBlock
A CodeBlock stores nodes: Vec<CodeNode> — a tree of self-contained nodes (Literal, TypeRef, NameRef, StringLit, Comment, Nested, etc.). Format strings are parsed at build time and immediately converted to CodeNode nodes. Each node is self-contained: TypeRef(TypeName) carries its type reference directly, with no separate arg-index lookup.
CodeBlocks are immutable after construction. The builder (CodeBlockBuilder) validates argument counts and indent balance before producing a block.
Layer 4: Spec Layer
src/spec/ contains structural builders that emit Vec<CodeBlock>. TypeSpec emits one or two blocks depending on methods_inside_type_body(). FunSpec emits one block. FileSpec orchestrates the full rendering pipeline.
The key design decision: specs emit CodeBlocks, never raw strings. This means the renderer and import system never need to change when new spec types are added. A new WidgetSpec would just emit CodeBlocks with %T references, and imports would work automatically.
Three-Pass Rendering Pipeline
FileSpec::render(width) drives everything. It runs three passes over the file’s members.
Pass 0: Materialize
Specs are converted to CodeBlocks:
FileMember::Type(TypeSpec)callstype_spec.emit(&lang)->Vec<CodeBlock>FileMember::Fun(FunSpec)callsfun_spec.emit(&lang, ctx)->CodeBlockFileMember::Code(CodeBlock)passes through unchangedFileMember::RawContent(String)passes through as-is
After this phase, everything is either a CodeBlock or raw content.
Pass 1: Collect Imports
import_collector walks every CodeBlock tree. For each CodeNode::TypeRef in any block, it calls type_name.collect_imports() to extract ImportRef structs (module + name + optional alias).
Nested CodeBlocks (CodeNode::Nested) are walked recursively. RawContentWithImports members have their type list walked for imports even though the content itself is opaque.
Import Resolution
ImportGroup::resolve() takes the collected ImportRef list and:
- Deduplicates: Same module + same name = one import
- Detects conflicts: Two different modules exporting the same name (e.g.,
Userfrom./modelsandUserfrom./legacy) - Assigns aliases: First-encountered
Userwins the simple name. The second gets aliased using a module-derived prefix (e.g.,LegacyUser) - Merges explicit imports:
ImportSpecentries (aliased, side-effect, wildcard) are merged into the resolved set
The result is an ImportGroup that maps each module to its resolved names with aliases.
Go’s qualify_import_name() adds another layer: instead of importing Server directly, it renders as http.Server in code, with a package-level import of "net/http".
Pass 2: Render
CodeRenderer walks each CodeBlock’s CodeNode sequence:
| Node | Action |
|---|---|
Literal(s) | Emit string directly |
TypeRef(tn) | Resolve import name via ImportGroup, emit |
NameRef(s) | Emit identifier |
StringLit(s) | Call lang.render_string_literal() |
InlineLiteral(s) | Emit raw literal |
Nested(block) | Recursively render the inner CodeBlock |
Comment(s) | Emit with lang.line_comment_prefix() |
SoftBreak | Pretty-print decision point |
Indent / Dedent | Adjust indent level |
StatementBegin / StatementEnd | Statement boundaries (; if applicable) |
Newline | Emit newline + indent |
BlockOpen / BlockClose | Block delimiters from lang.block_syntax() |
BlockOpenOverride(s) | Emit custom block opener (e.g. " where") |
BlockCloseTransition | Close delimiter + space (for } else { chains) |
Sequence(children) | Recursively render a sub-sequence of nodes |
Width-aware rendering: When a CodeBlock contains SoftBreak nodes, the renderer builds a pretty::BoxDoc tree (Send + Sync) via nodes_to_doc instead of doing direct string concatenation. The Wadler-Lindig algorithm then decides at each SoftBreak point whether to insert a line break or a space, based on the target width. CodeBlocks without SoftBreak use the simpler direct-concat path for efficiency.
Import Conflict Resolution
A concrete example of the conflict resolution:
use sigil_stitch::prelude::*;
let user_a = TypeName::importable_type("./models", "User");
let user_b = TypeName::importable_type("./legacy", "User");
let mut cb = CodeBlock::builder();
cb.add_statement("const a: %T = getA()", (user_a,));
cb.add_statement("const b: %T = getB()", (user_b,));
let body = cb.build().unwrap();
let output = FileSpec::builder("test.ts")
.add_code(body)
.build()
.unwrap()
.render(80)
.unwrap();
The output would contain:
import type { User } from './models'
import type { User as LegacyUser } from './legacy'
const a: User = getA();
const b: LegacyUser = getB();
The first User (from ./models) wins the simple name. The second (from ./legacy) gets the alias LegacyUser, derived from the module path.
Language-Agnostic Types
All public types (CodeBlock, TypeName, TypeSpec, FunSpec, etc.) are language-agnostic. The language is supplied at render time via &dyn CodeLang:
let user = TypeName::importable_type("./models", "User");
let mut cb = CodeBlock::builder();
cb.add("const u: %T = getUser()", (user,));
let block = cb.build().unwrap();
// Render for any language:
let output_ts = FileSpec::builder("user.ts")
.add_code(block.clone())
.build()
.unwrap()
.render(80)
.unwrap();
let output_rs = FileSpec::builder("user.rs")
.add_code(block)
.build()
.unwrap()
.render(80)
.unwrap();
FileSpec::builder("user.ts") auto-detects the language from the file extension. Use FileSpec::builder_with("user.ts", TypeScript::new()) for explicit control.
Type Presentation
This chapter describes how sigil-stitch renders TypeName variants across different languages using a data-driven presentation layer.
The Problem
TypeName is a semantic type algebra — Array(T) means “array of T” regardless of target language. But the surface syntax varies widely:
| TypeName | TypeScript | Rust | Go | Python | C++ |
|---|---|---|---|---|---|
Array(T) | T[] | Vec<T> | []T | list[T] | std::vector<T> |
Optional(T) | T | null | Option<T> | *T | T | None | std::optional<T> |
Map(K, V) | Record<K, V> | HashMap<K, V> | map[K]V | dict[K, V] | std::map<K, V> |
Pointer(T) | n/a | *const T | *T | n/a | T* |
Tuple(A, B) | [A, B] | (A, B) | n/a | tuple[A, B] | std::tuple<A, B> |
Reference(T) | (identity) | &T | (identity) | (identity) | const T& |
Reference(T, mut) | (identity) | &mut T | *T | (identity) | T& |
Each variant needs language-specific rendering, but the rendering follows a small set of structural patterns. Rather than writing per-language rendering code for every variant, we identify these patterns and let languages declare which pattern to use.
Architecture
┌──────────────┐
│ TypeName │ Semantic type algebra
│ (unchanged) │ Array, Optional, Map, ...
└──────┬───────┘
│ to_doc_with_lang(resolve, lang)
▼
┌──────────────────────────────┐
│ lang.type_presentation() │ CodeLang returns TypePresentationConfig (DATA)
└──────────────┬───────────────┘
▼
┌─────────────────────┐
│ Rendering engine │ Single function: (TypePresentation, inner docs) → BoxDoc
│ (one place) │ Lives in type_name.rs, NEVER in CodeLang impls
└──────────┬──────────┘
▼
BoxDoc output
The key invariant: BoxDoc never appears in the CodeLang trait. Languages declare data (which syntactic pattern to use). The rendering engine — a single function in type_name.rs — interprets that data into BoxDoc output.
This separates three concerns that were previously tangled:
- What a type means —
TypeNamevariants (semantic, language-independent) - How a language spells it —
TypePresentationdata (per-language, no rendering logic) - How to assemble output — rendering engine (one place, all patterns)
TypePresentation
TypePresentation is an enum of syntactic patterns. Each variant describes a structural template for assembling already-rendered inner type docs:
pub enum TypePresentation<'a> {
/// `name<P1, P2>` — delimiters from generic_syntax().open/.close.
/// Vec<T>, Option<T>, HashMap<K,V>, List<T>.
GenericWrap { name: &'a str },
/// `prefix inner` — *T, &T, []T, &mut T.
Prefix { prefix: &'a str },
/// `inner suffix` — T[], T?, T*.
Postfix { suffix: &'a str },
/// `prefix inner suffix` — const T&, const T*.
Surround { prefix: &'a str, suffix: &'a str },
/// `open P1 sep P2 sep ... close` — (A, B), [T], [K: V], dict[K, V].
Delimited {
open: &'a str,
sep: &'a str,
close: &'a str,
},
/// `P1 sep P2 sep ... Pn` — A | B, A & B, A + B.
Infix { sep: &'a str },
}
Six patterns cover every type rendering need across all supported languages. A language implementation never builds BoxDoc — it returns one of these variants with the appropriate strings filled in.
FunctionPresentation
Function types are too complex for a single TypePresentation variant — they have parameter lists, return types, arrows, optional keywords, and wrappers that combine in language-specific ways. They get their own struct:
pub struct FunctionPresentation<'a> {
pub keyword: &'a str, // "fn", "func", ""
pub params_open: &'a str, // "(", "Callable[["
pub params_sep: &'a str, // ", "
pub params_close: &'a str, // ")", "]]"
pub arrow: &'a str, // " -> ", " => ", ", "
pub return_first: bool, // Dart: R Function(A, B)
pub curried: bool, // Haskell: A -> B -> R
pub wrapper_open: &'a str, // C++: "std::function<"
pub wrapper_close: &'a str, // C++: ">"
}
This declaratively covers TypeScript (A, B) => R, Rust fn(A, B) -> R, Python Callable[[A, B], R], C++ std::function<R(A, B)>, Dart R Function(A, B), and Haskell A -> B -> R — all from a single rendering engine interpreting the data.
CodeLang Trait Method
Languages declare their type syntax by returning a TypePresentationConfig from a single method on the CodeLang trait:
trait CodeLang {
fn type_presentation(&self) -> TypePresentationConfig<'_>;
}
TypePresentationConfig bundles every type-rendering decision into one struct — never BoxDoc:
pub struct TypePresentationConfig<'a> {
pub array: TypePresentation<'a>,
pub readonly_array: Option<TypePresentation<'a>>,
pub optional: TypePresentation<'a>,
pub optional_absent_literal: &'a str,
pub map: TypePresentation<'a>,
pub union: TypePresentation<'a>,
pub intersection: TypePresentation<'a>,
pub pointer: TypePresentation<'a>,
pub slice: TypePresentation<'a>,
pub tuple: TypePresentation<'a>,
pub reference: TypePresentation<'a>,
pub reference_mut: TypePresentation<'a>,
pub function: FunctionPresentation<'a>,
pub associated_type: AssociatedTypeStyle<'a>,
pub impl_trait: BoundsPresentation<'a>,
pub dyn_trait: BoundsPresentation<'a>,
pub wildcard: WildcardPresentation<'a>,
}
Every field has a sensible default via Default::default(). TypeScript needs almost no overrides. Most languages override 3–5 fields with struct-update syntax (..Default::default()).
Rendering Engine
A single private function in type_name.rs interprets presentations:
fn render_presentation(
pres: &TypePresentation<'_>,
inner_docs: Vec<BoxDoc<'static, ()>>,
gs: &GenericSyntaxConfig<'_>,
) -> BoxDoc<'static, ()> {
match pres {
TypePresentation::GenericWrap { name } => {
// name<P1, P2> using lang.generic_syntax().open / .close
}
TypePresentation::Prefix { prefix } => {
// prefix inner
}
TypePresentation::Postfix { suffix } => {
// inner suffix
}
TypePresentation::Surround { prefix, suffix } => {
// prefix inner suffix
}
TypePresentation::Delimited { open, sep, close } => {
// open P1 sep P2 close
}
TypePresentation::Infix { sep } => {
// P1 sep P2 sep P3
}
}
}
Each TypeName variant in to_doc_with_lang becomes a three-step process:
- Recursively render inner types to
BoxDoc - Ask the language for a
TypePresentation - Pass both to
render_presentation
TypeName::Array(inner) => {
let inner_doc = inner.to_doc_with_lang(resolve, lang);
let tp = lang.type_presentation();
let gs = lang.generic_syntax();
render_presentation(&tp.array, vec![inner_doc], &gs)
}
Per-Language Examples
TypeScript
TypeScript overrides five fields from the defaults:
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
map: TypePresentation::GenericWrap { name: "Record" },
tuple: TypePresentation::Delimited { open: "[", sep: ", ", close: "]" },
associated_type: AssociatedTypeStyle::IndexAccess { open: "[\"", close: "\"]" },
impl_trait: BoundsPresentation { keyword: "", separator: " & " },
wildcard: WildcardPresentation { unbounded: "unknown", .. },
..Default::default()
}
}
The remaining fields use defaults: Array → Postfix { suffix: "[]" }, Optional → Infix { sep: " | " } with optional_absent_literal set to "null".
Rust
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::GenericWrap { name: "Vec" },
optional: TypePresentation::GenericWrap { name: "Option" },
map: TypePresentation::GenericWrap { name: "HashMap" },
intersection: TypePresentation::Infix { sep: " + " },
pointer: TypePresentation::Prefix { prefix: "*const " },
slice: TypePresentation::Delimited { open: "&[", sep: "", close: "]" },
reference: TypePresentation::Prefix { prefix: "&" },
reference_mut: TypePresentation::Prefix { prefix: "&mut " },
..Default::default()
}
}
C++
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::GenericWrap { name: "std::vector" },
optional: TypePresentation::GenericWrap { name: "std::optional" },
pointer: TypePresentation::Postfix { suffix: "*" },
reference: TypePresentation::Surround { prefix: "const ", suffix: "&" },
reference_mut: TypePresentation::Postfix { suffix: "&" },
tuple: TypePresentation::GenericWrap { name: "std::tuple" },
..Default::default()
}
}
The Surround variant was introduced specifically for C++’s const T& pattern, where a type needs both a prefix and a suffix. C uses it similarly for const T*.
Go
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::Prefix { prefix: "[]" },
map: TypePresentation::Delimited { open: "map[", sep: "]", close: "" },
..Default::default()
}
}
Note that GenericWrap reuses generic_syntax().open/.close, so Go’s List[T] works automatically because Go already sets generic_syntax().open to "[".
Swift
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::Delimited { open: "[", sep: "", close: "]" },
optional: TypePresentation::Postfix { suffix: "?" },
map: TypePresentation::Delimited { open: "[", sep: ": ", close: "]" },
..Default::default()
}
}
Design Properties
BoxDocnever appears inCodeLang— languages declare data, the engine renders.- Adding a
TypeNamevariant requires one new field onTypePresentationConfig. No per-language render code needed. - 17 fields on
TypePresentationConfigreplace what would otherwise be ~20+ render methods. Each override is a single struct field. - One rendering engine in
type_name.rshandles all patterns uniformly. - Semantic types are preserved —
Array(T)staysArray(T). The language says “render Array as GenericWrap(Vec)” not “rewrite Array to Generic(‘Vec’, [T])”. GenericWrapreusesgeneric_syntax().open/.close— languages that already configure these delimiters get correct rendering automatically.
Adding a Language
sigil-stitch supports new languages by implementing the CodeLang trait. The trait has 33 methods: 10 required (no default) plus 6 config struct accessors and 17 override methods — all with sensible defaults. You only need to override the defaults when your language diverges from the common patterns.
This guide walks through the process using a hypothetical language, with references to real implementations you can study.
Overview
Adding a language takes four steps:
- Create
src/lang/your_lang.rsimplementingCodeLang - Add
pub mod your_lang;tosrc/lang/mod.rs - Write integration tests in
tests/ - Run
just blessto generate golden files
The CodeLang Trait
The trait methods fall into natural groups.
Core Methods (6 required)
These are enough for CodeBlock-level code generation:
| Method | Example (TypeScript) | Purpose |
|---|---|---|
file_extension() | "ts" | File extension for output files |
reserved_words() | &["async", "await", ...] | Words that need escaping |
render_imports() | import { Foo } from '...' | Emit the import header |
render_string_literal() | 'hello' | Language-specific string quoting |
render_doc_comment() | /** ... */ | Doc comment block |
line_comment_prefix() | "//" | Single-line comment prefix |
render_imports() is the most complex. It receives an ImportGroup (deduplicated, with aliases resolved) and must emit the full import header string. Study src/lang/typescript.rs for ES module imports or src/lang/rust_lang.rs for use paths.
Spec Support Methods (4 required)
These enable TypeSpec, FunSpec, and FieldSpec rendering:
| Method | Example | Purpose |
|---|---|---|
render_visibility() | "public ", "pub " | Visibility prefix |
function_keyword() | "function", "fn" | Function declaration keyword |
type_keyword() | "class", "struct" | Type declaration keyword |
methods_inside_type_body() | true / false | Key structural decision (see below) |
The methods_inside_type_body Decision
This is the most important method for structural correctness. It determines whether TypeSpec emits one CodeBlock or two:
- Returns
true(TypeScript, Java, Python, Swift, Dart, Kotlin, C++): Methods go inside the type body. TypeSpec emits a single block:class Foo { fields; methods; }. - Returns
false(Rust struct/enum): Methods go in a separateimplblock. TypeSpec emits two blocks:struct Foo { fields }andimpl Foo { methods }.
The method takes a TypeKind parameter, so you can vary by type. Rust returns true for TypeKind::Trait (trait methods go inside) but false for TypeKind::Struct and TypeKind::Enum.
Config Struct Accessors and Default Methods
Instead of dozens of individual trait methods, the v2.0 API groups related configuration into 6 config structs returned by accessor methods. Each struct uses ..Default::default() so you only specify fields where your language differs. The remaining standalone override methods cover cases that don’t fit neatly into a struct.
block_syntax()
Returns BlockSyntaxConfig controlling block delimiters and formatting:
| Field | Default | Purpose |
|---|---|---|
block_open | " {" | Opening delimiter. Python overrides to ":". |
block_close | "}" | Closing delimiter. Python overrides to "" (indent-only). |
indent_unit | " " (2 spaces) | Indentation per level. |
uses_semicolons | true | Statement terminator behavior. |
field_terminator | "," | After each field. Java/C++ override to ";". |
type_close_terminator | (default) | Terminator after closing brace for types. |
bases_close | (default) | Closing syntax for base-class lists. |
function_syntax()
Returns FunctionSyntaxConfig controlling function declarations:
| Field | Default | Purpose |
|---|---|---|
return_type_separator | ": " | Between params and return type. Rust overrides to " -> ". |
async_keyword | "async " | Async function prefix. |
abstract_keyword | "abstract " | Abstract method prefix. C++ overrides to "virtual ". |
param_list_style | (default) | How parameter lists are formatted. |
function_signature_style | (default) | Controls overall signature layout. |
constructor_keyword | "" | Constructor keyword. Python: "def". Rust: "fn". |
constructor_delegation_style | (default Body) | Super/this call placement. Kotlin: Signature. |
where_clause_style | (default) | How where clauses are rendered. |
empty_body | "" | Empty method body. Python overrides to "...". |
type_decl_syntax()
Returns TypeDeclSyntaxConfig controlling type declarations:
| Field | Default | Purpose |
|---|---|---|
type_before_name | false | C/C++/Java override to true for int count. |
return_type_is_prefix | false | C/C++/Java override to true for int add(...). |
type_annotation_separator | ": " | Between name and type annotation. |
super_type_keyword | (default) | Inheritance keyword, e.g. " extends ". |
super_type_separator | (default) | Separator between multiple super types. |
super_type_subsequent_separator | (default) | Separator for subsequent super types. |
implements_keyword | (default) | Interface keyword, e.g. " implements ". |
type_alias_target_first | false | C overrides to true for typedef target name;. |
supports_primary_constructor | false | Kotlin overrides to true. |
generic_syntax()
Returns GenericSyntaxConfig controlling generic/type-parameter syntax:
| Field | Default | Purpose |
|---|---|---|
open | "<" | Generic opening bracket. Go overrides to "[". |
close | ">" | Generic closing bracket. Go overrides to "]". |
application_style | (default) | How generics are applied to types. |
constraint_keyword | ": " | Generic bounds keyword. Java/TS override to " extends ". |
constraint_separator | " + " | Between multiple bounds. Java/TS override to " & ". |
context_bound_keyword | (default) | Context bound syntax (e.g. Scala’s :). |
enum_and_annotation()
Returns EnumAndAnnotationConfig controlling enums, annotations, and field modifiers:
| Field | Default | Purpose |
|---|---|---|
variant_prefix | "" | Enum variant prefix. Swift overrides to "case ". |
variant_prefix_first | (default) | Prefix for the first variant specifically. |
variant_separator | "," | Between enum variants. Python/Swift override to "". |
variant_trailing_separator | false | Rust/TypeScript override to true. |
annotation_prefix | "@" | Annotation opening. Rust: "#[". C++: "[[". |
annotation_suffix | "" | Annotation closing. Rust: "]". C++: "]]". |
readonly_keyword | "const " | TS: "readonly ". Kotlin: "val ". Java: "final ". |
mutable_field_keyword | "" | Kotlin overrides to "var ". |
type_presentation()
Returns TypePresentationConfig controlling how semantic types (arrays, optionals, maps, tuples, references, function types, etc.) are rendered. See the Type Presentation section below for details.
Standalone Override Methods
These methods don’t belong to a config struct but have sensible defaults you can override:
escape_reserved()– how reserved words are escaped.qualify_import_name()– default passthrough. Go overrides to return"http.Server"(package-qualified names).module_separator()– returnsOption<&str>. DefaultNone. Override toSome("::")(Rust/C++) orSome(".")(Go/Python/Java/etc.) to enableTypeName::qualified()inline rendering.type_kind_suffix()– suffix after type close for specific type kinds.render_newtype_line()– default emits Rust tuple structstruct Name(Inner);. Go:type Name Inner, Kotlin:value class Name(val value: Inner), Python:Name = NewType("Name", Inner), C:typedef Inner Name;.fun_block_open()– custom block opener for functions.type_header_block_open()– custom block opener for type headers.doc_comment_inside_body()– whether doc comments go inside the body (Python docstrings).doc_before_annotations()– whether doc comments appear before annotations.optional_field_style()– how optional fields are represented.property_style()– defaultAccessor(TS/JS:get name()). Swift/Kotlin:Field(inline get/set).property_getter_keyword()– default"get". Kotlin:"get()".render_type_context()– additional context for type rendering.type_body_prefix()– content emitted before the type body.type_body_suffix()– content emitted after the type body.render_type_close_suffix()– suffix after type close brace.render_type_param_kind()– how type parameters are annotated with variance.line_comment_suffix()– suffix for line comments (default"").
Step-by-Step Walkthrough
1. Create the language file
Create src/lang/your_lang.rs:
use crate::import::ImportGroup;
use crate::lang::CodeLang;
use crate::spec::modifiers::{DeclarationContext, TypeKind, Visibility};
#[derive(Debug, Clone, Default)]
pub struct YourLang;
impl YourLang {
pub fn new() -> Self {
Self
}
}
const RESERVED: &[&str] = &["if", "else", "for", "while", /* ... */];
impl CodeLang for YourLang {
fn file_extension(&self) -> &str { "yl" }
fn reserved_words(&self) -> &[&str] { RESERVED }
fn line_comment_prefix(&self) -> &str { "//" }
fn render_string_literal(&self, s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
fn render_doc_comment(&self, lines: &[&str]) -> String {
let mut out = String::from("/**\n");
for line in lines {
out.push_str(&format!(" * {line}\n"));
}
out.push_str(" */\n");
out
}
fn render_imports(&self, imports: &ImportGroup) -> String {
// Build your import statements from imports.by_module()
let mut out = String::new();
for (module, entries) in imports.by_module() {
let names: Vec<&str> = entries.iter().map(|e| e.resolved_name.as_str()).collect();
out.push_str(&format!("import {{ {} }} from \"{}\";\n", names.join(", "), module));
}
out
}
// Spec support methods...
fn render_visibility(&self, vis: Visibility, _ctx: DeclarationContext) -> &str {
match vis {
Visibility::Public => "public ",
Visibility::Private => "private ",
Visibility::Protected => "protected ",
_ => "",
}
}
fn function_keyword(&self, _ctx: DeclarationContext) -> &str { "function" }
fn type_keyword(&self, kind: TypeKind) -> &str {
match kind {
TypeKind::Class => "class",
TypeKind::Interface | TypeKind::Trait => "interface",
TypeKind::Enum => "enum",
TypeKind::Struct => "class",
TypeKind::TypeAlias => "type",
TypeKind::Newtype => "class",
}
}
fn methods_inside_type_body(&self, _kind: TypeKind) -> bool { true }
// Config struct overrides...
fn block_syntax(&self) -> BlockSyntaxConfig<'_> {
BlockSyntaxConfig {
uses_semicolons: true,
indent_unit: " ",
field_terminator: ";",
..Default::default()
}
}
fn type_decl_syntax(&self) -> TypeDeclSyntaxConfig<'_> {
TypeDeclSyntaxConfig {
super_type_keyword: " extends ",
implements_keyword: " implements ",
..Default::default()
}
}
fn generic_syntax(&self) -> GenericSyntaxConfig<'_> {
GenericSyntaxConfig {
constraint_keyword: " extends ",
constraint_separator: " & ",
..Default::default()
}
}
fn function_syntax(&self) -> FunctionSyntaxConfig<'_> {
FunctionSyntaxConfig {
return_type_separator: ": ",
..Default::default()
}
}
}
2. Register the module
Add to src/lang/mod.rs:
/// YourLang language support.
pub mod your_lang;
3. Write tests
Create two test files following the existing pattern:
tests/your_lang_tests.rs – basic CodeBlock rendering:
mod golden;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::lang::your_lang::YourLang;
use sigil_stitch::type_name::TypeName;
#[test]
fn test_basic_statement() {
let mut cb = CodeBlock::builder();
cb.add_statement("const x = 1", ());
let block = cb.build().unwrap();
let lang = YourLang::new();
let imports = sigil_stitch::import::ImportGroup::new();
let mut renderer = sigil_stitch::code_renderer::CodeRenderer::new(&lang, &imports, 80);
let output = renderer.render(&block).unwrap();
golden::assert_golden("your_lang", "basic_statement", "yl", &output);
}
tests/phase2_your_lang_tests.rs – spec-layer rendering (TypeSpec, FunSpec, FileSpec).
4. Generate golden files
just bless
This runs all tests with BLESS=1, which creates tests/golden/your_lang/*.yl files from the actual output. Review them manually, then commit.
5. Override defaults
Run the full test suite and review golden file output. Override config struct accessors and default methods where your language’s syntax differs. Common overrides:
- If your language uses indentation instead of braces: override
block_syntax()to setblock_open,block_close; overridefunction_syntax()to setempty_body - If types come before names (
int xinstead ofx: int): overridetype_decl_syntax()to settype_before_name,return_type_is_prefix - If generics use brackets instead of angle brackets: override
generic_syntax()to setopen,close
Reference Implementations
Study these existing implementations for patterns similar to your target:
| Language | File | Notable Patterns |
|---|---|---|
| TypeScript | src/lang/typescript.rs | ES module imports, type-only imports, single-quoted strings |
| Rust | src/lang/rust_lang.rs | use paths, struct+impl split, pub(crate) visibility |
| Python | src/lang/python.rs | Indent-only blocks (no braces), docstrings inside body, from x import y |
| Go | src/lang/go_lang.rs | Package-qualified names (http.Server), bracket generics, func keyword |
| C | src/lang/c_lang.rs | Type-before-name, #include, __attribute__, struct close semicolon |
| C++ | src/lang/cpp_lang.rs | virtual instead of abstract, #include + using, [[attributes]] |
| Bash | src/lang/bash.rs | Keyword-based block closers (fi/done/esac), source imports, shell escaping |
| Scala | src/lang/scala.rs | case class, trait, [T] generics, <: bounds, = {/} blocks |
| Haskell | src/lang/haskell.rs | Split signature style, where/indentation blocks, postfix generics, deriving |
| OCaml | src/lang/ocaml.rs | Postfix generics, let keyword, = /indentation blocks, open Module imports, module_block helper |
Type Presentation
When your language uses type expressions (generics, arrays, optionals, maps, etc.), you configure how each semantic type concept renders by returning a TypePresentationConfig from the type_presentation() accessor. You never build BoxDoc directly.
How it works
Each TypeName variant (Array, Optional, Map, etc.) uses your language’s TypePresentationConfig to determine the syntactic pattern via TypePresentation — a small enum:
GenericWrap { name }—name<P1, P2>using yourgeneric_syntax().open/generic_syntax().closePrefix { prefix }—prefix inner(e.g., Go[]T, Rust*const T)Postfix { suffix }—inner suffix(e.g., TypeScriptT[], KotlinT?)Surround { prefix, suffix }—prefix inner suffix(e.g., C++const T&, Cconst T*)Delimited { open, sep, close }—open P1 sep P2 close(e.g., Swift[K: V], Gomap[K]V)Infix { sep }—P1 sep P2(e.g., TypeScriptA | B, RustA + B)
Configuring type presentation
All fields in TypePresentationConfig have defaults matching TypeScript conventions. Override only when your language differs:
impl CodeLang for YourLang {
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
// Array: default is Postfix { suffix: "[]" } (TS: T[])
// Override for Rust-style Vec<T>:
array: TypePresentation::GenericWrap { name: "Vec" },
// Optional: default is Infix { sep: " | " } with "null" literal
// Override for Kotlin-style T?:
optional: TypePresentation::Postfix { suffix: "?" },
// Map: default is GenericWrap { name: "Map" }
// Override for Go-style map[K]V:
map: TypePresentation::Delimited { open: "map[", sep: "]", close: "" },
// Tuple: default is Delimited { open: "(", sep: ", ", close: ")" }
// TS overrides to "[", "]" for [A, B] syntax. This shows Go-style (A, B):
tuple: TypePresentation::Delimited { open: "(", sep: ", ", close: ")" },
// Reference: default is Prefix { prefix: "" } (identity — for GC languages)
// Override for Rust-style &T:
reference: TypePresentation::Prefix { prefix: "&" },
// Function types: default is TypeScript (A, B) => R
function: FunctionPresentation {
keyword: "fn",
params_open: "(",
params_sep: ", ",
params_close: ")",
arrow: " -> ",
return_first: false,
curried: false,
wrapper_open: "",
wrapper_close: "",
},
..Default::default()
}
}
}
See Type Presentation for the full enum definition, all available fields, and examples for every supported language.