Rust

Example Output

The structs: lib.rs

The traits: poly.rs

Overview

Warning

The rust generator is still currently under development. Notable missing features are ifabsent processing and the enforcement of rules and constraints.

The Rust Generator produces a Rust crate with structs and enums from a LinkML model, with optional pyo3 and serde support. It additionally generates a trait for every struct to provide polymorphic access, in the poly.rs file.

For all classes having subclasses, an extra enum is generated to represent an object of the class or a subtype. All the enums implement the trait, so they can be (optionally) directly used without match statement.

How to Generate a Rust Crate with Python Bindings

It is possible to generate a Rust Crate from a linkml schema complete with python bindings.

The steps below walk through producing a PyO3-enabled crate from a LinkML schema and installing it in a Python environment. The commands were exercised against examples/PersonSchema/personinfo.yaml to verify they work end-to-end.

To build a rust crate as a python lib, you need maturin. Maturin can easily be installed using pip. A convenient way to build pip packages for multiple python versions and libc alternatives is the manylinux docker image.

  1. Generate the crate using the main linkml CLI. The --mode crate option is the default; --pyo3 and --serde ensure the generated Cargo.toml enables those features by default. Add --handwritten-lib when you want a regeneration-friendly layout: generated sources land under src/generated while src/lib.rs becomes a shim that is created only on the first run and then left untouched.

    linkml generate rust \
      --output personinfo_rust \
      --pyo3 --serde --handwritten-lib \
      --force \
      examples/PersonSchema/personinfo.yaml
    

    The output directory contains Cargo.toml (with [lib] configured for both cdylib and rlib when PyO3 is requested), pyproject.toml with a maturin build-system section, and a src/generated tree that houses the regenerated code. A small src/lib.rs file is emitted on the first run only and re-exported functions from generated so you can regenerate safely.

  2. Adjust what is exposed to Python in src/lib.rs. The file declares the PyO3 module and calls generated::register_pymodule; edit it to add your own functions/classes. Because the shim is only created on the first run, later regenerations leave your changes intact.

  3. Generate type stubs so Python users get better typing support. With the stubgen feature enabled, run cargo run --bin stub_gen --features stubgen from the crate directory; add -- --check when you want to verify existing stubs instead of overwriting them.

  4. Build and install the wheel with maturin. Running maturin develop compiles the extension and puts it in the active virtual environment.

    cd personinfo_rust
    maturin develop
    

    maturin build is an alternative when you want distributable wheels in target/wheels instead.

  5. Import the module from Python and try the generated YAML loader.

    import personinfo
    # Update the path below to point at your data file.
    container = personinfo.load_yaml_container("examples/PersonSchema/data/example_personinfo_data.yaml")
    print(container.persons[0].name)
    

When repeating the process, pass --force (as above) or delete the output directory to avoid collisions with previous runs.

Feature Compliance

The current implementation status is summarised below. These notes mirror the ongoing work tracked in linkml/linkml#2360.

Supported

  • Core schema constructs: slots, classes, enums, and type aliases are emitted as Rust structs, enums, and aliases.

  • Basic metamodel features: multivalued slots, required vs. optional cardinalities, inheritance (is_a), union slots (any_of), inline list/dict slots, and slot aliases.

  • Build targets: both single-file output and full Cargo crates (with Cargo.toml).

  • Fundamental scalar types: string, integer, bool, and float map to native Rust types.

  • Temporal scalars: date and datetime map to chrono’s NaiveDate and NaiveDateTime respectively.

  • Traits for polymorphic access to class hierarchies, along with enums for class-or-subtype containers.

  • PyO3 bindings for the generated structs (behind a Cargo feature flag).

  • Basic serde deserialization and serialization, including normalisation (behind a Cargo feature flag).

Partially Supported

  • Many scalar types (e.g. time, URI-related types) currently fall back to String representations.

  • Testing covers unit-level behaviour with a dedicated Rust CI workflow; dynamic compilation and compliance suites are still pending.

Not Yet Supported

  • Default handling (ifabsent) and broader constraint enforcement (values_from, value_presence, equality and cardinality checks, numeric bounds, and pattern).

  • Schema metadata exports (linkml_meta hash maps and module-level constants such as id and version) .

  • Serde data normalisation for serialization

  • Compliance test integration

  • Rule/expression support

  • Dynamic enumerations

Example

Given a definition of a Person class:

Event:
  slots:
    - started_at_time
    - ended_at_time
    - duration
    - is_current

EmploymentEvent:
  is_a: Event
  slots:
    - employed_at

MedicalEvent:
  is_a: Event
  slots:
    - in_location
    - diagnosis
    - procedure

The generate rust looks like this (serde and pyo3 annotations omitted for brevity):

