Skip to content

feat: field metadata option + cross-language registry generators (supersedes #905)#952

Draft
jamesarich wants to merge 1 commit into
masterfrom
jamesarich/field-metadata
Draft

feat: field metadata option + cross-language registry generators (supersedes #905)#952
jamesarich wants to merge 1 commit into
masterfrom
jamesarich/field-metadata

Conversation

@jamesarich

Copy link
Copy Markdown
Contributor

What & why

Some schema fields carry app/UI-relevant metadata — e.g. GPIO pins that only matter on DIY hardware, which apps want to hide on pre-assembled boards. This adds a first-class, structured way to declare that metadata in the schema and consume it from every client, with no reflection and no runtime cost.

Supersedes #905 (@jp-bennett's experiment). That PR proved the idea with a scalar (meshtastic.diy_only) option; this generalizes it into one extensible FieldMetadata message carried in a single FieldOptions extension, plus the generators that actually expose it to consumers.

How it works

  • Source of truthmeshtastic/field_metadata.proto: a structured FieldMetadata message (diy_only, admin_only, min/max_value, unit) carried in one extend google.protobuf.FieldOptions { field_metadata = 51001 }. config.proto's rx_gpio/tx_gpio are annotated { diy_only: true }.
  • Why codegen, not runtime reflection — nanopb (firmware), Wire (KMP), and prost (Rust) all strip options at runtime, so reflection can't serve them. Build-time codegen is the only mechanism that works for every consumer (the same approach Google's SDK generators use for google.api.field_behavior).
  • Two generators, one shape:
    • KMP/Wire — a Wire SchemaHandler in packages/kmp/buildSrc emits a FieldMetadataRegistry into the published artifact.
    • C / Python / TypeScript / Rust / Swifttools/protoc-gen-fieldmeta, a protoc/buf plugin (reads the option dynamically; generic over FieldMetadata's fields).
  • Each target gets a typed accessor (idiomatic per language) plus a dynamic get(type, tag) fallback. Where the language can extend the real generated message type it does (Kotlin companion extension, Swift type extension); TS/Python use a namespace, Rust a module, C a lookup table.

Accessing it (example: rx_gpiodiy_only)

Language Accessor
Kotlin Config.PositionConfig.rx_gpio.diy_only
Swift Config.PositionConfig.rxGpio.diyOnly
TypeScript FieldMeta.Config.PositionConfig.rxGpio.diyOnly
Python fm.Config.PositionConfig.rx_gpio["diy_only"]
Rust fm::config::position_config::RX_GPIO.diy_only
C meshtastic_field_metadata_get("meshtastic.Config.PositionConfig", 8)->diy_only

Adding metadata later

Schema-only: add a scalar field to FieldMetadata (next field number) → every target regenerates automatically. No generator/build changes, no new extension number consumed. Non-scalar attributes are a hard error at generation time. (Documented in field_metadata.proto's header.)

Testing performed

  • buf lint / buf build / buf breaking vs master — clean (additive change).
  • tools/protoc-gen-fieldmeta: gofmt / go vet / go test green (~92% coverage). Tests include an in-memory, buf-faithful end-to-end decode, the scalar-only guard, value rendering (incl. integral floats), string escaping, and multi-message cases.
  • Compiled the generated output for all six targets: KMP (compileKotlinJvm), C (clang -Wall -Wextra), Python, Swift (swiftc -typecheck), TypeScript (tsc --strict), Rust (rustc -D warnings).
  • buf generate produces field_metadata_registry.ts, re-exported from mod.ts as FieldMeta.

Draft — open items / for review

  • Wired and verified for the in-repo TS + KMP packages. The C/Python/Swift/Rust outputs are compile-verified but go live once each consumer repo adds the plugin to its codegen (recipes in tools/protoc-gen-fieldmeta/README.md).
  • @jp-bennett — keen on your review since this builds on Experminental PR for adding metadata to protobufs #905, especially: the FieldMetadata shape (which attributes to standardize), the 51001 extension number (keep it in the 50000–99999 in-house range, or request a global-registry number for a public schema?), and the Swift integration assumptions (validated against a stub, not yet real swift-protobuf output).
  • The full multi-target packages/kmp build runs in CI on this PR.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow pull-request / build (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedJun 17, 2026, 11:48 PM

@jamesarich

Copy link
Copy Markdown
Contributor Author

Heads-up on the red build check: it's failing only on buf format for meshtastic/powermon.proto — unrelated to this PR. buf-action runs the latest buf (now 1.71.0), which reformats that file's block comments; master's last green run predated 1.71.0, so it's repo-wide drift. This PR's own files (config.proto, field_metadata.proto) are format-clean. Fixed separately in #953 — once that merges, this PR's check clears on a rebase onto master. Keeping this draft focused on the field-metadata design for review. /cc @jp-bennett

Introduces a structured (meshtastic.field_metadata) custom field option and build-time generators that expose it as reflection-free static accessors in every consumer language: KMP/Wire (Wire SchemaHandler in packages/kmp/buildSrc) and C/Python/TypeScript/Rust/Swift (the tools/protoc-gen-fieldmeta plugin). The TS target is wired into buf.gen.yaml and re-exported from mod.ts as FieldMeta.

Generalizes the #905 experiment: one extensible FieldMetadata message carried in a single FieldOptions extension, so adding an attribute is a schema-only change (scalar-only, enforced at generation time) with no new extension number and no generator changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich jamesarich force-pushed the jamesarich/field-metadata branch from c52ced6 to 007d7b2 Compare June 17, 2026 23:36
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.

1 participant