Skip to content

Add Rust language target#1135

Open
jamesainslie wants to merge 24 commits into
grafana:mainfrom
jamesainslie:add-rust-target
Open

Add Rust language target#1135
jamesainslie wants to merge 24 commits into
grafana:mainfrom
jamesainslie:add-rust-target

Conversation

@jamesainslie

Copy link
Copy Markdown

What

Adds a Rust language target to cog, capable of generating a complete, idiomatic Rust version of the Grafana Foundation SDK.

This addresses the Rust support request in grafana/grafana-foundation-sdk#1208.

How

The target mirrors the structure of the existing python/golang jennies:

  • internal/jennies/rust/rawtypes.go + types.go: type emission. Structs are PascalCase with snake_case fields and #[serde(rename)] preserving wire keys; not-required fields are Option<T> with skip_serializing_if (collections stay bare Vec/HashMap with is_empty skips, mirroring Go's omitempty); enums use serde renames (numeric enums via serde_repr); disjunctions become #[serde(untagged)] enums; constant references pin their values through Default impls; recursive references are Boxed (including cycles through Option).
  • internal/jennies/rust/builder.go: builder emission. Each object gets a <Name>Builder { internal, errors } with a chaining option API; constraints accumulate cog::BuildErrors surfaced by build() -> Result<T, Vec<BuildError>>; delegation takes impl cog::Builder<T>; envelopes, nested-path initialization (get_or_insert_with), factories and composable-slot options are supported.
  • internal/jennies/rust/templates/runtime/: the hand-written runtime shipped with every generated crate: the Builder<T> trait, BuildError, and a variant registry (object-safe Dataquery trait objects with discriminator-keyed decoders and a lossless UnknownDataquery fallback, mirroring the Go runtime's UnmarshalDataquery).
  • internal/jennies/rust/plugins.go: emits src/cog/plugins.rs registering each variant's decoder.
  • internal/jennies/rust/module_init.go: emits Cargo.toml, src/lib.rs and the module tree.
  • internal/jennies/rust/postprocessor.go: runs rustfmt over generated files (skipped gracefully when unavailable), mirroring the Go target's gofmt postprocessor.

Testing

  • Developed TDD against the existing golden-file harness: every rawtypes and builder fixture is in scope except those other targets also skip (intersections and anonymous_struct matching python, dashboard_panel matching python, builder_delegation_in_disjunction matching golang).
  • Generating the full Foundation SDK from the Grafana kind-registry produces a crate (113 files) that passes cargo build, cargo clippy --all-targets -- -D warnings and cargo fmt --check.
  • As an end-to-end check, a real 25-panel production dashboard (timeseries/stat/logs panels, prometheus and loki dataqueries, template variables) was rebuilt with the generated builders and its serialized JSON matched the reference dashboard structurally.

A follow-up PR to grafana-foundation-sdk would add the rust: entry to its .cog/config.yaml to start publishing the Rust SDK.

Adds internal/jennies/rust with the Language interface conformance
(Name, Jennies, CompilerPasses, NullableKinds) mirroring the python
target. No emission logic yet; jenny list is empty.
Adds the Rust field to OutputLanguage, wires parameter interpolation,
and dispatches the rust jenny in OutputLanguages(). Covered by tests
for language registration and parameter interpolation.
…iant stubs)

Adds the hand-written Rust runtime shipped with every generated SDK:
a Builder<T> trait returning Result<T, Vec<BuildError>>, a BuildError
type implementing Display and std::error::Error, and Phase 6 variant
registry stubs. A Runtime jenny renders them under src/cog/. Also
teaches the shared comment-header postprocessor the // leader for .rs
files.
Adds skipped golden-file test suites for the Rust RawTypes and Builder
jennies with the RustRawTypes/RustBuilder golden-dir naming and un-skip
checklists for Phases 3 and 4.
Adds the RawTypes jenny with a type formatter, import collector and
naming helpers. Emits idiomatic Rust for scalar aliases, top-level
constants, and structs: PascalCase types, snake_case fields with
serde rename to preserve wire keys, Option<T> with skip_serializing_if
for optionals, and derived or manual Default impls. Covers the scalars,
struct_with_defaults and struct_with_scalar_fields fixtures; other
rawtypes fixtures are skipped pending later phases.
Adds enum emission: string enums with serde rename to wire values, and
numeric enums via serde_repr with explicit discriminants so they
round-trip as JSON numbers. Disjunctions of scalars become untagged
enums. Registers the AnonymousEnumToExplicitType pass to hoist inline
enum fields. Brings the enums, disjunction_of_numbers and
struct_with_optional_fields fixtures in-scope.