pub struct Event {
    pub started_at_time: Option<NaiveDate>,
    pub ended_at_time: Option<NaiveDate>,
    pub duration: Option<f64>,
    pub is_current: Option<bool>
}

pub struct EmploymentEvent {
    pub employed_at: Option<String>,
    pub started_at_time: Option<NaiveDate>,
    pub ended_at_time: Option<NaiveDate>,
    pub duration: Option<f64>,
    pub is_current: Option<bool>
}

pub struct MedicalEvent {
    pub in_location: Option<String>,
    pub diagnosis: Option<DiagnosisConcept>,
    pub procedure: Option<ProcedureConcept>,
    pub started_at_time: Option<NaiveDate>,
    pub ended_at_time: Option<NaiveDate>,
    pub duration: Option<f64>,
    pub is_current: Option<bool>
}

pub enum EventOrSubtype {
    Event(Event),
    EmploymentEvent(EmploymentEvent),
    MedicalEvent(MedicalEvent)
}

polymorphic traits are implemented:

pub trait Event {
    fn started_at_time<'a>(&'a self) -> Option<&'a NaiveDate>;
    fn ended_at_time<'a>(&'a self) -> Option<&'a NaiveDate>;
    fn duration<'a>(&'a self) -> Option<&'a f64>;
    fn is_current<'a>(&'a self) -> Option<&'a bool>;
}

pub trait MedicalEvent: Event {
    fn in_location<'a>(&'a self) -> Option<&'a str>;
    fn diagnosis<'a>(&'a self) -> Option<&'a crate::DiagnosisConcept>;
    fn procedure<'a>(&'a self) -> Option<&'a crate::ProcedureConcept>;
}

impl Event for crate::MedicalEvent {
        fn started_at_time(&self) -> Option<&NaiveDate> {
        self.started_at_time.as_ref()
    }
        fn ended_at_time(&self) -> Option<&NaiveDate> {
        self.ended_at_time.as_ref()
    }
        fn duration(&self) -> Option<&f64> {
        self.duration.as_ref()
    }
        fn is_current(&self) -> Option<&bool> {
        self.is_current.as_ref()
    }
}

...

impl Event for crate::EventOrSubtype {
        fn started_at_time(&self) -> Option<&NaiveDate> {
        match self {
                EventOrSubtype::Event(val) => val.started_at_time(),
                EventOrSubtype::EmploymentEvent(val) => val.started_at_time(),
                EventOrSubtype::MedicalEvent(val) => val.started_at_time(),

        }
    }
        fn ended_at_time(&self) -> Option<&NaiveDate> {
        match self {
                EventOrSubtype::Event(val) => val.ended_at_time(),
                EventOrSubtype::EmploymentEvent(val) => val.ended_at_time(),
                EventOrSubtype::MedicalEvent(val) => val.ended_at_time(),

        }
    }
        fn duration(&self) -> Option<&f64> {
        match self {
                EventOrSubtype::Event(val) => val.duration(),
                EventOrSubtype::EmploymentEvent(val) => val.duration(),
                EventOrSubtype::MedicalEvent(val) => val.duration(),

        }
    }
        fn is_current(&self) -> Option<&bool> {
        match self {
                EventOrSubtype::Event(val) => val.is_current(),
                EventOrSubtype::EmploymentEvent(val) => val.is_current(),
                EventOrSubtype::MedicalEvent(val) => val.is_current(),

        }
    }
}

Command Line

gen-rust

gen-rust [OPTIONS] YAMLFILE

Options

-V, --version

Show the version and exit.

-o, --output <output>

Output directory (crate mode) or .rs file (file mode)

-n, --crate-name <crate_name>

Name of the generated crate/module

--handwritten-lib, --no-handwritten-lib

When enabled, place generated sources under src/generated and create a shim lib.rs for handwritten code. The shim is only created on first run and left untouched on subsequent regenerations.

-s, --serde

Add ‘serde’ to Cargo.toml default features. Source always includes #[cfg(feature=”serde”)] derives/attrs; this flag only enables the crate feature by default.

-p, --pyo3

