feat: field metadata option + cross-language registry generators (supersedes #905)#952
feat: field metadata option + cross-language registry generators (supersedes #905)#952jamesarich wants to merge 4 commits into
Conversation
|
The latest Buf updates on your PR. Results from workflow pull-request / build (pull_request).
|
13cb74f to
c52ced6
Compare
|
Heads-up on the red |
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>
c52ced6 to
007d7b2
Compare
|
Validated the Swift side from the Meshtastic-Apple end — two findings. 1. The Swift integration assumption holds against real swift-protobuf output (closing the "validated against a stub" open item):
let isDiy: Bool? = Config.PositionConfig.rxGpio.diyOnly // typed accessor ✓
FieldMetadataRegistry.get("meshtastic.Config.PositionConfig", tag: 8) // dynamic lookup ✓
var config = Config.PositionConfig()
config.rxGpio = 7 // real instance property
(config.rxGpio, Config.PositionConfig.rxGpio.diyOnly) // static + instance coexist ✓That last line was the collision case worth worrying about (generated 2. A pure-Swift implementation of the generator produces byte-identical output. As an experiment in keeping the Apple pipeline Go-free, I wrote the same generator (~180 lines) on
Happy to contribute it as |
|
feel free to push changes here @garthvh |
garthvh
left a comment
There was a problem hiding this comment.
Looks like it will work, I have swift tooling
Swift-native sibling of tools/protoc-gen-fieldmeta for consumers whose codegen toolchain is already swift-protobuf based (e.g. Meshtastic-Apple), so adopting field metadata doesn't add Go to their pipeline. Built on SwiftProtobufPluginLibrary (the library protoc-gen-swift itself uses): type paths and property names come from SwiftProtobufNamer / NamingUtils, matching the consumer's real generated code by construction; the option arrives typed via customOptionExtensions. Output is byte-identical to protoc-gen-fieldmeta's target=swift over this repo's schema (verified on swift-protobuf 1.36.1 and 1.38.1), and the generated registry compiles inside Meshtastic-Apple's real MeshtasticProtobufs package with an external consumer typecheck (typed accessor, dynamic lookup, static/instance member coexistence). Includes the plugin's own generated field_metadata.pb.swift binding (regeneration recipe in the README), a cross-link from the Go plugin's Apple section, and a .gitignore entry for the tool's .build/.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Pushed the Swift implementation onto this branch as offered:
Feel free to reshape/drop anything that doesn't fit the PR's direction. |
|
Can we mirror the deprecated field info I can't currently read here? |
Fields marked `[deprecated = true]` carry that flag in FieldOptions, which every protobuf runtime strips — so apps can't read deprecation at runtime, the same gap this registry already closes for custom attributes like diy_only. Mirror the standard `deprecated` option into the registry as a `deprecated` attribute on FieldMetadata, populated by the generators from the field's own `[deprecated = true]` (never set by hand). Every existing and future deprecated field flows in automatically, with no annotation. - field_metadata.proto: add `optional bool deprecated = 6`, documented as generator-managed. It must be a real field (not a synthetic generator attribute) because the KMP registry instantiates the Wire-generated FieldMetadata type. - protoc-gen-fieldmeta (Go): read FieldOptions.deprecated and upsert the attribute; deprecated-only fields now get entries. Emitters stay generic. - protoc-gen-fieldmeta-swift: mirror field.options.deprecated; align entry and accessor ordering to the Go plugin so output stays byte-identical across many message types (previously only trivially identical with one message type). Regenerate the bundled field_metadata.pb.swift. - FieldMetadataRegistryHandler (KMP/Wire): mirror field.isDeprecated. Guard the generators in CI — previously the Go tests never ran there and the Go/Swift byte-identity was hand-checked only: - .github/workflows/field-metadata.yml: run the Go plugin tests + vet, and diff the Go and Swift generators' output to enforce byte-identity. - protoc-gen-fieldmeta-swift/scripts/verify-parity.sh: the reusable parity check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `deprecated` attribute is generator-managed — mirrored from the field's standard `[deprecated = true]` option. A hand-set value inside (meshtastic.field_metadata) previously produced silently divergent output: the Go and Kotlin/Wire generators emitted it while the Swift plugin ignored it (breaking byte-identity), and the schema carried two sources of truth for the same fact. Make it a hard error at generation time in all three generators, with the same message naming the field and the fix. Verified: both protoc plugins reject a hand-set fixture identically; parity over the real schema remains byte-identical; Go tests cover true/false/redundant hand-set cases. Also document the Go plugin's `target=swift` assumption that files set `option swift_prefix = ""` (all meshtastic protos do) — without it, swift-protobuf prefixes generated type names and the emitted extensions would reference nonexistent types; the parity CI catches that divergence. The bundled field_metadata.pb.swift regen picks up the proto comment change (comments only, no functional difference). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 extensibleFieldMetadatamessage carried in a singleFieldOptionsextension, plus the generators that actually expose it to consumers.How it works
meshtastic/field_metadata.proto: a structuredFieldMetadatamessage (diy_only,admin_only,min/max_value,unit) carried in oneextend google.protobuf.FieldOptions { field_metadata = 51001 }.config.proto'srx_gpio/tx_gpioare annotated{ diy_only: true }.google.api.field_behavior).SchemaHandlerinpackages/kmp/buildSrcemits aFieldMetadataRegistryinto the published artifact.tools/protoc-gen-fieldmeta, aprotoc/bufplugin (reads the option dynamically; generic overFieldMetadata's fields).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_gpio→diy_only)Config.PositionConfig.rx_gpio.diy_onlyConfig.PositionConfig.rxGpio.diyOnlyFieldMeta.Config.PositionConfig.rxGpio.diyOnlyfm.Config.PositionConfig.rx_gpio["diy_only"]fm::config::position_config::RX_GPIO.diy_onlymeshtastic_field_metadata_get("meshtastic.Config.PositionConfig", 8)->diy_onlyAdding 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 infield_metadata.proto's header.)Testing performed
buf lint/buf build/buf breakingvsmaster— clean (additive change).tools/protoc-gen-fieldmeta:gofmt/go vet/go testgreen (~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.compileKotlinJvm), C (clang -Wall -Wextra), Python, Swift (swiftc -typecheck), TypeScript (tsc --strict), Rust (rustc -D warnings).buf generateproducesfield_metadata_registry.ts, re-exported frommod.tsasFieldMeta.Draft — open items / for review
tools/protoc-gen-fieldmeta/README.md).FieldMetadatashape (which attributes to standardize), the51001extension 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).packages/kmp buildruns in CI on this PR.🤖 Generated with Claude Code