Skip to content

dannote/rustq

Repository files navigation

RustQ

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.

For larger wrapper bodies, prefer real Rust templates with placeholders over assembling statement lists in Elixir.

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.nif_exports(
  render_png: [
    args: [env: "Env<'a>", batch: "Term<'a>"],
    returns: "NifResult<Term<'a>>",
    lifetime: :a,
    schedule: :dirty_cpu
  ]
)

RustQ.Rustler.term_helpers(type_key: "atoms::r#type()")
RustQ.Rustler.opts_helpers()
RustQ.Rustler.term_decoder(:ProgramInput,
  fields: [
    body: [type: {:vec, "Term<'a>"}, key: "atoms::body()", required: true]
  ]
)

RustQ.Rustler.resource_handle(:EncodedImage,
  fields: [bytes: "Vec<u8>"],
  handle_field: "ref"
)

Atom-based decoders and dispatchers are intentionally low-level so projects can compose them into their own command, AST, or schema models:

RustQ.Rustler.atom_decoder(:decode_blend_mode,
  returns: :BlendMode,
  cases: [src_over: "BlendMode::SrcOver", multiply: "BlendMode::Multiply"]
)

RustQ.Rustler.atom_dispatch(:draw_command,
  args: [surface: "&mut Surface", command: "Term<'a>"],
  on: "command.map_get(atoms::op())?.decode::<Atom>()?",
  cases: [rect: "draw_rect(surface, command)"],
  unknown: "Ok(())"
)

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

    node Enum, rust: :ExEnum, module: MyApp.Content.EnumList do
      field :children, {:vec, Content}
    end

    tagged_enum Content do
      variants :all
      unknown :unknown_content_variant
    end
  end
end

Optionality 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()
end

The 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

About

Rust template quasiquoting and code generation for Elixir

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors