M README.md => README.md +7 -0
@@ 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_<field>(t) -- only emitted for proto3 explicit-`optional` fields
M.Foo_clear_<field>(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).
M cmd/protoc-gen-tarantool/internal/gen/gen.go => cmd/protoc-gen-tarantool/internal/gen/gen.go +46 -0
@@ 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.<Name>_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.<Name>_descriptor.oneofs = { <name> = {...members...} }`
// when the message has any non-synthetic oneofs.
func emitOneofTable(w *writer, name string, m *protogen.Message) {
M docs/api-modes.md => docs/api-modes.md +50 -11
@@ 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.<Type>_fields` table mapping each
+field name to itself, and (when applicable) `M.<Type>_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.
M examples/expected/full/conformance/conformance_pb.lua => examples/expected/full/conformance/conformance_pb.lua +39 -0
@@ 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
M examples/expected/full/hello/hello_pb.lua => examples/expected/full/hello/hello_pb.lua +51 -0
@@ 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
M examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua => examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +165 -0
@@ 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 = {
M examples/expected/runtime/conformance/conformance_pb.lua => examples/expected/runtime/conformance/conformance_pb.lua +39 -0
@@ 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
M examples/expected/runtime/hello/hello_pb.lua => examples/expected/runtime/hello/hello_pb.lua +51 -0
@@ 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
M examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua => examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +165 -0
@@ 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 = {
M runtime/pb/init.lua => runtime/pb/init.lua +17 -0
@@ 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.<Type>_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 = {}, {}
M test/lazy_test.lua => test/lazy_test.lua +88 -54
@@ 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.<Type>_fields and
+-- M.<Type>_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