Add ‘pyo3’ to Cargo.toml default features and emit Python module glue (cdylib + #[pymodule]). Source always includes #[cfg(feature=”pyo3”)] gates; this flag only enables the crate feature by default.

-f, --force

Overwrite output if it already exists

-m, --mode <mode>

Generation mode: ‘crate’ (Cargo package) or ‘file’ (single .rs)

Options:

crate | file

-f, --format <format>

Output format

Default:

'rust'

Options:

rust

--metadata, --no-metadata

Include metadata in output

Default:

True

--useuris, --metauris

Use class and slot URIs over model uris

Default:

True

-im, --importmap <importmap>

Import mapping file

--log_level <log_level>

Logging level

Default:

'WARNING'

Options:

CRITICAL | ERROR | WARNING | INFO | DEBUG

-v, --verbose

Verbosity. Takes precedence over –log_level.

--mergeimports, --no-mergeimports

Merge imports into source file (default=mergeimports)

--stacktrace, --no-stacktrace

Print a stack trace when an error occurs

Default:

False

Arguments

YAMLFILE

Required argument

Generator

class linkml.generators.rustgen.RustGenerator(schema: str | ~typing.TextIO | ~linkml_runtime.linkml_model.meta.SchemaDefinition | ~linkml.utils.generator.Generator | ~pathlib.Path, schemaview: ~linkml_runtime.utils.schemaview.SchemaView | None = None, format: str | None = None, metadata: bool = True, useuris: bool | None = None, log_level: int | None = 30, mergeimports: bool | None = True, source_file_date: str | None = None, source_file_size: int | None = None, logger: ~logging.Logger | None = None, verbose: bool | None = None, output: ~pathlib.Path | None = None, namespaces: ~linkml_runtime.utils.namespaces.Namespaces | None = None, directory_output: bool = False, base_dir: str = None, metamodel_name_map: dict[str, str] = None, importmap: str | ~collections.abc.Mapping[str, str] | None = None, emit_prefixes: set[str] = <factory>, metamodel: ~linkml.utils.schemaloader.SchemaLoader = None, stacktrace: bool = False, include: str | ~pathlib.Path | ~linkml_runtime.linkml_model.meta.SchemaDefinition | None = None, crate_name: str | None = None, pyo3: bool = True, pyo3_version: str = '>=0.21.1', serde: bool = True, stubgen: bool = True, handwritten_lib: bool = False, mode: ~typing.Literal['crate', 'file'] = 'crate', _environment: ~jinja2.environment.Environment | None = None)[source]

Generate rust types from a linkml schema

generate_attribute(attr: SlotDefinition, cls: ClassDefinition) AttributeResult[source]

Generate an attribute as a struct property

generate_cargo(imports: Imports) RustCargo[source]

Generate a Cargo.toml file

generate_class(cls: ClassDefinition) ClassResult[source]

Generate a class as a struct!

generate_pyproject() RustPyProject[source]

Generate a pyproject.toml file for a pyo3 rust crate

generate_slot(slot: SlotDefinition) SlotResult[source]

Generate a slot as a struct field

generatorname: ClassVar[str] = 'rustgenerator'

Name of the generator. Override with os.path.basename(__file__)

generatorversion: ClassVar[str] = '0.0.2'

Version of the generator. Consider deprecating and instead use overall linkml version

get_rust_range_info_across_descendants(cls: ClassDefinition, s: SlotDefinition) RustRange[source]

Compute a RustRange representing the union of a slot’s ranges across a class and all its descendants.

Container and optionality are taken from the base class slot.

handwritten_lib: bool = False

Place generated sources under src/generated and leave src/lib.rs for user code

mode: Literal['crate', 'file'] = 'crate'

Generate a cargo.toml file

output: Path | None = None
  • If mode == "crate" , a directory to contain the generated crate

  • If mode == "file" , a file with a .rs extension

If output is not provided at object instantiation, it must be provided on a call to serialize()

pyo3: bool = True

Generate pyO3 bindings for the rust defs

render(mode: Literal['file'] = 'file') FileResult[source]
render(mode: Literal['crate'] = 'crate') CrateResult

Render the template model of a rust file before serializing

Parameters:

mode (RUST_MODES, optional) – Override the instance-level generation mode

serde: bool = True

Generate serde derive serialization/deserialization attributes

serialize(output: Path | None = None, mode: Literal['crate', 'file'] | None = None, force: bool = False) str[source]

Serialize a schema to a rust crate or file.

Parameters:
  • output (Path, optional) – A .rs file if in file mode, directory otherwise.

  • force (bool) – If the output already exists, overwrite it. Otherwise raise a FileExistsError

stubgen: bool = True

Generate pyo3-stub-gen instrumentation alongside PyO3 bindings

valid_formats: ClassVar[list[str]] = ['rust']

Allowed formats - first format is default

Features

  • Serde: Code that depends on Serde is behind the Cargo feature serde (#[cfg(feature = "serde")]).

  • PyO3: Python bindings are behind the Cargo feature pyo3 (#[cfg(feature = "pyo3")]).

  • Enable features when building your crate (e.g., --features serde,pyo3) to include the corresponding code paths.

Single-File Mode

  • When generating a single .rs file (--mode file): - serde_utils is inlined into the file (no separate module file). - Polymorphic traits/containers (poly.rs/poly_containers.rs) are not emitted — they are crate-mode only.