Note: generated crates using numeric enums require the serde_repr
dependency (to be added to the generated Cargo.toml in Phase 7).
Top-level arrays and maps become Vec<T> and HashMap<String, V> type
aliases. Nullable collections render as bare Vec/HashMap (not Option),
matching the Go target's types, and not-required collections carry
skip_serializing_if = Vec/HashMap::is_empty to mirror Go's omitempty
wire behavior so empty collections are omitted from JSON. Manual Default
impls render collection literals. Brings arrays, maps and
struct_with_map_and_slice_default in-scope; struct_with_complex_fields
stays skipped pending refs and disjunctions.
References to types in other packages emit deduplicated
use crate::types::<package>::<Type> statements; same-package refs stay
bare. Package names are sanitized to legal Rust module identifiers
(dashes to underscores) consistently across emitted file paths and use
paths. Brings refs, reference_of_reference and package-with-dashes
in-scope. Phase 7 module_init must declare src/types/mod.rs and
src/lib.rs using the same package-name sanitization.
…Phase 3e)

Disjunctions of refs (discriminated and undiscriminated) become
#[serde(untagged)] enums; branch structs pin their discriminator
constant via a serde default fn so the discriminator key serializes
exactly once, byte-identical to the Go and Python targets (no duplicate
key). Constant references resolve to the referred enum or scalar type
with the pinned value preserved via the Default impl, including
Some(pinned) for nullable constant refs. Brings disjunctions,
disjunctions_of_refs_without_discriminator,
disjunctions_of_scalars_and_refs, constant_references,
constant_reference_as_default and constant_reference_discriminator
in-scope.

Note for Phase 6: variant disjunctions whose branches differ only by
discriminator value will need a custom Deserialize dispatching on the
discriminator rather than serde untagged.
…x fields) (Phase 3f)

Brings the remaining rawtypes fixtures in-scope: constraints and
time_hint map to their underlying wire types (validation is a builder
concern), field_with_struct_with_defaults delegates field defaults to
nested struct Defaults, and struct_with_complex_fields now resolves via
refs, disjunctions and constant references. Intersections stay skipped,
matching the python target.

Inline anonymous multi-arm disjunction fields are hoisted to named
#[serde(untagged)] enums (matching Python Union and Go union types)
instead of degrading to serde_json::Value. Unifies the scalar and ref
disjunction emitters onto one disambiguating untagged path, fixing a
latent duplicate-variant compile bug. Extracts shared isOptionWrapped
and isBareCollection predicates so serde attributes cannot drift from
emitted types.

All rawtypes fixtures pass except intersections (matches python) and the
dashboard/variant fixtures deferred to the composable-slot phase.
Adds the Builder jenny emitting a FooBuilder { internal: Foo } per
object: new() applies constructor constant and required-field
assignments, each option is a chaining pub fn name(mut self, ..) -> Self,
and build() implements cog::Builder<Foo> returning the constructed
object. Enum constructor constants resolve to typed variants that
serialize to the correct wire value. A rejectUnsupported guard makes
out-of-scope builder features (constraints, delegation, factories,
envelopes, variants, array-append, index assignment) fail loudly rather
than mis-generate. Brings basic_struct, basic_struct_defaults,
properties, constructor_argument and constructor_initializations
in-scope.
…ase 4b)

Builders gain an errors: Vec<cog::BuildError> field; constrained option
setters push a BuildError on violation and build() returns Err(errors)
when non-empty, else the constructed object. Implements numeric
comparison ops plus minLength/maxLength (char count); pattern
constraints are deferred pending a regex dependency. Brings constraints,
constant_assignment, discriminator_without_option and known_any
in-scope.

