From ab214a39023a13ef4c1e534589bdc0d20ff59ce4 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sat, 16 May 2026 22:28:13 +0300 Subject: [PATCH] codegen: emit strict _fields / _oneofs constants for lazy view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lazy-view callers passing a typo'd field name to :get / :has / :set / :clear / :which got `nil` back, indistinguishable from a legitimately- absent optional field. Failures surfaced as missing data downstream. Each generated message now exports a M._fields table mapping each field name to itself (and M._oneofs for oneof groups), wrapped by a new pb.field_names() helper that errors on unknown-key reads and on any write. Routing field-name arguments through these tables turns a typo into a load-time error at the read site. Eager _encode / _decode keep round-tripping plain Lua tables — the constants table is a lazy-view contract, documented in docs/api-modes.md. README and lazy_test.lua converted to the new pattern; three new tests cover typo / read-only / oneof-typo errors. --- README.md | 7 + cmd/protoc-gen-tarantool/internal/gen/gen.go | 46 +++++ docs/api-modes.md | 61 +++++-- .../full/conformance/conformance_pb.lua | 39 +++++ examples/expected/full/hello/hello_pb.lua | 51 ++++++ .../proto3/test_messages_proto3_pb.lua | 165 ++++++++++++++++++ .../runtime/conformance/conformance_pb.lua | 39 +++++ examples/expected/runtime/hello/hello_pb.lua | 51 ++++++ .../proto3/test_messages_proto3_pb.lua | 165 ++++++++++++++++++ runtime/pb/init.lua | 17 ++ test/lazy_test.lua | 142 +++++++++------ 11 files changed, 718 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index e35fba149e4a68007d1565816e50dba46b688d4c..89136c13cb37820a5cbf2cd4b75cffdb121303ca 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,15 @@ M.Foo_decode_lazy(b) -- wire bytes -> MessageView (zero-copy view) M.Foo_text(t, opts) -- table -> protoc-style text format (debug printer) M.Foo_has_(t) -- only emitted for proto3 explicit-`optional` fields M.Foo_clear_(t) -- same +M.Foo_fields -- strict {field = "field", ...} for lazy-view callers +M.Foo_oneofs -- strict {oneof_group = "oneof_group", ...}, when any ``` +Lazy-view field-name arguments should be routed through `M.Foo_fields` +/ `M.Foo_oneofs` rather than passed as string literals — typos error at +the read site instead of silently returning `nil`. See +[docs/api-modes.md](docs/api-modes.md#field-name-constants--required). + Text-format **decoding** is exposed on the runtime as `pb.text.decode(desc, text, opts)` (no per-message wrapper — it's used from a few places, like the conformance runner, and didn't warrant codegen surface). diff --git a/cmd/protoc-gen-tarantool/internal/gen/gen.go b/cmd/protoc-gen-tarantool/internal/gen/gen.go index 2e65a11ee6527cedcfb2ee25ab8ff3f43852c1a4..39e6880cebdffb6fa42f976682d692e302e1b6f6 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/gen.go +++ b/cmd/protoc-gen-tarantool/internal/gen/gen.go @@ -240,9 +240,55 @@ func emitMessageFields(w *writer, file *protogen.File, m *protogen.Message, impo emitOneofTable(w, name, m) emitReservedNames(w, name, m) w.line("pb.finalize_message(M.%s_descriptor)", name) + emitFieldNamesTable(w, name, m) + emitOneofNamesTable(w, name, m) w.line("") } +// emitFieldNamesTable emits a strict, typo-checked field-name constants +// table per message: +// +// M._fields = pb.field_names({ +// foo = "foo", +// bar = "bar", +// }) +// +// Callers of the lazy view (`view:get(F.foo)`) get a load-time error on +// typos instead of the silent `nil` that a raw `view:get('fooo')` would +// return. See docs/api-modes.md for the documented contract. +func emitFieldNamesTable(w *writer, name string, m *protogen.Message) { + if len(m.Fields) == 0 { + return + } + w.line("M.%s_fields = pb.field_names({", name) + for _, f := range m.Fields { + fn := string(f.Desc.Name()) + w.line(" %s = %q,", fn, fn) + } + w.line("})") +} + +// emitOneofNamesTable emits a strict, typo-checked oneof-name constants +// table per message that declares non-synthetic oneofs. Symmetric to +// emitFieldNamesTable; used by `view:which(O.outcome)` etc. +func emitOneofNamesTable(w *writer, name string, m *protogen.Message) { + var names []string + for _, oo := range m.Oneofs { + if oo.Fields[0].Desc.HasOptionalKeyword() { + continue + } + names = append(names, string(oo.Desc.Name())) + } + if len(names) == 0 { + return + } + w.line("M.%s_oneofs = pb.field_names({", name) + for _, on := range names { + w.line(" %s = %q,", on, on) + } + w.line("})") +} + // emitOneofTable emits `M._descriptor.oneofs = { = {...members...} }` // when the message has any non-synthetic oneofs. func emitOneofTable(w *writer, name string, m *protogen.Message) { diff --git a/docs/api-modes.md b/docs/api-modes.md index 2984736685ba0716eddb3b226eebaba6dc8efa75..d6937ff06ad0ab94934f4f9adae7efccd190ad77 100644 --- a/docs/api-modes.md +++ b/docs/api-modes.md @@ -151,42 +151,81 @@ materialization happens on `:get`, repeated/map fields become ```lua local view = hello.Person_decode_lazy(bytes) +local F = hello.Person_fields -- field-name constants +local AF = hello.Address_fields +local R = hello.Result_oneofs -- oneof-group constants -- Direct field reads (decoded on demand, then cached on the view). -local name = view:get('name') -local uid = view:get('user_id') +local name = view:get(F.name) +local uid = view:get(F.user_id) -- Repeated field: ArrayView. -local emails = view:get('emails') -- ArrayView, no per-element table +local emails = view:get(F.emails) -- ArrayView, no per-element table for i, e in emails:iter() do print(i, e) end print('count:', emails:len()) --- Map field: MapView. -local ages = view:get('ages_by_nickname') +-- Map field: MapView. Field name uses the constants table; the map +-- key passed to MapView:get is a raw value, not a field name. +local ages = view:get(F.ages_by_nickname) print(ages:get('alice')) --- Sub-message: a nested MessageView. -local addr = view:get('address') -print(addr:get('city')) +-- Sub-message: a nested MessageView. Switch to the sub-message's +-- own constants table for its fields. +local addr = view:get(F.address) +print(addr:get(AF.city)) -- Presence + oneof which-branch checks without forcing a decode. -view:has('user_id') -- bool -view:which('outcome') -- string? +view:has(F.user_id) -- bool +view:which(R.outcome) -- string? -- Iterate fields actually present on the wire (skip-aware). for name, val in view:iter() do print(name, val) end -- Mutate, then re-encode. Untouched fields are spliced byte-for-byte -- from the original payload; only dirty fields run through encode. -view:set('user_id', require('ffi').cast('uint64_t', 99)) +view:set(F.user_id, require('ffi').cast('uint64_t', 99)) local new_bytes = view:encode() -- Force a fully-materialized table when you actually need one. local t = view:totable() ``` +### Field-name constants — required + +Every generated message exports a `M._fields` table mapping each +field name to itself, and (when applicable) `M._oneofs` for +oneof group names. Both tables are frozen behind a strict `__index` +that errors on unknown keys and a read-only `__newindex`. + +The lazy view API (`:get` / `:has` / `:set` / `:clear` / `:which`) +takes a field name string — route every field-name argument through +the constants table rather than passing a string literal: + +```lua +-- right +view:get(hello.Person_fields.user_id) +-- typo here errors at the read site: +-- "unknown field name: 'user_di'" + +-- wrong (silently returns nil; indistinguishable from absent optional) +view:get('user_di') +``` + +Why required: `view:get('user_di')` returns `nil` whether the field +name is wrong *or* the field was legitimately absent on the wire. The +ambiguity tends to surface as missing data far downstream. The +constants table catches the typo where it was written. + +This is a lazy-view contract. The eager `_decode` / `_encode` path +round-trips plain Lua tables whose keys are written by the caller's +own code, so the same safety net doesn't apply there — eager users +already see misspelled keys as direct test failures. + +Map keys (`MapView:get(k)`, `MapView:has(k)`) and array indices +(`ArrayView:at(i)`) are values, not field names — pass them raw. + ### When lazy wins - **Sparse reads** — payload is large, you only need a few fields. diff --git a/examples/expected/full/conformance/conformance_pb.lua b/examples/expected/full/conformance/conformance_pb.lua index 82b1299bf4415dba1c929c79f98176670639ad2b..8b7cf69c991fe6e25932b43b39c9fea6721432ed 100644 --- a/examples/expected/full/conformance/conformance_pb.lua +++ b/examples/expected/full/conformance/conformance_pb.lua @@ -52,12 +52,20 @@ M.TestStatus_descriptor.fields = { {name="matched_name", id=3, kind='scalar', proto_type="string"}, } pb.finalize_message(M.TestStatus_descriptor) +M.TestStatus_fields = pb.field_names({ + name = "name", + failure_message = "failure_message", + matched_name = "matched_name", +}) -- Message: conformance.FailureSet M.FailureSet_descriptor.fields = { {name="test", id=2, kind='message', message=M.TestStatus_descriptor, repeated=true}, } pb.finalize_message(M.FailureSet_descriptor) +M.FailureSet_fields = pb.field_names({ + test = "test", +}) -- Message: conformance.ConformanceRequest M.ConformanceRequest_descriptor.fields = { @@ -75,6 +83,20 @@ M.ConformanceRequest_descriptor.oneofs = { payload = {"protobuf_payload", "json_payload", "jspb_payload", "text_payload"}, } pb.finalize_message(M.ConformanceRequest_descriptor) +M.ConformanceRequest_fields = pb.field_names({ + protobuf_payload = "protobuf_payload", + json_payload = "json_payload", + jspb_payload = "jspb_payload", + text_payload = "text_payload", + requested_output_format = "requested_output_format", + message_type = "message_type", + test_category = "test_category", + jspb_encoding_options = "jspb_encoding_options", + print_unknown_fields = "print_unknown_fields", +}) +M.ConformanceRequest_oneofs = pb.field_names({ + payload = "payload", +}) -- Message: conformance.ConformanceResponse M.ConformanceResponse_descriptor.fields = { @@ -92,12 +114,29 @@ M.ConformanceResponse_descriptor.oneofs = { result = {"parse_error", "serialize_error", "timeout_error", "runtime_error", "protobuf_payload", "json_payload", "skipped", "jspb_payload", "text_payload"}, } pb.finalize_message(M.ConformanceResponse_descriptor) +M.ConformanceResponse_fields = pb.field_names({ + parse_error = "parse_error", + serialize_error = "serialize_error", + timeout_error = "timeout_error", + runtime_error = "runtime_error", + protobuf_payload = "protobuf_payload", + json_payload = "json_payload", + skipped = "skipped", + jspb_payload = "jspb_payload", + text_payload = "text_payload", +}) +M.ConformanceResponse_oneofs = pb.field_names({ + result = "result", +}) -- Message: conformance.JspbEncodingConfig M.JspbEncodingConfig_descriptor.fields = { {name="use_jspb_array_any_format", id=1, kind='scalar', proto_type="bool"}, } pb.finalize_message(M.JspbEncodingConfig_descriptor) +M.JspbEncodingConfig_fields = pb.field_names({ + use_jspb_array_any_format = "use_jspb_array_any_format", +}) -- EmmyLua / lua-language-server type annotations. -- These are comments — no runtime effect. They give editors diff --git a/examples/expected/full/hello/hello_pb.lua b/examples/expected/full/hello/hello_pb.lua index 9ad63278ef5604a01450b1c322d3e3339b75fff3..84754212a555690559dcb271fe91dadbe41c2394 100644 --- a/examples/expected/full/hello/hello_pb.lua +++ b/examples/expected/full/hello/hello_pb.lua @@ -35,18 +35,33 @@ M.Result_descriptor.oneofs = { outcome = {"text", "code", "details"}, } pb.finalize_message(M.Result_descriptor) +M.Result_fields = pb.field_names({ + id = "id", + text = "text", + code = "code", + details = "details", +}) +M.Result_oneofs = pb.field_names({ + outcome = "outcome", +}) -- Message: hello.HelloRequest M.HelloRequest_descriptor.fields = { {name="name", id=1, kind='scalar', proto_type="string"}, } pb.finalize_message(M.HelloRequest_descriptor) +M.HelloRequest_fields = pb.field_names({ + name = "name", +}) -- Message: hello.HelloReply M.HelloReply_descriptor.fields = { {name="greeting", id=1, kind='scalar', proto_type="string"}, } pb.finalize_message(M.HelloReply_descriptor) +M.HelloReply_fields = pb.field_names({ + greeting = "greeting", +}) -- Message: hello.Event M.Event_descriptor.fields = { @@ -64,6 +79,20 @@ M.Event_descriptor.fields = { {name="update_mask", id=12, kind='message', message=pb.wkt.FieldMask_descriptor}, } pb.finalize_message(M.Event_descriptor) +M.Event_fields = pb.field_names({ + title = "title", + created_at = "created_at", + duration = "duration", + ack = "ack", + retry_count = "retry_count", + note = "note", + is_admin = "is_admin", + payload = "payload", + attribute = "attribute", + tags = "tags", + extension = "extension", + update_mask = "update_mask", +}) -- Message: hello.Address M.Address_descriptor.fields = { @@ -73,6 +102,12 @@ M.Address_descriptor.fields = { {name="apartment", id=4, kind='scalar', proto_type="string", optional=true}, } pb.finalize_message(M.Address_descriptor) +M.Address_fields = pb.field_names({ + street = "street", + city = "city", + zip = "zip", + apartment = "apartment", +}) -- Message: hello.Person M.Person_descriptor.fields = { @@ -92,6 +127,22 @@ M.Person_descriptor.fields = { {name="addresses_by_label", id=15, kind='map', key={kind='scalar', proto_type="string"}, value={kind='message', message=M.Address_descriptor}}, } pb.finalize_message(M.Person_descriptor) +M.Person_fields = pb.field_names({ + name = "name", + age = "age", + emails = "emails", + status = "status", + address = "address", + friends = "friends", + lucky_numbers = "lucky_numbers", + avatar = "avatar", + user_id = "user_id", + balance = "balance", + weight_kg = "weight_kg", + ages_by_nickname = "ages_by_nickname", + nickname_by_age = "nickname_by_age", + addresses_by_label = "addresses_by_label", +}) -- EmmyLua / lua-language-server type annotations. -- These are comments — no runtime effect. They give editors diff --git a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua index 6533d682534de5e9ef975c88b6e7863352c370c8..32a53e840346076d5660e9adb327cf7d2d64ee96 100644 --- a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +++ b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua @@ -213,6 +213,164 @@ M.TestAllTypesProto3_descriptor.reserved_names = { ["reserved_field"] = true, } pb.finalize_message(M.TestAllTypesProto3_descriptor) +M.TestAllTypesProto3_fields = pb.field_names({ + optional_int32 = "optional_int32", + optional_int64 = "optional_int64", + optional_uint32 = "optional_uint32", + optional_uint64 = "optional_uint64", + optional_sint32 = "optional_sint32", + optional_sint64 = "optional_sint64", + optional_fixed32 = "optional_fixed32", + optional_fixed64 = "optional_fixed64", + optional_sfixed32 = "optional_sfixed32", + optional_sfixed64 = "optional_sfixed64", + optional_float = "optional_float", + optional_double = "optional_double", + optional_bool = "optional_bool", + optional_string = "optional_string", + optional_bytes = "optional_bytes", + optional_nested_message = "optional_nested_message", + optional_foreign_message = "optional_foreign_message", + optional_nested_enum = "optional_nested_enum", + optional_foreign_enum = "optional_foreign_enum", + optional_aliased_enum = "optional_aliased_enum", + optional_string_piece = "optional_string_piece", + optional_cord = "optional_cord", + recursive_message = "recursive_message", + repeated_int32 = "repeated_int32", + repeated_int64 = "repeated_int64", + repeated_uint32 = "repeated_uint32", + repeated_uint64 = "repeated_uint64", + repeated_sint32 = "repeated_sint32", + repeated_sint64 = "repeated_sint64", + repeated_fixed32 = "repeated_fixed32", + repeated_fixed64 = "repeated_fixed64", + repeated_sfixed32 = "repeated_sfixed32", + repeated_sfixed64 = "repeated_sfixed64", + repeated_float = "repeated_float", + repeated_double = "repeated_double", + repeated_bool = "repeated_bool", + repeated_string = "repeated_string", + repeated_bytes = "repeated_bytes", + repeated_nested_message = "repeated_nested_message", + repeated_foreign_message = "repeated_foreign_message", + repeated_nested_enum = "repeated_nested_enum", + repeated_foreign_enum = "repeated_foreign_enum", + repeated_string_piece = "repeated_string_piece", + repeated_cord = "repeated_cord", + packed_int32 = "packed_int32", + packed_int64 = "packed_int64", + packed_uint32 = "packed_uint32", + packed_uint64 = "packed_uint64", + packed_sint32 = "packed_sint32", + packed_sint64 = "packed_sint64", + packed_fixed32 = "packed_fixed32", + packed_fixed64 = "packed_fixed64", + packed_sfixed32 = "packed_sfixed32", + packed_sfixed64 = "packed_sfixed64", + packed_float = "packed_float", + packed_double = "packed_double", + packed_bool = "packed_bool", + packed_nested_enum = "packed_nested_enum", + unpacked_int32 = "unpacked_int32", + unpacked_int64 = "unpacked_int64", + unpacked_uint32 = "unpacked_uint32", + unpacked_uint64 = "unpacked_uint64", + unpacked_sint32 = "unpacked_sint32", + unpacked_sint64 = "unpacked_sint64", + unpacked_fixed32 = "unpacked_fixed32", + unpacked_fixed64 = "unpacked_fixed64", + unpacked_sfixed32 = "unpacked_sfixed32", + unpacked_sfixed64 = "unpacked_sfixed64", + unpacked_float = "unpacked_float", + unpacked_double = "unpacked_double", + unpacked_bool = "unpacked_bool", + unpacked_nested_enum = "unpacked_nested_enum", + map_int32_int32 = "map_int32_int32", + map_int64_int64 = "map_int64_int64", + map_uint32_uint32 = "map_uint32_uint32", + map_uint64_uint64 = "map_uint64_uint64", + map_sint32_sint32 = "map_sint32_sint32", + map_sint64_sint64 = "map_sint64_sint64", + map_fixed32_fixed32 = "map_fixed32_fixed32", + map_fixed64_fixed64 = "map_fixed64_fixed64", + map_sfixed32_sfixed32 = "map_sfixed32_sfixed32", + map_sfixed64_sfixed64 = "map_sfixed64_sfixed64", + map_int32_float = "map_int32_float", + map_int32_double = "map_int32_double", + map_bool_bool = "map_bool_bool", + map_string_string = "map_string_string", + map_string_bytes = "map_string_bytes", + map_string_nested_message = "map_string_nested_message", + map_string_foreign_message = "map_string_foreign_message", + map_string_nested_enum = "map_string_nested_enum", + map_string_foreign_enum = "map_string_foreign_enum", + oneof_uint32 = "oneof_uint32", + oneof_nested_message = "oneof_nested_message", + oneof_string = "oneof_string", + oneof_bytes = "oneof_bytes", + oneof_bool = "oneof_bool", + oneof_uint64 = "oneof_uint64", + oneof_float = "oneof_float", + oneof_double = "oneof_double", + oneof_enum = "oneof_enum", + oneof_null_value = "oneof_null_value", + optional_bool_wrapper = "optional_bool_wrapper", + optional_int32_wrapper = "optional_int32_wrapper", + optional_int64_wrapper = "optional_int64_wrapper", + optional_uint32_wrapper = "optional_uint32_wrapper", + optional_uint64_wrapper = "optional_uint64_wrapper", + optional_float_wrapper = "optional_float_wrapper", + optional_double_wrapper = "optional_double_wrapper", + optional_string_wrapper = "optional_string_wrapper", + optional_bytes_wrapper = "optional_bytes_wrapper", + repeated_bool_wrapper = "repeated_bool_wrapper", + repeated_int32_wrapper = "repeated_int32_wrapper", + repeated_int64_wrapper = "repeated_int64_wrapper", + repeated_uint32_wrapper = "repeated_uint32_wrapper", + repeated_uint64_wrapper = "repeated_uint64_wrapper", + repeated_float_wrapper = "repeated_float_wrapper", + repeated_double_wrapper = "repeated_double_wrapper", + repeated_string_wrapper = "repeated_string_wrapper", + repeated_bytes_wrapper = "repeated_bytes_wrapper", + optional_duration = "optional_duration", + optional_timestamp = "optional_timestamp", + optional_field_mask = "optional_field_mask", + optional_struct = "optional_struct", + optional_any = "optional_any", + optional_value = "optional_value", + optional_null_value = "optional_null_value", + optional_empty = "optional_empty", + repeated_duration = "repeated_duration", + repeated_timestamp = "repeated_timestamp", + repeated_fieldmask = "repeated_fieldmask", + repeated_struct = "repeated_struct", + repeated_any = "repeated_any", + repeated_value = "repeated_value", + repeated_list_value = "repeated_list_value", + repeated_empty = "repeated_empty", + fieldname1 = "fieldname1", + field_name2 = "field_name2", + _field_name3 = "_field_name3", + field__name4_ = "field__name4_", + field0name5 = "field0name5", + field_0_name6 = "field_0_name6", + fieldName7 = "fieldName7", + FieldName8 = "FieldName8", + field_Name9 = "field_Name9", + Field_Name10 = "Field_Name10", + FIELD_NAME11 = "FIELD_NAME11", + FIELD_name12 = "FIELD_name12", + __field_name13 = "__field_name13", + __Field_name14 = "__Field_name14", + field__name15 = "field__name15", + field__Name16 = "field__Name16", + field_name17__ = "field_name17__", + Field_name18__ = "Field_name18__", +}) +M.TestAllTypesProto3_oneofs = pb.field_names({ + oneof_field = "oneof_field", +}) -- Message: protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage M.TestAllTypesProto3_NestedMessage_descriptor.fields = { @@ -220,12 +378,19 @@ M.TestAllTypesProto3_NestedMessage_descriptor.fields = { {name="corecursive", id=2, kind='message', message=M.TestAllTypesProto3_descriptor}, } pb.finalize_message(M.TestAllTypesProto3_NestedMessage_descriptor) +M.TestAllTypesProto3_NestedMessage_fields = pb.field_names({ + a = "a", + corecursive = "corecursive", +}) -- Message: protobuf_test_messages.proto3.ForeignMessage M.ForeignMessage_descriptor.fields = { {name="c", id=1, kind='scalar', proto_type="int32"}, } pb.finalize_message(M.ForeignMessage_descriptor) +M.ForeignMessage_fields = pb.field_names({ + c = "c", +}) -- Message: protobuf_test_messages.proto3.NullHypothesisProto3 M.NullHypothesisProto3_descriptor.fields = { diff --git a/examples/expected/runtime/conformance/conformance_pb.lua b/examples/expected/runtime/conformance/conformance_pb.lua index 52cd1ec98c91d926ea6358f1484f1d12e4d1e5c5..1ce7866ab5ebe12e6c9d11b4e5808ea85e6bf66d 100644 --- a/examples/expected/runtime/conformance/conformance_pb.lua +++ b/examples/expected/runtime/conformance/conformance_pb.lua @@ -52,12 +52,20 @@ M.TestStatus_descriptor.fields = { {name="matched_name", id=3, kind='scalar', proto_type="string"}, } pb.finalize_message(M.TestStatus_descriptor) +M.TestStatus_fields = pb.field_names({ + name = "name", + failure_message = "failure_message", + matched_name = "matched_name", +}) -- Message: conformance.FailureSet M.FailureSet_descriptor.fields = { {name="test", id=2, kind='message', message=M.TestStatus_descriptor, repeated=true}, } pb.finalize_message(M.FailureSet_descriptor) +M.FailureSet_fields = pb.field_names({ + test = "test", +}) -- Message: conformance.ConformanceRequest M.ConformanceRequest_descriptor.fields = { @@ -75,6 +83,20 @@ M.ConformanceRequest_descriptor.oneofs = { payload = {"protobuf_payload", "json_payload", "jspb_payload", "text_payload"}, } pb.finalize_message(M.ConformanceRequest_descriptor) +M.ConformanceRequest_fields = pb.field_names({ + protobuf_payload = "protobuf_payload", + json_payload = "json_payload", + jspb_payload = "jspb_payload", + text_payload = "text_payload", + requested_output_format = "requested_output_format", + message_type = "message_type", + test_category = "test_category", + jspb_encoding_options = "jspb_encoding_options", + print_unknown_fields = "print_unknown_fields", +}) +M.ConformanceRequest_oneofs = pb.field_names({ + payload = "payload", +}) -- Message: conformance.ConformanceResponse M.ConformanceResponse_descriptor.fields = { @@ -92,12 +114,29 @@ M.ConformanceResponse_descriptor.oneofs = { result = {"parse_error", "serialize_error", "timeout_error", "runtime_error", "protobuf_payload", "json_payload", "skipped", "jspb_payload", "text_payload"}, } pb.finalize_message(M.ConformanceResponse_descriptor) +M.ConformanceResponse_fields = pb.field_names({ + parse_error = "parse_error", + serialize_error = "serialize_error", + timeout_error = "timeout_error", + runtime_error = "runtime_error", + protobuf_payload = "protobuf_payload", + json_payload = "json_payload", + skipped = "skipped", + jspb_payload = "jspb_payload", + text_payload = "text_payload", +}) +M.ConformanceResponse_oneofs = pb.field_names({ + result = "result", +}) -- Message: conformance.JspbEncodingConfig M.JspbEncodingConfig_descriptor.fields = { {name="use_jspb_array_any_format", id=1, kind='scalar', proto_type="bool"}, } pb.finalize_message(M.JspbEncodingConfig_descriptor) +M.JspbEncodingConfig_fields = pb.field_names({ + use_jspb_array_any_format = "use_jspb_array_any_format", +}) -- EmmyLua / lua-language-server type annotations. -- These are comments — no runtime effect. They give editors diff --git a/examples/expected/runtime/hello/hello_pb.lua b/examples/expected/runtime/hello/hello_pb.lua index 6f9cda5b474ff7c78d7e69f94a59522f298d23e6..e43741f11f800cb4247216d6e47e1e7eecaabc90 100644 --- a/examples/expected/runtime/hello/hello_pb.lua +++ b/examples/expected/runtime/hello/hello_pb.lua @@ -35,18 +35,33 @@ M.Result_descriptor.oneofs = { outcome = {"text", "code", "details"}, } pb.finalize_message(M.Result_descriptor) +M.Result_fields = pb.field_names({ + id = "id", + text = "text", + code = "code", + details = "details", +}) +M.Result_oneofs = pb.field_names({ + outcome = "outcome", +}) -- Message: hello.HelloRequest M.HelloRequest_descriptor.fields = { {name="name", id=1, kind='scalar', proto_type="string"}, } pb.finalize_message(M.HelloRequest_descriptor) +M.HelloRequest_fields = pb.field_names({ + name = "name", +}) -- Message: hello.HelloReply M.HelloReply_descriptor.fields = { {name="greeting", id=1, kind='scalar', proto_type="string"}, } pb.finalize_message(M.HelloReply_descriptor) +M.HelloReply_fields = pb.field_names({ + greeting = "greeting", +}) -- Message: hello.Event M.Event_descriptor.fields = { @@ -64,6 +79,20 @@ M.Event_descriptor.fields = { {name="update_mask", id=12, kind='message', message=pb.wkt.FieldMask_descriptor}, } pb.finalize_message(M.Event_descriptor) +M.Event_fields = pb.field_names({ + title = "title", + created_at = "created_at", + duration = "duration", + ack = "ack", + retry_count = "retry_count", + note = "note", + is_admin = "is_admin", + payload = "payload", + attribute = "attribute", + tags = "tags", + extension = "extension", + update_mask = "update_mask", +}) -- Message: hello.Address M.Address_descriptor.fields = { @@ -73,6 +102,12 @@ M.Address_descriptor.fields = { {name="apartment", id=4, kind='scalar', proto_type="string", optional=true}, } pb.finalize_message(M.Address_descriptor) +M.Address_fields = pb.field_names({ + street = "street", + city = "city", + zip = "zip", + apartment = "apartment", +}) -- Message: hello.Person M.Person_descriptor.fields = { @@ -92,6 +127,22 @@ M.Person_descriptor.fields = { {name="addresses_by_label", id=15, kind='map', key={kind='scalar', proto_type="string"}, value={kind='message', message=M.Address_descriptor}}, } pb.finalize_message(M.Person_descriptor) +M.Person_fields = pb.field_names({ + name = "name", + age = "age", + emails = "emails", + status = "status", + address = "address", + friends = "friends", + lucky_numbers = "lucky_numbers", + avatar = "avatar", + user_id = "user_id", + balance = "balance", + weight_kg = "weight_kg", + ages_by_nickname = "ages_by_nickname", + nickname_by_age = "nickname_by_age", + addresses_by_label = "addresses_by_label", +}) -- EmmyLua / lua-language-server type annotations. -- These are comments — no runtime effect. They give editors diff --git a/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua b/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua index 124f73970230453745010d49f9945ba9827ee4e9..04ba37a0dc2ae44be867c22f05111d01693aeb91 100644 --- a/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +++ b/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua @@ -213,6 +213,164 @@ M.TestAllTypesProto3_descriptor.reserved_names = { ["reserved_field"] = true, } pb.finalize_message(M.TestAllTypesProto3_descriptor) +M.TestAllTypesProto3_fields = pb.field_names({ + optional_int32 = "optional_int32", + optional_int64 = "optional_int64", + optional_uint32 = "optional_uint32", + optional_uint64 = "optional_uint64", + optional_sint32 = "optional_sint32", + optional_sint64 = "optional_sint64", + optional_fixed32 = "optional_fixed32", + optional_fixed64 = "optional_fixed64", + optional_sfixed32 = "optional_sfixed32", + optional_sfixed64 = "optional_sfixed64", + optional_float = "optional_float", + optional_double = "optional_double", + optional_bool = "optional_bool", + optional_string = "optional_string", + optional_bytes = "optional_bytes", + optional_nested_message = "optional_nested_message", + optional_foreign_message = "optional_foreign_message", + optional_nested_enum = "optional_nested_enum", + optional_foreign_enum = "optional_foreign_enum", + optional_aliased_enum = "optional_aliased_enum", + optional_string_piece = "optional_string_piece", + optional_cord = "optional_cord", + recursive_message = "recursive_message", + repeated_int32 = "repeated_int32", + repeated_int64 = "repeated_int64", + repeated_uint32 = "repeated_uint32", + repeated_uint64 = "repeated_uint64", + repeated_sint32 = "repeated_sint32", + repeated_sint64 = "repeated_sint64", + repeated_fixed32 = "repeated_fixed32", + repeated_fixed64 = "repeated_fixed64", + repeated_sfixed32 = "repeated_sfixed32", + repeated_sfixed64 = "repeated_sfixed64", + repeated_float = "repeated_float", + repeated_double = "repeated_double", + repeated_bool = "repeated_bool", + repeated_string = "repeated_string", + repeated_bytes = "repeated_bytes", + repeated_nested_message = "repeated_nested_message", + repeated_foreign_message = "repeated_foreign_message", + repeated_nested_enum = "repeated_nested_enum", + repeated_foreign_enum = "repeated_foreign_enum", + repeated_string_piece = "repeated_string_piece", + repeated_cord = "repeated_cord", + packed_int32 = "packed_int32", + packed_int64 = "packed_int64", + packed_uint32 = "packed_uint32", + packed_uint64 = "packed_uint64", + packed_sint32 = "packed_sint32", + packed_sint64 = "packed_sint64", + packed_fixed32 = "packed_fixed32", + packed_fixed64 = "packed_fixed64", + packed_sfixed32 = "packed_sfixed32", + packed_sfixed64 = "packed_sfixed64", + packed_float = "packed_float", + packed_double = "packed_double", + packed_bool = "packed_bool", + packed_nested_enum = "packed_nested_enum", + unpacked_int32 = "unpacked_int32", + unpacked_int64 = "unpacked_int64", + unpacked_uint32 = "unpacked_uint32", + unpacked_uint64 = "unpacked_uint64", + unpacked_sint32 = "unpacked_sint32", + unpacked_sint64 = "unpacked_sint64", + unpacked_fixed32 = "unpacked_fixed32", + unpacked_fixed64 = "unpacked_fixed64", + unpacked_sfixed32 = "unpacked_sfixed32", + unpacked_sfixed64 = "unpacked_sfixed64", + unpacked_float = "unpacked_float", + unpacked_double = "unpacked_double", + unpacked_bool = "unpacked_bool", + unpacked_nested_enum = "unpacked_nested_enum", + map_int32_int32 = "map_int32_int32", + map_int64_int64 = "map_int64_int64", + map_uint32_uint32 = "map_uint32_uint32", + map_uint64_uint64 = "map_uint64_uint64", + map_sint32_sint32 = "map_sint32_sint32", + map_sint64_sint64 = "map_sint64_sint64", + map_fixed32_fixed32 = "map_fixed32_fixed32", + map_fixed64_fixed64 = "map_fixed64_fixed64", + map_sfixed32_sfixed32 = "map_sfixed32_sfixed32", + map_sfixed64_sfixed64 = "map_sfixed64_sfixed64", + map_int32_float = "map_int32_float", + map_int32_double = "map_int32_double", + map_bool_bool = "map_bool_bool", + map_string_string = "map_string_string", + map_string_bytes = "map_string_bytes", + map_string_nested_message = "map_string_nested_message", + map_string_foreign_message = "map_string_foreign_message", + map_string_nested_enum = "map_string_nested_enum", + map_string_foreign_enum = "map_string_foreign_enum", + oneof_uint32 = "oneof_uint32", + oneof_nested_message = "oneof_nested_message", + oneof_string = "oneof_string", + oneof_bytes = "oneof_bytes", + oneof_bool = "oneof_bool", + oneof_uint64 = "oneof_uint64", + oneof_float = "oneof_float", + oneof_double = "oneof_double", + oneof_enum = "oneof_enum", + oneof_null_value = "oneof_null_value", + optional_bool_wrapper = "optional_bool_wrapper", + optional_int32_wrapper = "optional_int32_wrapper", + optional_int64_wrapper = "optional_int64_wrapper", + optional_uint32_wrapper = "optional_uint32_wrapper", + optional_uint64_wrapper = "optional_uint64_wrapper", + optional_float_wrapper = "optional_float_wrapper", + optional_double_wrapper = "optional_double_wrapper", + optional_string_wrapper = "optional_string_wrapper", + optional_bytes_wrapper = "optional_bytes_wrapper", + repeated_bool_wrapper = "repeated_bool_wrapper", + repeated_int32_wrapper = "repeated_int32_wrapper", + repeated_int64_wrapper = "repeated_int64_wrapper", + repeated_uint32_wrapper = "repeated_uint32_wrapper", + repeated_uint64_wrapper = "repeated_uint64_wrapper", + repeated_float_wrapper = "repeated_float_wrapper", + repeated_double_wrapper = "repeated_double_wrapper", + repeated_string_wrapper = "repeated_string_wrapper", + repeated_bytes_wrapper = "repeated_bytes_wrapper", + optional_duration = "optional_duration", + optional_timestamp = "optional_timestamp", + optional_field_mask = "optional_field_mask", + optional_struct = "optional_struct", + optional_any = "optional_any", + optional_value = "optional_value", + optional_null_value = "optional_null_value", + optional_empty = "optional_empty", + repeated_duration = "repeated_duration", + repeated_timestamp = "repeated_timestamp", + repeated_fieldmask = "repeated_fieldmask", + repeated_struct = "repeated_struct", + repeated_any = "repeated_any", + repeated_value = "repeated_value", + repeated_list_value = "repeated_list_value", + repeated_empty = "repeated_empty", + fieldname1 = "fieldname1", + field_name2 = "field_name2", + _field_name3 = "_field_name3", + field__name4_ = "field__name4_", + field0name5 = "field0name5", + field_0_name6 = "field_0_name6", + fieldName7 = "fieldName7", + FieldName8 = "FieldName8", + field_Name9 = "field_Name9", + Field_Name10 = "Field_Name10", + FIELD_NAME11 = "FIELD_NAME11", + FIELD_name12 = "FIELD_name12", + __field_name13 = "__field_name13", + __Field_name14 = "__Field_name14", + field__name15 = "field__name15", + field__Name16 = "field__Name16", + field_name17__ = "field_name17__", + Field_name18__ = "Field_name18__", +}) +M.TestAllTypesProto3_oneofs = pb.field_names({ + oneof_field = "oneof_field", +}) -- Message: protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage M.TestAllTypesProto3_NestedMessage_descriptor.fields = { @@ -220,12 +378,19 @@ M.TestAllTypesProto3_NestedMessage_descriptor.fields = { {name="corecursive", id=2, kind='message', message=M.TestAllTypesProto3_descriptor}, } pb.finalize_message(M.TestAllTypesProto3_NestedMessage_descriptor) +M.TestAllTypesProto3_NestedMessage_fields = pb.field_names({ + a = "a", + corecursive = "corecursive", +}) -- Message: protobuf_test_messages.proto3.ForeignMessage M.ForeignMessage_descriptor.fields = { {name="c", id=1, kind='scalar', proto_type="int32"}, } pb.finalize_message(M.ForeignMessage_descriptor) +M.ForeignMessage_fields = pb.field_names({ + c = "c", +}) -- Message: protobuf_test_messages.proto3.NullHypothesisProto3 M.NullHypothesisProto3_descriptor.fields = { diff --git a/runtime/pb/init.lua b/runtime/pb/init.lua index ee7b84f44cf44474d1b15c8dd231f24bf69bbdf1..0b6972092dffb3c1b6e74ea5ac2e7cebdc929478 100644 --- a/runtime/pb/init.lua +++ b/runtime/pb/init.lua @@ -96,6 +96,23 @@ return { to_uint64 = wire.to_uint64, to_int64 = wire.to_int64, + -- Strict field-name constants table. Generated code wraps each + -- message's `M._fields = pb.field_names({...})` so callers + -- pass typo-checked names to the lazy view: `view:get(F.user_id)` + -- errors at the read site if the field name is wrong, instead of + -- the silent `nil` that `view:get('user_di')` returns. + field_names = function(tbl) + return setmetatable(tbl, { + __index = function(_, k) + error(("unknown field name: %q"):format(tostring(k)), 2) + end, + __newindex = function(_, k) + error(("field_names table is read-only: %q"):format(tostring(k)), 2) + end, + __metatable = false, + }) + end, + -- Helper for building enum descriptors at codegen time. enum = function(name, values) local by_name, by_value = {}, {} diff --git a/test/lazy_test.lua b/test/lazy_test.lua index 7d0dee753dfe050d67a2cb4da29b2c857387070b..3ec9657474d945170371535a7a4b26f69155d2af 100644 --- a/test/lazy_test.lua +++ b/test/lazy_test.lua @@ -2,6 +2,12 @@ -- Parameterized over both codegen modes — the generated _decode_lazy -- shim is identical in both, but the descriptor shape it consumes -- comes from each mode's emitted module. +-- +-- Field-name arguments to :get / :has / :set / :clear / :which are +-- routed through the codegen-emitted M._fields and +-- M._oneofs constants tables. See docs/api-modes.md for the +-- documented contract. Map keys and repeated indices stay as raw +-- values — those aren't field names. local t = require('luatest') local ffi = require('ffi') @@ -13,30 +19,38 @@ for _, mode in ipairs(MODES) do local g = t.group('lazy.' .. mode) local hello = require(mode .. '.hello.hello_pb') + -- Local aliases keep the assertions short without losing the + -- typo-strict lookup. + local AF = hello.Address_fields + local PF = hello.Person_fields + local RF = hello.Result_fields + local EF = hello.Event_fields + local RO = hello.Result_oneofs + -- ---- Basic scalar access ---- g.test_singular_scalars_decode_on_demand = function() local enc = hello.Address_encode({street = 'Main St', city = 'SF', zip = 42}) local v = hello.Address_decode_lazy(enc) - t.assert_equals(v:get('street'), 'Main St') - t.assert_equals(v:get('city'), 'SF') - t.assert_equals(v:get('zip'), 42) - t.assert_equals(v:get('apartment'), nil, 'absent optional') + t.assert_equals(v:get(AF.street), 'Main St') + t.assert_equals(v:get(AF.city), 'SF') + t.assert_equals(v:get(AF.zip), 42) + t.assert_equals(v:get(AF.apartment), nil, 'absent optional') end g.test_has_reports_wire_presence = function() local enc = hello.Address_encode({street = 'X'}) local v = hello.Address_decode_lazy(enc) - t.assert_equals(v:has('street'), true) - t.assert_equals(v:has('city'), false) - t.assert_equals(v:has('zip'), false) + t.assert_equals(v:has(AF.street), true) + t.assert_equals(v:has(AF.city), false) + t.assert_equals(v:has(AF.zip), false) end g.test_get_caches_repeated_calls = function() local enc = hello.Address_encode({street = 'Main'}) local v = hello.Address_decode_lazy(enc) - local a = v:get('street') - local b = v:get('street') + local a = v:get(AF.street) + local b = v:get(AF.street) t.assert_is(a, b, 'string values are interned but cache should hit') end @@ -45,7 +59,7 @@ for _, mode in ipairs(MODES) do g.test_unpacked_repeated_string = function() local enc = hello.Person_encode({emails = {'a@x', 'b@x', 'c@x'}}) local v = hello.Person_decode_lazy(enc) - local arr = v:get('emails') + local arr = v:get(PF.emails) t.assert_not_equals(arr, nil) t.assert_equals(arr:len(), 3) t.assert_equals(arr:at(1), 'a@x') @@ -59,7 +73,7 @@ for _, mode in ipairs(MODES) do g.test_packed_repeated_int32 = function() local enc = hello.Person_encode({lucky_numbers = {7, 13, 42}}) local v = hello.Person_decode_lazy(enc) - local arr = v:get('lucky_numbers') + local arr = v:get(PF.lucky_numbers) t.assert_equals(arr:len(), 3) t.assert_equals(arr:at(1), 7) t.assert_equals(arr:at(2), 13) @@ -74,18 +88,18 @@ for _, mode in ipairs(MODES) do }, }) local v = hello.Person_decode_lazy(enc) - local fr = v:get('friends') + local fr = v:get(PF.friends) t.assert_equals(fr:len(), 2) - t.assert_equals(fr:at(1):get('name'), 'Bob') - t.assert_equals(fr:at(1):get('age'), 20) - t.assert_equals(fr:at(2):get('name'), 'Carol') + t.assert_equals(fr:at(1):get(PF.name), 'Bob') + t.assert_equals(fr:at(1):get(PF.age), 20) + t.assert_equals(fr:at(2):get(PF.name), 'Carol') end g.test_absent_repeated_is_nil = function() local enc = hello.Person_encode({name = 'X'}) local v = hello.Person_decode_lazy(enc) - t.assert_equals(v:get('emails'), nil) - t.assert_equals(v:has('emails'), false) + t.assert_equals(v:get(PF.emails), nil) + t.assert_equals(v:has(PF.emails), false) end -- ---- Nested singular message ---- @@ -96,9 +110,9 @@ for _, mode in ipairs(MODES) do address = {street = 'Main', city = 'Springfield'}, }) local v = hello.Person_decode_lazy(enc) - local addr = v:get('address') - t.assert_equals(addr:get('street'), 'Main') - t.assert_equals(addr:get('city'), 'Springfield') + local addr = v:get(PF.address) + t.assert_equals(addr:get(AF.street), 'Main') + t.assert_equals(addr:get(AF.city), 'Springfield') end -- ---- Map fields ---- @@ -106,7 +120,7 @@ for _, mode in ipairs(MODES) do g.test_map_get_and_has = function() local enc = hello.Person_encode({ages_by_nickname = {alice = 30, bob = 25}}) local v = hello.Person_decode_lazy(enc) - local m = v:get('ages_by_nickname') + local m = v:get(PF.ages_by_nickname) t.assert_equals(m:get('alice'), 30) t.assert_equals(m:get('bob'), 25) t.assert_equals(m:has('alice'), true) @@ -117,7 +131,7 @@ for _, mode in ipairs(MODES) do g.test_map_keys_and_iter = function() local enc = hello.Person_encode({ages_by_nickname = {alice = 30}}) local v = hello.Person_decode_lazy(enc) - local m = v:get('ages_by_nickname') + local m = v:get(PF.ages_by_nickname) t.assert_equals(m:keys(), {'alice'}) local seen = {} for k, val in m:iter() do seen[k] = val end @@ -129,10 +143,10 @@ for _, mode in ipairs(MODES) do addresses_by_label = {home = {street = 'Main', city = 'SF'}}, }) local v = hello.Person_decode_lazy(enc) - local m = v:get('addresses_by_label') + local m = v:get(PF.addresses_by_label) local home = m:get('home') - t.assert_equals(home:get('street'), 'Main') - t.assert_equals(home:get('city'), 'SF') + t.assert_equals(home:get(AF.street), 'Main') + t.assert_equals(home:get(AF.city), 'SF') end -- ---- Oneof ---- @@ -140,25 +154,25 @@ for _, mode in ipairs(MODES) do g.test_oneof_which_text_branch = function() local enc = hello.Result_encode({id = 1, text = 'ok'}) local v = hello.Result_decode_lazy(enc) - t.assert_equals(v:which('outcome'), 'text') - t.assert_equals(v:get('text'), 'ok') + t.assert_equals(v:which(RO.outcome), 'text') + t.assert_equals(v:get(RF.text), 'ok') -- Inactive branches: not on the wire, so :has is false. - t.assert_equals(v:has('code'), false) - t.assert_equals(v:has('details'), false) + t.assert_equals(v:has(RF.code), false) + t.assert_equals(v:has(RF.details), false) end g.test_oneof_which_message_branch = function() local enc = hello.Result_encode({details = {street = 'Main'}}) local v = hello.Result_decode_lazy(enc) - t.assert_equals(v:which('outcome'), 'details') - local d = v:get('details') - t.assert_equals(d:get('street'), 'Main') + t.assert_equals(v:which(RO.outcome), 'details') + local d = v:get(RF.details) + t.assert_equals(d:get(AF.street), 'Main') end g.test_oneof_no_branch_set = function() local enc = hello.Result_encode({id = 1}) local v = hello.Result_decode_lazy(enc) - t.assert_equals(v:which('outcome'), nil) + t.assert_equals(v:which(RO.outcome), nil) end -- ---- iter / names ---- @@ -187,7 +201,7 @@ for _, mode in ipairs(MODES) do local names = {} for name in v:names() do names[#names + 1] = name end t.assert_equals(names, {'street'}) - t.assert_equals(v:get('street'), 'X') + t.assert_equals(v:get(AF.street), 'X') end -- ---- WKT eager-wrap ---- @@ -197,8 +211,8 @@ for _, mode in ipairs(MODES) do local dt = datetime.new({timestamp = 1700000000, nsec = 0}) local enc = hello.Event_encode({title = 'launch', created_at = dt}) local v = hello.Event_decode_lazy(enc) - t.assert_equals(v:get('title'), 'launch') - local ts = v:get('created_at') + t.assert_equals(v:get(EF.title), 'launch') + local ts = v:get(EF.created_at) -- WKT descriptors carry desc.decode; lazy delegates to it, -- producing whatever the eager codec produces — for Timestamp, -- a datetime cdata equal to the original. @@ -211,7 +225,7 @@ for _, mode in ipairs(MODES) do local big = ffi.cast('uint64_t', 0xdeadbeefcafebabeULL) local enc = hello.Person_encode({user_id = big}) local v = hello.Person_decode_lazy(enc) - local got = v:get('user_id') + local got = v:get(PF.user_id) t.assert_equals(ffi.cast('uint64_t', got), big) end @@ -230,7 +244,7 @@ for _, mode in ipairs(MODES) do g.test_set_singular_scalar_round_trips_via_eager = function() local orig = hello.Address_encode({street = 'A', city = 'B', zip = 1}) local v = hello.Address_decode_lazy(orig) - v:set('city', 'C') + v:set(AF.city, 'C') local out = v:encode() local eager = hello.Address_decode(out) t.assert_equals(eager.street, 'A') @@ -241,7 +255,7 @@ for _, mode in ipairs(MODES) do g.test_set_repeated_replaces_entire_field = function() local orig = hello.Person_encode({emails = {'a@x', 'b@x'}}) local v = hello.Person_decode_lazy(orig) - v:set('emails', {'new@x'}) + v:set(PF.emails, {'new@x'}) local eager = hello.Person_decode(v:encode()) t.assert_equals(eager.emails, {'new@x'}) end @@ -253,7 +267,7 @@ for _, mode in ipairs(MODES) do address = {street = 'Old', city = 'X'}, }) local v = hello.Person_decode_lazy(orig) - v:set('address', {street = 'New', city = 'Y'}) + v:set(PF.address, {street = 'New', city = 'Y'}) local eager = hello.Person_decode(v:encode()) t.assert_equals(eager.name, 'Alice') t.assert_equals(eager.age, 30) @@ -266,7 +280,7 @@ for _, mode in ipairs(MODES) do local known = hello.Address_encode({street = 'X'}) local extra = string.char(0x68, 0x05) -- id=13, varint, value=5 local v = hello.Address_decode_lazy(known .. extra) - v:set('zip', 99) + v:set(AF.zip, 99) local out = v:encode() -- Unknown field bytes should still be present in the output. t.assert(out:find(extra, 1, true) ~= nil, 'unknown bytes preserved') @@ -278,8 +292,8 @@ for _, mode in ipairs(MODES) do g.test_oneof_set_clears_other_branches = function() local orig = hello.Result_encode({id = 1, text = 'hello'}) local v = hello.Result_decode_lazy(orig) - v:set('code', 42) - v:set('text', nil) -- explicit clear + v:set(RF.code, 42) + v:set(RF.text, nil) -- explicit clear local eager = hello.Result_decode(v:encode()) t.assert_equals(eager.id, 1) t.assert_equals(eager.code, 42) @@ -292,8 +306,8 @@ for _, mode in ipairs(MODES) do address = {street = 'Old', city = 'X', zip = 1}, }) local v = hello.Person_decode_lazy(orig) - local addr = v:get('address') - addr:set('street', 'New') + local addr = v:get(PF.address) + addr:set(AF.street, 'New') local eager = hello.Person_decode(v:encode()) t.assert_equals(eager.name, 'Alice') t.assert_equals(eager.address.street, 'New') @@ -315,14 +329,34 @@ for _, mode in ipairs(MODES) do local enc = hello.Person_encode(p) local eager = hello.Person_decode(enc) local v = hello.Person_decode_lazy(enc) - t.assert_equals(v:get('name'), eager.name) - t.assert_equals(v:get('age'), eager.age) - t.assert_equals(v:get('status'), eager.status) - t.assert_equals(v:get('emails'):tolist(), eager.emails) - t.assert_equals(v:get('lucky_numbers'):tolist(), eager.lucky_numbers) - t.assert_equals(v:get('ages_by_nickname'):totable(), eager.ages_by_nickname) - local addr = v:get('address') - t.assert_equals(addr:get('street'), eager.address.street) - t.assert_equals(addr:get('zip'), eager.address.zip) + t.assert_equals(v:get(PF.name), eager.name) + t.assert_equals(v:get(PF.age), eager.age) + t.assert_equals(v:get(PF.status), eager.status) + t.assert_equals(v:get(PF.emails):tolist(), eager.emails) + t.assert_equals(v:get(PF.lucky_numbers):tolist(), eager.lucky_numbers) + t.assert_equals(v:get(PF.ages_by_nickname):totable(), eager.ages_by_nickname) + local addr = v:get(PF.address) + t.assert_equals(addr:get(AF.street), eager.address.street) + t.assert_equals(addr:get(AF.zip), eager.address.zip) + end + + -- ---- Field-name constants table contract ---- + + g.test_field_names_table_errors_on_typo = function() + t.assert_error_msg_contains( + 'unknown field name: "steet"', + function() return hello.Address_fields.steet end) + end + + g.test_field_names_table_is_read_only = function() + t.assert_error_msg_contains( + 'field_names table is read-only', + function() hello.Address_fields.new_key = 'x' end) + end + + g.test_oneof_names_table_errors_on_typo = function() + t.assert_error_msg_contains( + 'unknown field name: "outcom"', + function() return hello.Result_oneofs.outcom end) end end