RustQ is Rust template quasiquoting for Elixir. It helps Elixir projects generate
Rust from real .rs templates instead of building Rust strings by hand.
Use it for Rustler projects, schema-driven NIF surfaces, or any codegen where you want Rust syntax highlighting, Rust parsing, formatting, and AST-aware placeholder replacement.
Installation
Add RustQ to mix.exs:
{:rustq, "~> 0.1", only: [:dev, :test], runtime: false}RustQ compiles a Rustler NIF at generation time, so Rust/Cargo must be available
where mix rustq.gen or your own codegen task runs.
Generate from real Rust templates
Templates are ordinary Rust with parseable placeholder forms:
use RustQ.Sigil
alias RustQ.Rust
template = ~R"""
pub struct __rq_Resource {
__rq_fields: (),
}
impl __rq_Resource {
__rq_methods!();
pub fn table() -> &'static str {
__rq_table_name!()
}
}
"""
code =
template
|> RustQ.parse!("resource.rs")
|> RustQ.bind(Resource: :User, table_name: {:literal, "users"})
|> RustQ.splice(:fields, [
Rust.field(:id, :i64, vis: :pub),
Rust.field(:name, :String, vis: :pub)
])
|> RustQ.splice(:methods, [
Rust.fn(:new,
vis: :pub,
args: [id: :i64, name: :String],
returns: :Self,
body: "Self { id, name }"
)
])
|> RustQ.codegen!()For file templates:
RustQ.render_file!("priv/templates/resource.rs",
bind: [Resource: :User],
splice: [fields: [RustQ.Rust.field(:id, :i64, vis: :pub)]]
)Placeholder forms
RustQ placeholders use the visually distinct __rq_ prefix. The exact shape
matches the Rust syntax position, but the name is consistent with the Elixir
bind: or splice: key:
__rq_Name— identifier, type path, or lifetime replacement.__rq_value!()— expression or type replacement.__rq_items!();— item splice point.__rq_methods!();— impl-item splice point.__rq_body!();— statement splice point.__rq_arms => unreachable!(),— match-arm splice point.__rq_fields: (),— struct-field splice point.fn target(__rq_args: ()) {}— function-argument splice point.
Rust builders
RustQ.Rust provides small Elixir builders for common Rust fragments:
alias RustQ.Rust
items = [
Rust.use([:std, :sync, :OnceLock]),
Rust.const(:TABLE, {:ref, :str}, Rust.expr(Rust.literal("users")), vis: :pub),
Rust.struct(:User,
vis: :pub,
derive: [:Clone, :Debug],
fields: [Rust.field(:id, :i64, vis: :pub)]
)
]Use Rust.raw/1, Rust.item/1, Rust.impl_item/1, Rust.stmt/1,
Rust.expr/1, and Rust.arm/1 when hand-written Rust is clearer than a builder.
Rustler helpers
RustQ.Rustler generates common Rustler code as Rust fragments:
RustQ.Rustler.atoms([:ok, :error, {"r#type", "type"}])
RustQ.Rustler.cached_atoms([:ok, node_changes: "nodeChanges"])
RustQ.Rustler.nif(:add,
args: [a: :i64, b: :i64],
returns: :i64,
body: "a + b"
)
RustQ.Rustler.term_helpers(type_key: "atoms::r#type()")
RustQ.Rustler.term_decoder(:ProgramInput,
fields: [
body: [type: {:vec, "Term<'a>"}, key: "atoms::body()", required: true]
]
)Safe term builders use Term<'a>:
RustQ.Rustler.term_builders(include: [:map_from_terms, :struct_from_terms])Low-level raw NIF_TERM helpers are explicit:
RustQ.Rustler.nif_term_builders(include: [:map_from_nif_terms, :struct_from_nif_terms])Rustler schema DSL
For larger Elixir struct surfaces, define a schema once and generate Rust NIF structs plus tagged enums:
defmodule MyApp.Codegen.ContentSchema do
use RustQ.Rustler.Schema
schema MyApp.Content do
default_attrs ["allow(dead_code)"]
node Text do
field :text, :String
field :size, {:option, :String}
end
node Paragraph do
field :body, {:vec, Content}
end
tagged_enum Content do
variants :all
unknown :unknown_content_variant
end
end
endOptionality is part of the Rust type ({:option, :String}), not a separate
boolean flag.
Generated files with rustq.exs
Create rustq.exs in your project root:
use RustQ.Config
alias RustQ.Rustler
require_file "lib/my_app/codegen/content_schema.ex"
rust "native/my_nif/src/generated_term_helpers.rs" do
Rustler.term_helpers(type_key: "atoms::r#type()")
end
rust "native/my_nif/src/generated_content.rs" do
MyApp.Codegen.ContentSchema.rust_items()
endThe manifest is ordinary Elixir, so use aliases, helper functions, modules, and macros to keep project-specific codegen readable.
Then run:
mix rustq.gen
mix rustq.gen --check
mix rustq.gen term_helpers
Path-only targets infer their name from the file name and strip a leading
generated_, so generated_term_helpers.rs is selectable as term_helpers.
Use mix rustq.gen --check in CI to fail when generated files are stale.
Fragment validation
You can validate individual Rust fragments in the same contexts RustQ splices:
RustQ.valid_fragment?(:field, "pub id: i64")
RustQ.parse_fragment!(:arm, RustQ.Rust.arm("Some(value)", "value"))License
MIT