Fixes two latent bugs surfaced by multi-builder packages: builders are
now grouped one file per package with merged deduplicated imports, and
same-package type references in builder modules import from
crate::types::<pkg>.
Append assignments emit self.internal.<field>.push(value); index
assignments emit self.internal.<map>.insert(key, value), with the key
from a constant or option argument. Nullable maps stay bare HashMap so
their assignment is a plain direct set, matching the rawtypes type and
the Go/Python builders. Brings array_append, map_index_assignment and
nullable_map_assignment in-scope.
Option arguments that are themselves builders render as impl
cog::Builder<T>; the setter calls build(), appends any errors to the
parent and returns early on failure (matching the Go target), otherwise
assigns the built value (direct, Some, push or insert). Handles nested
Vec and map-of-builders, and cross-package foreign builders resolving
the For-type from its referred package. Brings builder_delegation,
references, foreign_builder, map_of_builders and struct_with_defaults
in-scope. Anonymous inline struct arguments widen to serde_json::Value,
matching the Python target. builder_delegation_in_disjunction stays
skipped (the Go target skips it too).
Multi-level option assignments initialize Option-wrapped intermediates
with get_or_insert_with(T::default) before assigning the leaf, matching
Go's nil-check-then-assign while reusing an existing value (no clobber);
bare intermediates need no guard. Envelope assignments construct the
nested object as a struct literal and push it. Brings envelope_assignment
and initialization_safeguards in-scope. anonymous_struct and
struct_fields_as_args_assignment stay skipped: their args are un-hoisted
inline anonymous structs (no named type in the builder IR), the same
reason the python target skips anonymous_struct.
…hase 4f + finalize)

Adds builder factories as named associated functions that chain preset
option calls (including nested factory calls and cross-package builder
refs), and brings factories, package-with-dashes and map_of_disjunctions
in-scope. This completes the non-variant builders; only the
composable-slot/variant fixtures remain for Phase 6 (plus the fixtures
the Go and Python targets also skip).

Introduces a FormatRustFiles postprocessor that runs rustfmt over
generated .rs files (degrading to unformatted-but-valid output when
rustfmt is absent), mirroring the Go target's gofmt postprocessor, and
deletes the hand-replicated rustfmt width heuristics from the jennies so
rustfmt owns all line wrapping. The golden tests run the same formatter
so fixtures stay consistent. Decomposes rejectUnsupported into per-
feature predicates, de-duplicates the constructor Default tail and the
delegated-value assignment path, and renames the builder formatValue
method to formatAssignmentValue.

Known gaps for Phase 7 (surfaced only when compiling a full multi-type
crate): recursive types need Box, plus a cross-builder factory ref and a
bare-value-to-any assignment.
… 6a)

Replaces the variant runtime stubs with a real registry: an object-safe
Dataquery trait (Box<dyn Dataquery>), a Panelcfg marker trait,
DataqueryConfig/PanelcfgConfig holding discriminator-keyed decoder fns,
and a Registry whose unmarshal_dataquery does hint -> datasource.type ->
UnknownDataquery fallback (data-preserving), mirroring the Go runtime's
UnmarshalDataquery. Discriminator-driven dispatch is used because serde
untagged cannot disambiguate variants differing only by discriminator.
No typetag/erased-serde dependency. The rawtypes jenny emits impl
variants::Dataquery for dataquery-variant objects. Brings
variant_dataquery, variant_panelcfg_full and variant_panelcfg_only_options
in-scope.
… type (Phase 6b)

