M Justfile => Justfile +20 -1
@@ 26,6 26,7 @@ gen_dir := "examples/expected"
docs_dir := "examples/docs"
proto_dir := "examples/proto"
conformance_proto := "test/conformance/proto"
+proto2_test_dir := "test/proto"
luatest := ".rocks/bin/luatest"
image := "tarantool-protobuf-conformance:latest"
@@ 61,7 62,7 @@ build-doc:
# ---------------------------------------------------------------------------
# Regenerate examples/expected/{full,runtime}/* + conformance protos.
-gen: gen-full gen-runtime gen-conformance
+gen: gen-full gen-runtime gen-conformance gen-proto2-tests
# Generate full-mode Lua (inline encode/decode bodies).
gen-full: build
@@ 99,6 100,24 @@ gen-conformance: build
-I {{conformance_proto}} -I options \
{{conformance_proto}}/*.proto
+# Generate the proto2 test fixtures (test/proto/*.proto) in both modes.
+# Lives outside examples/ because proto2 is exercised through the luatest
+# suite, not the per-example runners.
+gen-proto2-tests: build
+ mkdir -p {{gen_dir}}
+ protoc \
+ --plugin=./{{plugin}} \
+ --tarantool_out={{gen_dir}} \
+ --tarantool_opt=mode=full,prefix=full \
+ -I {{proto2_test_dir}} -I options \
+ {{proto2_test_dir}}/*.proto
+ protoc \
+ --plugin=./{{plugin}} \
+ --tarantool_out={{gen_dir}} \
+ --tarantool_opt=mode=runtime,prefix=runtime \
+ -I {{proto2_test_dir}} -I options \
+ {{proto2_test_dir}}/*.proto
+
# Regenerate Markdown reference docs (examples/docs/*.md) — committed output.
gen-docs: build-doc
mkdir -p {{docs_dir}}
M cmd/protoc-gen-tarantool/internal/gen/gen.go => cmd/protoc-gen-tarantool/internal/gen/gen.go +55 -4
@@ 55,9 55,10 @@ type Config struct {
// GenerateFile emits one `<lua_pkg>.lua` file per input `.proto`.
func GenerateFile(plug *protogen.Plugin, file *protogen.File, cfg Config) error {
- if file.Desc.Syntax() != protoreflect.Proto3 {
- return fmt.Errorf("%s: only proto3 is supported, got %s",
- file.Desc.Path(), file.Desc.Syntax())
+ syntax := file.Desc.Syntax()
+ if syntax != protoreflect.Proto3 && syntax != protoreflect.Proto2 {
+ return fmt.Errorf("%s: only proto2 and proto3 are supported, got %s",
+ file.Desc.Path(), syntax)
}
allMsgs := flattenMessagesSkippingMapEntries(file.Messages, nil)
@@ 473,11 474,25 @@ func renderFieldEntry(w *writer, file *protogen.File, f *protogen.Field, selfPat
parts = append(parts, fmt.Sprintf("oneof=%q", string(f.Oneof.Desc.Name())))
}
- // Proto3 explicit optional (field presence).
+ // Presence-tracked singular field. Proto3: explicit `optional` keyword
+ // (synthetic-oneof wrapped). Proto2: every singular field declared
+ // `optional` (and `required` — required fields also have presence).
+ // Repeated/map already short-circuit above; messages don't need the
+ // flag because the codec's message writer is presence-aware anyway.
if f.Desc.HasOptionalKeyword() {
parts = append(parts, "optional=true")
}
+ // Proto2 required cardinality. Codec validates on encode.
+ if f.Desc.Cardinality() == protoreflect.Required {
+ parts = append(parts, "required=true")
+ }
+
+ // Explicit `[default = X]` (proto2 only — proto3 has no custom defaults).
+ if f.Desc.HasDefault() {
+ parts = append(parts, "default_value="+renderDefaultValueLiteral(f))
+ }
+
if opts := w.renderOpts(f.Desc.Options()); opts != "" {
parts = append(parts, "options="+opts)
}
@@ 485,6 500,42 @@ func renderFieldEntry(w *writer, file *protogen.File, f *protogen.Field, selfPat
return "{" + strings.Join(parts, ", ") + "}"
}
+// renderDefaultValueLiteral converts a field's proto2 default value to the
+// Lua expression that materializes it. Matches the runtime convention:
+// strings/bytes are quoted, 64-bit integers use LuaJIT cdata literals,
+// enums use the symbolic name so codec lookups stay readable.
+func renderDefaultValueLiteral(f *protogen.Field) string {
+ v := f.Desc.Default()
+ switch f.Desc.Kind() {
+ case protoreflect.BoolKind:
+ if v.Bool() {
+ return "true"
+ }
+ return "false"
+ case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
+ return strconv.FormatInt(int64(int32(v.Int())), 10)
+ case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
+ return strconv.FormatUint(uint64(uint32(v.Uint())), 10)
+ case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
+ return strconv.FormatInt(v.Int(), 10) + "LL"
+ case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
+ return strconv.FormatUint(v.Uint(), 10) + "ULL"
+ case protoreflect.FloatKind, protoreflect.DoubleKind:
+ return formatLuaFloat(v.Float())
+ case protoreflect.StringKind:
+ return strconv.Quote(v.String())
+ case protoreflect.BytesKind:
+ return luaByteString(v.Bytes())
+ case protoreflect.EnumKind:
+ ev := f.Enum.Desc.Values().ByNumber(v.Enum())
+ if ev != nil {
+ return strconv.Quote(string(ev.Name()))
+ }
+ return strconv.FormatInt(int64(v.Enum()), 10)
+ }
+ panic("renderDefaultValueLiteral: unhandled kind " + f.Desc.Kind().String())
+}
+
// renderMapEntry renders a sub-field descriptor for a map's key or value.
// It mirrors renderFieldEntry but always for a singular non-map value, and
// emits without the `name`/`id` (caller knows: id 1 = key, id 2 = value).
M cmd/protoc-gen-tarantool/internal/gen/inline.go => cmd/protoc-gen-tarantool/internal/gen/inline.go +52 -1
@@ 94,6 94,14 @@ func emitInlineEncodeField(w *writer, f *protogen.Field, file *protogen.File, se
w.line(" -- field %d: %s", id, fname)
w.line(" v = %s", luaFieldAccess("t", fname))
+ // Proto2 `required`: error on encode if missing, never elide. Mutually
+ // exclusive with oneof and repeated, so the rest of the branching below
+ // stays unchanged for non-required fields.
+ if f.Desc.Cardinality() == protoreflect.Required {
+ emitInlineEncodeRequiredField(w, f, tag, file, selfPath, imports, prefix)
+ return
+ }
+
oneof := fieldRealOneof(f)
// Default presence gate: a regular nil check. For message fields we
// also need to accept box.NULL (which == nil via Tarantool's cdata
@@ 105,7 113,8 @@ func emitInlineEncodeField(w *writer, f *protogen.Field, file *protogen.File, se
} else if f.Message != nil {
gate = "v ~= nil or type(v) == 'cdata'"
}
- // Explicit-optional fields: presence is meaningful, no default elision.
+ // Presence semantics: oneof branches, proto3 explicit `optional`, and
+ // every proto2 singular field (`optional` keyword). No default elision.
hasPresence := oneof != "" || f.Desc.HasOptionalKeyword()
switch {
@@ 173,6 182,48 @@ func emitInlineEncodeField(w *writer, f *protogen.Field, file *protogen.File, se
}
}
+// emitInlineEncodeRequiredField generates the encode body for a proto2
+// `required` field: error if missing, always emit (no default elision).
+func emitInlineEncodeRequiredField(w *writer, f *protogen.Field, tag string, file *protogen.File, selfPath string, imports map[string]string, prefix string) {
+ fullName := string(f.Desc.FullName())
+ w.line(" if v == nil then")
+ w.line(" error(%q, 0)", "required field missing on encode: "+fullName)
+ w.line(" end")
+ switch {
+ case f.Message != nil:
+ ref := typeRef(file, f.Message.Desc, selfPath, imports, "_encode", prefix)
+ w.line(" local _b = %s(v)", ref)
+ w.line(" n = n + 1; out[n] = %s", tag)
+ w.line(" n = n + 1; out[n] = wire.encode_varint(#_b)")
+ w.line(" n = n + 1; out[n] = _b")
+ case f.Enum != nil:
+ enumLocal := typeRef(file, f.Enum.Desc, selfPath, imports, "", prefix)
+ enumFull := string(f.Enum.Desc.FullName())
+ w.line(" do")
+ w.line(" local nv = v")
+ w.line(" if type(v) == 'string' then")
+ w.line(" nv = %s[v]", enumLocal)
+ w.line(" if nv == nil then error(\"unknown enum value '\" .. v .. \"' for %s\", 0) end", enumFull)
+ w.line(" end")
+ w.line(" n = n + 1; out[n] = %s", tag)
+ w.line(" n = n + 1; out[n] = wire.encode_int32(nv)")
+ w.line(" end")
+ default:
+ st := scalarName(f.Desc.Kind())
+ if st == "" {
+ panic("unhandled scalar kind: " + f.Desc.Kind().String())
+ }
+ if st == "string" || st == "bytes" {
+ w.line(" n = n + 1; out[n] = %s", tag)
+ w.line(" n = n + 1; out[n] = wire.encode_varint(#v)")
+ w.line(" n = n + 1; out[n] = v")
+ } else {
+ w.line(" n = n + 1; out[n] = %s", tag)
+ w.line(" n = n + 1; out[n] = wire.encode_%s(v)", st)
+ }
+ }
+}
+
func emitInlineEncodeRepeated(w *writer, f *protogen.Field, tag, fname string, file *protogen.File, selfPath string, imports map[string]string, prefix string) {
switch {
case f.Message != nil:
A examples/expected/full/proto2_basic/proto2_basic_pb.lua => examples/expected/full/proto2_basic/proto2_basic_pb.lua +611 -0
@@ 0,0 1,611 @@
+-- Code generated by protoc-gen-tarantool. DO NOT EDIT.
+-- source: proto2_basic.proto
+-- syntax: proto2
+-- package: proto2_basic
+
+local pb = require("pb")
+local wire = pb.wire
+
+local M = {}
+
+M.options = {go_package = "tarantoolpb_synthetic/proto2_basic"}
+
+-- Enum: proto2_basic.Defaults.Color
+M.Defaults_Color_descriptor = pb.enum("proto2_basic.Defaults.Color", {
+ RED = 0,
+ GREEN = 1,
+ BLUE = 2,
+})
+M.Defaults_Color = M.Defaults_Color_descriptor.by_name
+
+-- Pre-declare message descriptors so cross-references resolve.
+M.Defaults_descriptor = {name = "proto2_basic.Defaults"}
+M.Cardinality_descriptor = {name = "proto2_basic.Cardinality"}
+M.Nested_descriptor = {name = "proto2_basic.Nested"}
+M.Nested_Inner_descriptor = {name = "proto2_basic.Nested.Inner"}
+
+-- Message: proto2_basic.Defaults
+M.Defaults_descriptor.fields = {
+ {name="i", id=1, kind='scalar', proto_type="int32", optional=true, default_value=17},
+ {name="s", id=2, kind='scalar', proto_type="string", optional=true, default_value="hello"},
+ {name="b", id=3, kind='scalar', proto_type="bool", optional=true, default_value=true},
+ {name="f", id=4, kind='scalar', proto_type="float", optional=true, default_value=3.5},
+ {name="d", id=5, kind='scalar', proto_type="double", optional=true, default_value=1.5},
+ {name="i64", id=6, kind='scalar', proto_type="int64", optional=true, default_value=1234567890123LL},
+ {name="u64", id=7, kind='scalar', proto_type="uint64", optional=true, default_value=17ULL},
+ {name="by", id=8, kind='scalar', proto_type="bytes", optional=true, default_value="\x00\xff"},
+ {name="color", id=9, kind='enum', enum=M.Defaults_Color_descriptor, optional=true, default_value="GREEN"},
+}
+pb.finalize_message(M.Defaults_descriptor)
+M.Defaults_fields = pb.field_names({
+ i = "i",
+ s = "s",
+ b = "b",
+ f = "f",
+ d = "d",
+ i64 = "i64",
+ u64 = "u64",
+ by = "by",
+ color = "color",
+})
+
+-- Message: proto2_basic.Cardinality
+M.Cardinality_descriptor.fields = {
+ {name="r", id=1, kind='scalar', proto_type="int32", required=true},
+ {name="o", id=2, kind='scalar', proto_type="int32", optional=true},
+ {name="packed_default", id=3, kind='scalar', proto_type="int32", repeated=true, packed=false},
+ {name="explicitly_packed", id=4, kind='scalar', proto_type="int32", repeated=true, packed=true, options={packed = true}},
+ {name="explicitly_unpacked", id=5, kind='scalar', proto_type="int32", repeated=true, packed=false, options={packed = false}},
+}
+pb.finalize_message(M.Cardinality_descriptor)
+M.Cardinality_fields = pb.field_names({
+ r = "r",
+ o = "o",
+ packed_default = "packed_default",
+ explicitly_packed = "explicitly_packed",
+ explicitly_unpacked = "explicitly_unpacked",
+})
+
+-- Message: proto2_basic.Nested
+M.Nested_descriptor.fields = {
+ {name="inner", id=1, kind='message', message=M.Nested_Inner_descriptor, required=true},
+ {name="inner_opt", id=2, kind='message', message=M.Nested_Inner_descriptor, optional=true},
+}
+pb.finalize_message(M.Nested_descriptor)
+M.Nested_fields = pb.field_names({
+ inner = "inner",
+ inner_opt = "inner_opt",
+})
+
+-- Message: proto2_basic.Nested.Inner
+M.Nested_Inner_descriptor.fields = {
+ {name="x", id=1, kind='scalar', proto_type="int32", required=true},
+}
+pb.finalize_message(M.Nested_Inner_descriptor)
+M.Nested_Inner_fields = pb.field_names({
+ x = "x",
+})
+
+-- EmmyLua / lua-language-server type annotations.
+-- These are comments — no runtime effect. They give editors
+-- autocomplete and type-checking for the generated wrappers.
+---@alias proto2_basic.Defaults.Color integer
+
+---@class proto2_basic.Defaults
+---@field i? integer
+---@field s? string
+---@field b? boolean
+---@field f? number
+---@field d? number
+---@field i64? integer
+---@field u64? integer
+---@field by? string
+---@field color? proto2_basic.Defaults.Color
+
+---@class proto2_basic.Cardinality
+---@field r integer
+---@field o? integer
+---@field packed_default integer[]
+---@field explicitly_packed integer[]
+---@field explicitly_unpacked integer[]
+
+---@class proto2_basic.Nested
+---@field inner proto2_basic.Nested.Inner
+---@field inner_opt? proto2_basic.Nested.Inner
+
+---@class proto2_basic.Nested.Inner
+---@field x integer
+
+---@param t? proto2_basic.Defaults
+---@return proto2_basic.Defaults
+function M.Defaults_new(t) return t or {} end
+
+---@param t proto2_basic.Defaults
+---@return string
+function M.Defaults_encode(t)
+ if type(t) ~= 'table' then
+ error("expected table for proto2_basic.Defaults, got " .. type(t), 0)
+ end
+ local out, n = {}, 0
+ local v
+ -- field 1: i
+ v = t.i
+ if v ~= nil then
+ n = n + 1; out[n] = "\x08"
+ n = n + 1; out[n] = wire.encode_int32(v)
+ end
+ -- field 2: s
+ v = t.s
+ if v ~= nil then
+ n = n + 1; out[n] = "\x12"
+ n = n + 1; out[n] = wire.encode_varint(#v)
+ n = n + 1; out[n] = v
+ end
+ -- field 3: b
+ v = t.b
+ if v ~= nil then
+ n = n + 1; out[n] = "\x18"
+ n = n + 1; out[n] = wire.encode_bool(v)
+ end
+ -- field 4: f
+ v = t.f
+ if v ~= nil then
+ n = n + 1; out[n] = "\x25"
+ n = n + 1; out[n] = wire.encode_float(v)
+ end
+ -- field 5: d
+ v = t.d
+ if v ~= nil then
+ n = n + 1; out[n] = "\x29"
+ n = n + 1; out[n] = wire.encode_double(v)
+ end
+ -- field 6: i64
+ v = t.i64
+ if v ~= nil then
+ n = n + 1; out[n] = "\x30"
+ n = n + 1; out[n] = wire.encode_int64(v)
+ end
+ -- field 7: u64
+ v = t.u64
+ if v ~= nil then
+ n = n + 1; out[n] = "\x38"
+ n = n + 1; out[n] = wire.encode_uint64(v)
+ end
+ -- field 8: by
+ v = t.by
+ if v ~= nil then
+ n = n + 1; out[n] = "\x42"
+ n = n + 1; out[n] = wire.encode_varint(#v)
+ n = n + 1; out[n] = v
+ end
+ -- field 9: color
+ v = t.color
+ if v ~= nil then
+ local nv = v
+ if type(v) == 'string' then
+ nv = M.Defaults_Color[v]
+ if nv == nil then error("unknown enum value '" .. v .. "' for proto2_basic.Defaults.Color", 0) end
+ end
+ n = n + 1; out[n] = "\x48"
+ n = n + 1; out[n] = wire.encode_int32(nv)
+ end
+ local _uf = t._unknown_fields
+ if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
+ return table.concat(out)
+end
+
+---@param b string
+---@return proto2_basic.Defaults
+function M.Defaults_decode(buf)
+ if type(buf) ~= 'string' then
+ error("expected string for proto2_basic.Defaults decode, got " .. type(buf), 0)
+ end
+ local result = {}
+ local pos, len = 1, #buf
+ local _uf
+ while pos <= len do
+ local _tag_start = pos
+ local id, wt
+ id, wt, pos = wire.decode_tag(buf, pos)
+ if id == 1 then
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ result.i = val
+ elseif id == 2 then
+ local val
+ val, pos = wire.decode_string(buf, pos)
+ result.s = val
+ elseif id == 3 then
+ local val
+ val, pos = wire.decode_bool(buf, pos)
+ result.b = val
+ elseif id == 4 then
+ local val
+ val, pos = wire.decode_float(buf, pos)
+ result.f = val
+ elseif id == 5 then
+ local val
+ val, pos = wire.decode_double(buf, pos)
+ result.d = val
+ elseif id == 6 then
+ local val
+ val, pos = wire.decode_int64(buf, pos)
+ result.i64 = val
+ elseif id == 7 then
+ local val
+ val, pos = wire.decode_uint64(buf, pos)
+ result.u64 = val
+ elseif id == 8 then
+ local val
+ val, pos = wire.decode_bytes(buf, pos)
+ result.by = val
+ elseif id == 9 then
+ local u
+ u, pos = wire.decode_varint(buf, pos)
+ result.color = wire.varint_to_int32(u)
+ else
+ pos = wire.skip_field(buf, pos, wt, id)
+ if _uf == nil then _uf = {} end
+ _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
+ end
+ end
+ if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
+ return result
+end
+
+---@param b string
+---@return pb.MessageView
+function M.Defaults_decode_lazy(b) return pb.decode_lazy(M.Defaults_descriptor, b) end
+---@param t proto2_basic.Defaults
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Defaults_text(t, opts) return pb.text.encode(M.Defaults_descriptor, t, opts) end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_i(t) return t.i ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_i(t) t.i = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_s(t) return t.s ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_s(t) t.s = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_b(t) return t.b ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_b(t) t.b = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_f(t) return t.f ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_f(t) t.f = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_d(t) return t.d ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_d(t) t.d = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_i64(t) return t.i64 ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_i64(t) t.i64 = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_u64(t) return t.u64 ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_u64(t) t.u64 = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_by(t) return t.by ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_by(t) t.by = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_color(t) return t.color ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_color(t) t.color = nil end
+
+---@param t? proto2_basic.Cardinality
+---@return proto2_basic.Cardinality
+function M.Cardinality_new(t) return t or {} end
+
+---@param t proto2_basic.Cardinality
+---@return string
+function M.Cardinality_encode(t)
+ if type(t) ~= 'table' then
+ error("expected table for proto2_basic.Cardinality, got " .. type(t), 0)
+ end
+ local out, n = {}, 0
+ local v
+ -- field 1: r
+ v = t.r
+ if v == nil then
+ error("required field missing on encode: proto2_basic.Cardinality.r", 0)
+ end
+ n = n + 1; out[n] = "\x08"
+ n = n + 1; out[n] = wire.encode_int32(v)
+ -- field 2: o
+ v = t.o
+ if v ~= nil then
+ n = n + 1; out[n] = "\x10"
+ n = n + 1; out[n] = wire.encode_int32(v)
+ end
+ -- field 3: packed_default
+ v = t.packed_default
+ if v ~= nil and #v > 0 then
+ local _tag = "\x18"
+ for _i = 1, #v do
+ n = n + 1; out[n] = _tag
+ n = n + 1; out[n] = wire.encode_int32(v[_i])
+ end
+ end
+ -- field 4: explicitly_packed
+ v = t.explicitly_packed
+ if v ~= nil and #v > 0 then
+ local parts, m = {}, 0
+ for _i = 1, #v do
+ m = m + 1; parts[m] = wire.encode_int32(v[_i])
+ end
+ local _b = table.concat(parts)
+ n = n + 1; out[n] = "\x22"
+ n = n + 1; out[n] = wire.encode_varint(#_b)
+ n = n + 1; out[n] = _b
+ end
+ -- field 5: explicitly_unpacked
+ v = t.explicitly_unpacked
+ if v ~= nil and #v > 0 then
+ local _tag = "\x28"
+ for _i = 1, #v do
+ n = n + 1; out[n] = _tag
+ n = n + 1; out[n] = wire.encode_int32(v[_i])
+ end
+ end
+ local _uf = t._unknown_fields
+ if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
+ return table.concat(out)
+end
+
+---@param b string
+---@return proto2_basic.Cardinality
+function M.Cardinality_decode(buf)
+ if type(buf) ~= 'string' then
+ error("expected string for proto2_basic.Cardinality decode, got " .. type(buf), 0)
+ end
+ local result = {}
+ local pos, len = 1, #buf
+ local _uf
+ while pos <= len do
+ local _tag_start = pos
+ local id, wt
+ id, wt, pos = wire.decode_tag(buf, pos)
+ if id == 1 then
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ result.r = val
+ elseif id == 2 then
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ result.o = val
+ elseif id == 3 then
+ local list = result.packed_default
+ if list == nil then list = {}; result.packed_default = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_int32(payload, p2)
+ list[#list + 1] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ list[#list + 1] = val
+ end
+ elseif id == 4 then
+ local list = result.explicitly_packed
+ if list == nil then list = {}; result.explicitly_packed = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_int32(payload, p2)
+ list[#list + 1] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ list[#list + 1] = val
+ end
+ elseif id == 5 then
+ local list = result.explicitly_unpacked
+ if list == nil then list = {}; result.explicitly_unpacked = list end
+ if wt == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local p2, lim = 1, #payload
+ while p2 <= lim do
+ local val
+ val, p2 = wire.decode_int32(payload, p2)
+ list[#list + 1] = val
+ end
+ else
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ list[#list + 1] = val
+ end
+ else
+ pos = wire.skip_field(buf, pos, wt, id)
+ if _uf == nil then _uf = {} end
+ _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
+ end
+ end
+ if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
+ return result
+end
+
+---@param b string
+---@return pb.MessageView
+function M.Cardinality_decode_lazy(b) return pb.decode_lazy(M.Cardinality_descriptor, b) end
+---@param t proto2_basic.Cardinality
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Cardinality_text(t, opts) return pb.text.encode(M.Cardinality_descriptor, t, opts) end
+---@param t proto2_basic.Cardinality
+---@return boolean
+function M.Cardinality_has_o(t) return t.o ~= nil end
+---@param t proto2_basic.Cardinality
+function M.Cardinality_clear_o(t) t.o = nil end
+
+---@param t? proto2_basic.Nested
+---@return proto2_basic.Nested
+function M.Nested_new(t) return t or {} end
+
+---@param t proto2_basic.Nested
+---@return string
+function M.Nested_encode(t)
+ if type(t) ~= 'table' then
+ error("expected table for proto2_basic.Nested, got " .. type(t), 0)
+ end
+ local out, n = {}, 0
+ local v
+ -- field 1: inner
+ v = t.inner
+ if v == nil then
+ error("required field missing on encode: proto2_basic.Nested.inner", 0)
+ end
+ local _b = M.Nested_Inner_encode(v)
+ n = n + 1; out[n] = "\x0a"
+ n = n + 1; out[n] = wire.encode_varint(#_b)
+ n = n + 1; out[n] = _b
+ -- field 2: inner_opt
+ v = t.inner_opt
+ if v ~= nil or type(v) == 'cdata' then
+ local _b = M.Nested_Inner_encode(v)
+ n = n + 1; out[n] = "\x12"
+ n = n + 1; out[n] = wire.encode_varint(#_b)
+ n = n + 1; out[n] = _b
+ end
+ local _uf = t._unknown_fields
+ if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
+ return table.concat(out)
+end
+
+---@param b string
+---@return proto2_basic.Nested
+function M.Nested_decode(buf)
+ if type(buf) ~= 'string' then
+ error("expected string for proto2_basic.Nested decode, got " .. type(buf), 0)
+ end
+ local result = {}
+ local pos, len = 1, #buf
+ local _uf
+ while pos <= len do
+ local _tag_start = pos
+ local id, wt
+ id, wt, pos = wire.decode_tag(buf, pos)
+ if id == 1 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local prev = result.inner
+ if prev == nil then
+ result.inner = M.Nested_Inner_decode(payload)
+ else
+ pb.codec.merge_message(M.Nested_Inner_descriptor, prev, M.Nested_Inner_decode(payload))
+ end
+ elseif id == 2 then
+ local payload
+ payload, pos = wire.decode_len(buf, pos)
+ local prev = result.inner_opt
+ if prev == nil then
+ result.inner_opt = M.Nested_Inner_decode(payload)
+ else
+ pb.codec.merge_message(M.Nested_Inner_descriptor, prev, M.Nested_Inner_decode(payload))
+ end
+ else
+ pos = wire.skip_field(buf, pos, wt, id)
+ if _uf == nil then _uf = {} end
+ _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
+ end
+ end
+ if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
+ return result
+end
+
+---@param b string
+---@return pb.MessageView
+function M.Nested_decode_lazy(b) return pb.decode_lazy(M.Nested_descriptor, b) end
+---@param t proto2_basic.Nested
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Nested_text(t, opts) return pb.text.encode(M.Nested_descriptor, t, opts) end
+---@param t proto2_basic.Nested
+---@return boolean
+function M.Nested_has_inner_opt(t) return t.inner_opt ~= nil end
+---@param t proto2_basic.Nested
+function M.Nested_clear_inner_opt(t) t.inner_opt = nil end
+
+---@param t? proto2_basic.Nested.Inner
+---@return proto2_basic.Nested.Inner
+function M.Nested_Inner_new(t) return t or {} end
+
+---@param t proto2_basic.Nested.Inner
+---@return string
+function M.Nested_Inner_encode(t)
+ if type(t) ~= 'table' then
+ error("expected table for proto2_basic.Nested.Inner, got " .. type(t), 0)
+ end
+ local out, n = {}, 0
+ local v
+ -- field 1: x
+ v = t.x
+ if v == nil then
+ error("required field missing on encode: proto2_basic.Nested.Inner.x", 0)
+ end
+ n = n + 1; out[n] = "\x08"
+ n = n + 1; out[n] = wire.encode_int32(v)
+ local _uf = t._unknown_fields
+ if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end
+ return table.concat(out)
+end
+
+---@param b string
+---@return proto2_basic.Nested.Inner
+function M.Nested_Inner_decode(buf)
+ if type(buf) ~= 'string' then
+ error("expected string for proto2_basic.Nested.Inner decode, got " .. type(buf), 0)
+ end
+ local result = {}
+ local pos, len = 1, #buf
+ local _uf
+ while pos <= len do
+ local _tag_start = pos
+ local id, wt
+ id, wt, pos = wire.decode_tag(buf, pos)
+ if id == 1 then
+ local val
+ val, pos = wire.decode_int32(buf, pos)
+ result.x = val
+ else
+ pos = wire.skip_field(buf, pos, wt, id)
+ if _uf == nil then _uf = {} end
+ _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)
+ end
+ end
+ if _uf ~= nil then result._unknown_fields = table.concat(_uf) end
+ return result
+end
+
+---@param b string
+---@return pb.MessageView
+function M.Nested_Inner_decode_lazy(b) return pb.decode_lazy(M.Nested_Inner_descriptor, b) end
+---@param t proto2_basic.Nested.Inner
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Nested_Inner_text(t, opts) return pb.text.encode(M.Nested_Inner_descriptor, t, opts) end
+
+return M
A examples/expected/runtime/proto2_basic/proto2_basic_pb.lua => examples/expected/runtime/proto2_basic/proto2_basic_pb.lua +242 -0
@@ 0,0 1,242 @@
+-- Code generated by protoc-gen-tarantool. DO NOT EDIT.
+-- source: proto2_basic.proto
+-- syntax: proto2
+-- package: proto2_basic
+
+local pb = require("pb")
+local wire = pb.wire
+
+local M = {}
+
+M.options = {go_package = "tarantoolpb_synthetic/proto2_basic"}
+
+-- Enum: proto2_basic.Defaults.Color
+M.Defaults_Color_descriptor = pb.enum("proto2_basic.Defaults.Color", {
+ RED = 0,
+ GREEN = 1,
+ BLUE = 2,
+})
+M.Defaults_Color = M.Defaults_Color_descriptor.by_name
+
+-- Pre-declare message descriptors so cross-references resolve.
+M.Defaults_descriptor = {name = "proto2_basic.Defaults"}
+M.Cardinality_descriptor = {name = "proto2_basic.Cardinality"}
+M.Nested_descriptor = {name = "proto2_basic.Nested"}
+M.Nested_Inner_descriptor = {name = "proto2_basic.Nested.Inner"}
+
+-- Message: proto2_basic.Defaults
+M.Defaults_descriptor.fields = {
+ {name="i", id=1, kind='scalar', proto_type="int32", optional=true, default_value=17},
+ {name="s", id=2, kind='scalar', proto_type="string", optional=true, default_value="hello"},
+ {name="b", id=3, kind='scalar', proto_type="bool", optional=true, default_value=true},
+ {name="f", id=4, kind='scalar', proto_type="float", optional=true, default_value=3.5},
+ {name="d", id=5, kind='scalar', proto_type="double", optional=true, default_value=1.5},
+ {name="i64", id=6, kind='scalar', proto_type="int64", optional=true, default_value=1234567890123LL},
+ {name="u64", id=7, kind='scalar', proto_type="uint64", optional=true, default_value=17ULL},
+ {name="by", id=8, kind='scalar', proto_type="bytes", optional=true, default_value="\x00\xff"},
+ {name="color", id=9, kind='enum', enum=M.Defaults_Color_descriptor, optional=true, default_value="GREEN"},
+}
+pb.finalize_message(M.Defaults_descriptor)
+M.Defaults_fields = pb.field_names({
+ i = "i",
+ s = "s",
+ b = "b",
+ f = "f",
+ d = "d",
+ i64 = "i64",
+ u64 = "u64",
+ by = "by",
+ color = "color",
+})
+
+-- Message: proto2_basic.Cardinality
+M.Cardinality_descriptor.fields = {
+ {name="r", id=1, kind='scalar', proto_type="int32", required=true},
+ {name="o", id=2, kind='scalar', proto_type="int32", optional=true},
+ {name="packed_default", id=3, kind='scalar', proto_type="int32", repeated=true, packed=false},
+ {name="explicitly_packed", id=4, kind='scalar', proto_type="int32", repeated=true, packed=true, options={packed = true}},
+ {name="explicitly_unpacked", id=5, kind='scalar', proto_type="int32", repeated=true, packed=false, options={packed = false}},
+}
+pb.finalize_message(M.Cardinality_descriptor)
+M.Cardinality_fields = pb.field_names({
+ r = "r",
+ o = "o",
+ packed_default = "packed_default",
+ explicitly_packed = "explicitly_packed",
+ explicitly_unpacked = "explicitly_unpacked",
+})
+
+-- Message: proto2_basic.Nested
+M.Nested_descriptor.fields = {
+ {name="inner", id=1, kind='message', message=M.Nested_Inner_descriptor, required=true},
+ {name="inner_opt", id=2, kind='message', message=M.Nested_Inner_descriptor, optional=true},
+}
+pb.finalize_message(M.Nested_descriptor)
+M.Nested_fields = pb.field_names({
+ inner = "inner",
+ inner_opt = "inner_opt",
+})
+
+-- Message: proto2_basic.Nested.Inner
+M.Nested_Inner_descriptor.fields = {
+ {name="x", id=1, kind='scalar', proto_type="int32", required=true},
+}
+pb.finalize_message(M.Nested_Inner_descriptor)
+M.Nested_Inner_fields = pb.field_names({
+ x = "x",
+})
+
+-- EmmyLua / lua-language-server type annotations.
+-- These are comments — no runtime effect. They give editors
+-- autocomplete and type-checking for the generated wrappers.
+---@alias proto2_basic.Defaults.Color integer
+
+---@class proto2_basic.Defaults
+---@field i? integer
+---@field s? string
+---@field b? boolean
+---@field f? number
+---@field d? number
+---@field i64? integer
+---@field u64? integer
+---@field by? string
+---@field color? proto2_basic.Defaults.Color
+
+---@class proto2_basic.Cardinality
+---@field r integer
+---@field o? integer
+---@field packed_default integer[]
+---@field explicitly_packed integer[]
+---@field explicitly_unpacked integer[]
+
+---@class proto2_basic.Nested
+---@field inner proto2_basic.Nested.Inner
+---@field inner_opt? proto2_basic.Nested.Inner
+
+---@class proto2_basic.Nested.Inner
+---@field x integer
+
+---@param t? proto2_basic.Defaults
+---@return proto2_basic.Defaults
+function M.Defaults_new(t) return t or {} end
+---@param t proto2_basic.Defaults
+---@return string
+function M.Defaults_encode(t) return pb.encode(M.Defaults_descriptor, t) end
+---@param b string
+---@return proto2_basic.Defaults
+function M.Defaults_decode(b) return pb.decode(M.Defaults_descriptor, b) end
+---@param b string
+---@return pb.MessageView
+function M.Defaults_decode_lazy(b) return pb.decode_lazy(M.Defaults_descriptor, b) end
+---@param t proto2_basic.Defaults
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Defaults_text(t, opts) return pb.text.encode(M.Defaults_descriptor, t, opts) end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_i(t) return t.i ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_i(t) t.i = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_s(t) return t.s ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_s(t) t.s = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_b(t) return t.b ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_b(t) t.b = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_f(t) return t.f ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_f(t) t.f = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_d(t) return t.d ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_d(t) t.d = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_i64(t) return t.i64 ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_i64(t) t.i64 = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_u64(t) return t.u64 ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_u64(t) t.u64 = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_by(t) return t.by ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_by(t) t.by = nil end
+---@param t proto2_basic.Defaults
+---@return boolean
+function M.Defaults_has_color(t) return t.color ~= nil end
+---@param t proto2_basic.Defaults
+function M.Defaults_clear_color(t) t.color = nil end
+
+---@param t? proto2_basic.Cardinality
+---@return proto2_basic.Cardinality
+function M.Cardinality_new(t) return t or {} end
+---@param t proto2_basic.Cardinality
+---@return string
+function M.Cardinality_encode(t) return pb.encode(M.Cardinality_descriptor, t) end
+---@param b string
+---@return proto2_basic.Cardinality
+function M.Cardinality_decode(b) return pb.decode(M.Cardinality_descriptor, b) end
+---@param b string
+---@return pb.MessageView
+function M.Cardinality_decode_lazy(b) return pb.decode_lazy(M.Cardinality_descriptor, b) end
+---@param t proto2_basic.Cardinality
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Cardinality_text(t, opts) return pb.text.encode(M.Cardinality_descriptor, t, opts) end
+---@param t proto2_basic.Cardinality
+---@return boolean
+function M.Cardinality_has_o(t) return t.o ~= nil end
+---@param t proto2_basic.Cardinality
+function M.Cardinality_clear_o(t) t.o = nil end
+
+---@param t? proto2_basic.Nested
+---@return proto2_basic.Nested
+function M.Nested_new(t) return t or {} end
+---@param t proto2_basic.Nested
+---@return string
+function M.Nested_encode(t) return pb.encode(M.Nested_descriptor, t) end
+---@param b string
+---@return proto2_basic.Nested
+function M.Nested_decode(b) return pb.decode(M.Nested_descriptor, b) end
+---@param b string
+---@return pb.MessageView
+function M.Nested_decode_lazy(b) return pb.decode_lazy(M.Nested_descriptor, b) end
+---@param t proto2_basic.Nested
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Nested_text(t, opts) return pb.text.encode(M.Nested_descriptor, t, opts) end
+---@param t proto2_basic.Nested
+---@return boolean
+function M.Nested_has_inner_opt(t) return t.inner_opt ~= nil end
+---@param t proto2_basic.Nested
+function M.Nested_clear_inner_opt(t) t.inner_opt = nil end
+
+---@param t? proto2_basic.Nested.Inner
+---@return proto2_basic.Nested.Inner
+function M.Nested_Inner_new(t) return t or {} end
+---@param t proto2_basic.Nested.Inner
+---@return string
+function M.Nested_Inner_encode(t) return pb.encode(M.Nested_Inner_descriptor, t) end
+---@param b string
+---@return proto2_basic.Nested.Inner
+function M.Nested_Inner_decode(b) return pb.decode(M.Nested_Inner_descriptor, b) end
+---@param b string
+---@return pb.MessageView
+function M.Nested_Inner_decode_lazy(b) return pb.decode_lazy(M.Nested_Inner_descriptor, b) end
+---@param t proto2_basic.Nested.Inner
+---@param opts? {single_line: boolean?, indent: string?}
+---@return string
+function M.Nested_Inner_text(t, opts) return pb.text.encode(M.Nested_Inner_descriptor, t, opts) end
+
+return M
M runtime/pb/codec.lua => runtime/pb/codec.lua +71 -2
@@ 389,10 389,79 @@ local function build_repeated_writer(f)
return nil
end
-local function build_writer(f)
+-- build_required_writer specializes for proto2 `required` singular fields:
+-- error if the value is missing, never elide (defaults are not skipped).
+-- Required is mutually exclusive with repeated/map/oneof, so this only
+-- handles singular scalar/enum/message. `owner_name` is the parent
+-- message's full name; threaded through compile_writers so the missing-
+-- field error tells the caller which message they were encoding.
+local function build_required_writer(f, owner_name)
+ local fname = f.name
+ local kind = f.kind
+ local missing_msg = "required field missing on encode: "
+ .. tostring(owner_name) .. "." .. tostring(f.name)
+
+ if kind == 'scalar' then
+ local handler = scalar[f.proto_type]
+ if not handler then return nil end
+ local tag_bytes = wire.encode_tag(f.id, handler.wire)
+ local encode_value = handler.encode
+ local proto_type = f.proto_type
+ if proto_type == 'string' or proto_type == 'bytes' then
+ return function(data, out)
+ local v = data[fname]
+ if v == nil then error(missing_msg, 0) end
+ local n = #out
+ out[n + 1] = tag_bytes
+ out[n + 2] = wire.encode_varint(#v)
+ out[n + 3] = v
+ end
+ end
+ return function(data, out)
+ local v = data[fname]
+ if v == nil then error(missing_msg, 0) end
+ local n = #out
+ out[n + 1] = tag_bytes
+ out[n + 2] = encode_value(v)
+ end
+ end
+
+ if kind == 'enum' then
+ local enum_desc = f.enum
+ local tag_bytes = wire.encode_tag(f.id, wire.WIRE_VARINT)
+ return function(data, out)
+ local v = data[fname]
+ if v == nil then error(missing_msg, 0) end
+ local n_enum = encode_enum_value(enum_desc, v)
+ local n = #out
+ out[n + 1] = tag_bytes
+ out[n + 2] = wire.encode_varint(n_enum)
+ end
+ end
+
+ if kind == 'message' then
+ local sub_desc = f.message
+ local tag_bytes = wire.encode_tag(f.id, wire.WIRE_LEN)
+ return function(data, out)
+ local v = data[fname]
+ if v == nil and type(v) ~= 'cdata' then error(missing_msg, 0) end
+ local body = encode_msg(sub_desc, v)
+ local n = #out
+ out[n + 1] = tag_bytes
+ out[n + 2] = wire.encode_varint(#body)
+ out[n + 3] = body
+ end
+ end
+
+ return nil
+end
+
+local function build_writer(f, owner_name)
-- Maps and oneof branches keep going through encode_field.
if f.kind == 'map' or f.oneof then return nil end
+ if f.required then return build_required_writer(f, owner_name) end
+
if f.repeated then return build_repeated_writer(f) end
local fname = f.name
@@ 515,7 584,7 @@ end
-- specialized. Called from pb.finalize_message after the oneof flatten.
function M.compile_writers(desc)
for _, f in ipairs(desc.fields) do
- f._writer = build_writer(f)
+ f._writer = build_writer(f, desc.name)
end
end
A test/proto/proto2_basic.proto => test/proto/proto2_basic.proto +38 -0
@@ 0,0 1,38 @@
+syntax = "proto2";
+
+package proto2_basic;
+
+message Defaults {
+ optional int32 i = 1 [default = 17];
+ optional string s = 2 [default = "hello"];
+ optional bool b = 3 [default = true];
+ optional float f = 4 [default = 3.5];
+ optional double d = 5 [default = 1.5];
+ optional int64 i64 = 6 [default = 1234567890123];
+ optional uint64 u64 = 7 [default = 17];
+ optional bytes by = 8 [default = "\x00\xff"];
+ optional Color color = 9 [default = GREEN];
+
+ enum Color {
+ RED = 0;
+ GREEN = 1;
+ BLUE = 2;
+ }
+}
+
+message Cardinality {
+ required int32 r = 1;
+ optional int32 o = 2;
+ repeated int32 packed_default = 3;
+ repeated int32 explicitly_packed = 4 [packed = true];
+ repeated int32 explicitly_unpacked = 5 [packed = false];
+}
+
+message Nested {
+ required Inner inner = 1;
+ optional Inner inner_opt = 2;
+
+ message Inner {
+ required int32 x = 1;
+ }
+}
A test/proto2_test.lua => test/proto2_test.lua +187 -0
@@ 0,0 1,187 @@
+-- Proto2 round-trip + required-field validation, parametrized over both
+-- codegen modes. The fixture is generated from test/proto/proto2_basic.proto
+-- by `just gen-proto2-tests`.
+local t = require('luatest')
+local ffi = require('ffi')
+
+local function hex(s)
+ local out = {}
+ for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end
+ return table.concat(out)
+end
+
+local MODES = {'full', 'runtime'}
+
+for _, mode in ipairs(MODES) do
+ local g = t.group('proto2_basic.' .. mode)
+ local pb = require(mode .. '.proto2_basic.proto2_basic_pb')
+
+ -- ----- Defaults: explicit-optional fields with [default = X] -----
+
+ g.test_defaults_descriptor_carries_default_value = function()
+ local d = pb.Defaults_descriptor.field_by_name
+ t.assert_equals(d.i.default_value, 17)
+ t.assert_equals(d.s.default_value, 'hello')
+ t.assert_equals(d.b.default_value, true)
+ t.assert_equals(d.f.default_value, 3.5)
+ t.assert_equals(d.d.default_value, 1.5)
+ t.assert(d.i64.default_value == ffi.cast('int64_t', 1234567890123),
+ 'int64 default cdata equality')
+ t.assert(d.u64.default_value == ffi.cast('uint64_t', 17),
+ 'uint64 default cdata equality')
+ t.assert_equals(d.by.default_value, '\x00\xff')
+ t.assert_equals(d.color.default_value, 'GREEN')
+ end
+
+ g.test_empty_message_round_trips_to_empty_bytes = function()
+ -- All proto2 fields are presence-tracked. An empty Lua table has no
+ -- fields set, so encode produces zero bytes (no defaults serialized).
+ local enc = pb.Defaults_encode({})
+ t.assert_equals(enc, '', 'no defaults on the wire')
+ t.assert_equals(pb.Defaults_decode(enc), {})
+ end
+
+ g.test_set_value_round_trip = function()
+ local val = {i = 42, s = 'world', b = false, f = -1.5}
+ local dec = pb.Defaults_decode(pb.Defaults_encode(val))
+ t.assert_equals(dec.i, 42)
+ t.assert_equals(dec.s, 'world')
+ t.assert_equals(dec.b, false)
+ t.assert_equals(dec.f, -1.5)
+ -- d/i64/u64/by/color stay absent.
+ t.assert_equals(dec.d, nil)
+ t.assert_equals(dec.color, nil)
+ end
+
+ g.test_set_to_proto_default_still_serializes = function()
+ -- proto2 presence means setting a field to its declared default
+ -- still emits it on the wire (no proto3-style elision).
+ local enc = pb.Defaults_encode({i = 17})
+ t.assert_not_equals(enc, '', 'presence-tracked field at default must serialize')
+ t.assert_equals(pb.Defaults_decode(enc).i, 17)
+ end
+
+ -- ----- Cardinality: required vs optional vs repeated -----
+
+ g.test_required_missing_errors = function()
+ local ok, err = pcall(pb.Cardinality_encode, {})
+ t.assert_equals(ok, false)
+ t.assert_str_contains(err, 'required field missing on encode')
+ t.assert_str_contains(err, 'proto2_basic.Cardinality.r')
+ end
+
+ g.test_required_zero_emitted = function()
+ -- Required field at proto2-default zero must still be on the wire.
+ local enc = pb.Cardinality_encode({r = 0})
+ t.assert_equals(hex(enc), '0800',
+ 'required int32=0: tag 1/VARINT + varint 0')
+ end
+
+ g.test_required_set = function()
+ local enc = pb.Cardinality_encode({r = 7})
+ local dec = pb.Cardinality_decode(enc)
+ t.assert_equals(dec.r, 7)
+ end
+
+ g.test_repeated_unpacked_by_default = function()
+ -- Proto2 default for repeated scalars is NOT packed.
+ local enc = pb.Cardinality_encode({r = 0, packed_default = {1, 2, 3}})
+ -- field 3, wire VARINT (0x18) repeated three times.
+ t.assert_equals(hex(enc), '0800' .. '180118021803')
+ end
+
+ g.test_repeated_explicit_packed = function()
+ local enc = pb.Cardinality_encode({r = 0, explicitly_packed = {1, 2, 3}})
+ -- field 4, wire LEN (0x22), len=3, varints 1,2,3.
+ t.assert_equals(hex(enc), '0800' .. '2203' .. '010203')
+ end
+
+ g.test_repeated_explicit_unpacked = function()
+ local enc = pb.Cardinality_encode({r = 0, explicitly_unpacked = {1, 2, 3}})
+ -- field 5, wire VARINT (0x28) repeated three times.
+ t.assert_equals(hex(enc), '0800' .. '280128022803')
+ end
+
+ g.test_repeated_decode_accepts_both_packed_and_unpacked = function()
+ -- A wire stream with packed_default encoded as packed (legal — proto
+ -- consumers must accept either form) decodes the same way as unpacked.
+ local packed = '\x08\x00\x1a\x03\x01\x02\x03' -- field 3 with LEN
+ local unpacked = '\x08\x00\x18\x01\x18\x02\x18\x03'
+ local a = pb.Cardinality_decode(packed)
+ local b = pb.Cardinality_decode(unpacked)
+ t.assert_equals(a.packed_default, {1, 2, 3})
+ t.assert_equals(b.packed_default, {1, 2, 3})
+ end
+
+ -- ----- Nested required message -----
+
+ g.test_nested_required_message_missing_errors = function()
+ local ok, err = pcall(pb.Nested_encode, {})
+ t.assert_equals(ok, false)
+ t.assert_str_contains(err, 'required field missing on encode')
+ t.assert_str_contains(err, 'proto2_basic.Nested.inner')
+ end
+
+ g.test_nested_required_inner_required = function()
+ -- Inner message also has its own required field (x). When Inner
+ -- itself is required on the outer message, missing it errors on the
+ -- inner encode call.
+ local ok, err = pcall(pb.Nested_encode, {inner = {}})
+ t.assert_equals(ok, false)
+ t.assert_str_contains(err, 'proto2_basic.Nested.Inner.x')
+ end
+
+ g.test_nested_required_filled_round_trips = function()
+ local val = {inner = {x = 5}, inner_opt = {x = 9}}
+ local dec = pb.Nested_decode(pb.Nested_encode(val))
+ t.assert_equals(dec.inner.x, 5)
+ t.assert_equals(dec.inner_opt.x, 9)
+ end
+
+ -- ----- Enum default surfaced in descriptor -----
+
+ g.test_enum_default_descriptor = function()
+ local f = pb.Defaults_descriptor.field_by_name.color
+ t.assert_equals(f.default_value, 'GREEN')
+ -- The enum descriptor itself round-trips the symbolic name.
+ t.assert_equals(pb.Defaults_Color_descriptor.by_name['GREEN'], 1)
+ end
+end
+
+-- Parity: full and runtime modes must produce byte-identical output for
+-- the same input. Equivalent to the existing parity.full_vs_runtime group.
+do
+ local g = t.group('proto2_basic.parity')
+ local full = require('full.proto2_basic.proto2_basic_pb')
+ local runtime = require('runtime.proto2_basic.proto2_basic_pb')
+
+ local function check_parity(msg, encode_full, encode_runtime, value)
+ local bf, br = encode_full(value), encode_runtime(value)
+ t.assert_equals(hex(bf), hex(br),
+ msg .. ': full and runtime modes must produce identical bytes')
+ end
+
+ g.test_defaults_round_trip_parity = function()
+ check_parity('Defaults set values', full.Defaults_encode,
+ runtime.Defaults_encode,
+ {i = 42, s = 'world', b = true, f = 0.5, d = -1.5})
+ end
+
+ g.test_cardinality_required_parity = function()
+ check_parity('Cardinality.r=0', full.Cardinality_encode,
+ runtime.Cardinality_encode, {r = 0})
+ check_parity('Cardinality full', full.Cardinality_encode,
+ runtime.Cardinality_encode, {
+ r = 1,
+ o = 2,
+ packed_default = {3, 4, 5},
+ explicitly_packed = {6, 7, 8},
+ explicitly_unpacked = {9, 10},
+ })
+ end
+
+ g.test_nested_parity = function()
+ check_parity('Nested', full.Nested_encode, runtime.Nested_encode,
+ {inner = {x = 5}, inner_opt = {x = 9}})
+ end
+end