Add Rust language target#1135
Open
jamesainslie wants to merge 24 commits into
Open
Conversation
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.
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 areOption<T>withskip_serializing_if(collections stay bareVec/HashMapwithis_emptyskips, mirroring Go'somitempty); enums use serde renames (numeric enums viaserde_repr); disjunctions become#[serde(untagged)]enums; constant references pin their values throughDefaultimpls; recursive references areBoxed (including cycles throughOption).internal/jennies/rust/builder.go: builder emission. Each object gets a<Name>Builder { internal, errors }with a chaining option API; constraints accumulatecog::BuildErrors surfaced bybuild() -> Result<T, Vec<BuildError>>; delegation takesimpl 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: theBuilder<T>trait,BuildError, and a variant registry (object-safeDataquerytrait objects with discriminator-keyed decoders and a losslessUnknownDataqueryfallback, mirroring the Go runtime'sUnmarshalDataquery).internal/jennies/rust/plugins.go: emitssrc/cog/plugins.rsregistering each variant's decoder.internal/jennies/rust/module_init.go: emitsCargo.toml,src/lib.rsand the module tree.internal/jennies/rust/postprocessor.go: runsrustfmtover generated files (skipped gracefully when unavailable), mirroring the Go target's gofmt postprocessor.Testing
intersectionsandanonymous_structmatching python,dashboard_panelmatching python,builder_delegation_in_disjunctionmatching golang).cargo build,cargo clippy --all-targets -- -D warningsandcargo fmt --check.A follow-up PR to grafana-foundation-sdk would add the
rust:entry to its.cog/config.yamlto start publishing the Rust SDK.