Handles ast.KindComposableSlot in the type formatter: dataquery slots
render as Box<dyn variants::Dataquery> with serde deserialize_with
dispatching through a process-wide registry, panelcfg slots as
serde_json::Value (matching Go's any). Adds a Plugins jenny emitting
src/cog/plugins.rs that registers each variant object's decoder closure
(gated by HintSkipVariantPluginRegistration like Go) and exposes
register_default_plugins. The runtime gains Clone/PartialEq for
Box<dyn Dataquery> and an OnceLock default registry. Adds conservative
recursive-Box detection so self-recursive struct fields are wrapped to
stay Sized. Brings the dashboard fixture in-scope; only intersections
remains skipped (matching the python target).
Builder options that fill a dataquery composable slot take
impl cog::Builder<Box<dyn variants::Dataquery>> (Vec for targets),
build the argument and store or push the resulting trait object, reusing
the Phase 4d delegation machinery for error accumulation. Builders for
dataquery-variant types now implement cog::Builder<Box<dyn
variants::Dataquery>> (build returns the boxed trait object), required
because Rust generics are invariant unlike Python duck typing. Adds
Default for Box<dyn Dataquery> for required single slots. Brings
composable_slot, dataquery_variant_builder and panel_builders in-scope;
dashboard_panel stays skipped (Java-generics-specific, as in python).
Adds a ModuleInit jenny emitting the files that turn the generated .rs
modules into a compilable crate: Cargo.toml (configurable crate
name/version, gated by generate_cargo_toml, with serde/serde_json/
serde_repr deps), src/lib.rs (declaring cog/types/builders modules and a
small set of crate-level clippy allows for generated code), and
src/types/mod.rs + src/builders/mod.rs listing one module per package
using the shared package-name sanitization. Type and builder package
sets are derived independently from the context.
Generates the full Grafana Foundation SDK from the real kind-registry
(113 files). Many jenny fixes driven by compiling the generated crate:
adds the PrefixEnumValues pass; module-qualifies cross-package refs to
avoid same-name collisions; Boxes Option-wrapped recursive cycles;
emits duplicate-discriminant enums as type alias + consts; resolves
Some() wrapping from schema field optionality across single/multi-level/
append/known-any paths; delegates builders at deep paths; coerces typed
values into any fields; handles constructor-value and cross-builder
factories, nested envelopes, multi-level append, and nullable-arg
constraint guards. cargo build errors on the generated SDK reduced from
354 to 100; cog golden tests stay green (goldens re-verified, wire
format unchanged).
Drives the generated Grafana Foundation SDK from 100 cargo errors to a
clean cargo build and clippy -D warnings, all via jenny fixes:

- builder.go: resolve hinted destination field types from the generated
  struct (following type aliases and inline-disjunction hoisting) and
  coerce serde_json::Value args into the concrete field type, fixing
  panelcfg/known-any composition; serialize built values into any fields;
  box recursive Vec/Map elements; resolve Some-wrapping and field
  optionality from the generated struct and method signature; build-and-
  insert for map/append delegation; Some-wrap optional factory args.
- rawtypes.go: emit the Dataquery trait impl for disjunction-typed query
  roots; give every untagged enum a manual Default selecting its first
  branch; Some-wrap nullable struct-literal defaults.
- variants.tmpl: deserialize_opt_dataquery for nullable dataquery slots.
- module_init.go: crate-wide clippy allows for lints generated code
  triggers by construction (documented).

The SDK is 113 files / ~128k lines. cog golden tests stay green; the
disjunction fixtures gained additive, wire-faithful Default impls.
Adds a Converter jenny emitting one src/converters/<pkg>.rs per builder
package: each builder gets a function turning an object back into
chained builder-call source, with guards mirroring the Rust optionality
model (is_some/is_empty), values rendered through the shared scalar
formatting, any-valued paths via new cog::dump/dump_json runtime
helpers, and composable slots via cog::convert_dataquery_to_code backed
by an optional converter hook on the variant configs. Covers the same
converter fixtures as the Go target (plus skips matching its skips).

Adds the Rust APIReferenceFormatter (snake_case functions and methods,
builder constructors, compact object definitions) gated by the global
APIReference flag, with registration points mirroring python.
Pins the generated crate as its own workspace root so a vendored copy
inside another repository does not get attached to the enclosing cargo
workspace (which breaks cargo fmt/metadata there). Standalone behavior
is unchanged.
@jamesainslie jamesainslie marked this pull request as ready for review June 10, 2026 09:38
@jamesainslie jamesainslie requested a review from a team as a code owner June 10, 2026 09:38
@jamesainslie jamesainslie requested review from K-Phoen and amalavet June 10, 2026 09:38
@K-Phoen

K-Phoen commented Jun 16, 2026

Copy link
Copy Markdown
Member

Hello!

Thanks for the PR, but while I'd love to have Rust support in cog, my bandwidth (and rust knowledge) are limited and I don't think now is a good time to add Rust.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants