~bigbes/tarantool

tarantool-protobuf

e9b650e9acdfc6cdd18842c133c4e4111908613b — Eugene Blikh 3 months ago a9036f1
text,codec: proto2 group text syntax + closed-enum semantics

Text decoder now resolves a group field reference by the group's
capitalized submessage name (e.g. \`Data\`, \`MultiWordGroupField\`)
and by the ASCII-lowercase fold (\`data\`, \`multiwordgroupfield\`),
in addition to the existing lowercase field-name lookup. The \`:\`
between field and \`{\` is optional for groups (matches the
message/map rule).

Proto2 enums are closed: parse_enum_value now rejects an integer
literal that doesn't map to any declared value. The plugin emits
\`closed = true\` on every proto2 enum descriptor (driven by
protoreflect's IsClosed()); proto3 enums stay open to preserve
forward-compatibility on the wire.

Conformance text-format suite: 16 unexpected failures → 3, all in
the remaining extension-bracketed-group cases.
M cmd/protoc-gen-tarantool/internal/gen/gen.go => cmd/protoc-gen-tarantool/internal/gen/gen.go +6 -0
@@ 239,6 239,12 @@ func emitEnum(w *writer, file *protogen.File, e *protogen.Enum) {
	if opts := w.renderOpts(e.Desc.Options()); opts != "" {
		w.line("M.%s_descriptor.options = %s", name, opts)
	}
	// Proto2 enums are closed: unknown numeric values must be rejected at
	// JSON/text decode time and on wire they round-trip as unknown fields.
	// Proto3 enums are open. Surface the flag so codecs can branch.
	if e.Desc.IsClosed() {
		w.line("M.%s_descriptor.closed = true", name)
	}
	emitEnumValueOptions(w, name, e)
	// Convenience aliases the user can reach via `M.MyEnum.RED`, etc.
	w.line("M.%s = M.%s_descriptor.by_name", name, name)

M examples/expected/full/proto2_basic/proto2_basic_pb.lua => examples/expected/full/proto2_basic/proto2_basic_pb.lua +1 -0
@@ 16,6 16,7 @@ M.Defaults_Color_descriptor = pb.enum("proto2_basic.Defaults.Color", {
    GREEN = 1,
    BLUE = 2,
})
M.Defaults_Color_descriptor.closed = true
M.Defaults_Color = M.Defaults_Color_descriptor.by_name

-- Pre-declare message descriptors so cross-references resolve.

M examples/expected/full/protobuf_test_messages/proto2/test_messages_proto2_pb.lua => examples/expected/full/protobuf_test_messages/proto2/test_messages_proto2_pb.lua +4 -0
@@ 16,6 16,7 @@ M.ForeignEnumProto2_descriptor = pb.enum("protobuf_test_messages.proto2.ForeignE
    FOREIGN_BAR = 1,
    FOREIGN_BAZ = 2,
})
M.ForeignEnumProto2_descriptor.closed = true
M.ForeignEnumProto2 = M.ForeignEnumProto2_descriptor.by_name

-- Enum: protobuf_test_messages.proto2.TestAllTypesProto2.NestedEnum


@@ 25,6 26,7 @@ M.TestAllTypesProto2_NestedEnum_descriptor = pb.enum("protobuf_test_messages.pro
    BAZ = 2,
    NEG = -1,
})
M.TestAllTypesProto2_NestedEnum_descriptor.closed = true
M.TestAllTypesProto2_NestedEnum = M.TestAllTypesProto2_NestedEnum_descriptor.by_name

-- Enum: protobuf_test_messages.proto2.EnumOnlyProto2.Bool


@@ 32,6 34,7 @@ M.EnumOnlyProto2_Bool_descriptor = pb.enum("protobuf_test_messages.proto2.EnumOn
    kFalse = 0,
    kTrue = 1,
})
M.EnumOnlyProto2_Bool_descriptor.closed = true
M.EnumOnlyProto2_Bool = M.EnumOnlyProto2_Bool_descriptor.by_name

-- Enum: protobuf_test_messages.proto2.TestAllRequiredTypesProto2.NestedEnum


@@ 41,6 44,7 @@ M.TestAllRequiredTypesProto2_NestedEnum_descriptor = pb.enum("protobuf_test_mess
    BAZ = 2,
    NEG = -1,
})
M.TestAllRequiredTypesProto2_NestedEnum_descriptor.closed = true
M.TestAllRequiredTypesProto2_NestedEnum = M.TestAllRequiredTypesProto2_NestedEnum_descriptor.by_name

-- Pre-declare message descriptors so cross-references resolve.

M examples/expected/runtime/proto2_basic/proto2_basic_pb.lua => examples/expected/runtime/proto2_basic/proto2_basic_pb.lua +1 -0
@@ 16,6 16,7 @@ M.Defaults_Color_descriptor = pb.enum("proto2_basic.Defaults.Color", {
    GREEN = 1,
    BLUE = 2,
})
M.Defaults_Color_descriptor.closed = true
M.Defaults_Color = M.Defaults_Color_descriptor.by_name

-- Pre-declare message descriptors so cross-references resolve.

M examples/expected/runtime/protobuf_test_messages/proto2/test_messages_proto2_pb.lua => examples/expected/runtime/protobuf_test_messages/proto2/test_messages_proto2_pb.lua +4 -0
@@ 16,6 16,7 @@ M.ForeignEnumProto2_descriptor = pb.enum("protobuf_test_messages.proto2.ForeignE
    FOREIGN_BAR = 1,
    FOREIGN_BAZ = 2,
})
M.ForeignEnumProto2_descriptor.closed = true
M.ForeignEnumProto2 = M.ForeignEnumProto2_descriptor.by_name

-- Enum: protobuf_test_messages.proto2.TestAllTypesProto2.NestedEnum


@@ 25,6 26,7 @@ M.TestAllTypesProto2_NestedEnum_descriptor = pb.enum("protobuf_test_messages.pro
    BAZ = 2,
    NEG = -1,
})
M.TestAllTypesProto2_NestedEnum_descriptor.closed = true
M.TestAllTypesProto2_NestedEnum = M.TestAllTypesProto2_NestedEnum_descriptor.by_name

-- Enum: protobuf_test_messages.proto2.EnumOnlyProto2.Bool


@@ 32,6 34,7 @@ M.EnumOnlyProto2_Bool_descriptor = pb.enum("protobuf_test_messages.proto2.EnumOn
    kFalse = 0,
    kTrue = 1,
})
M.EnumOnlyProto2_Bool_descriptor.closed = true
M.EnumOnlyProto2_Bool = M.EnumOnlyProto2_Bool_descriptor.by_name

-- Enum: protobuf_test_messages.proto2.TestAllRequiredTypesProto2.NestedEnum


@@ 41,6 44,7 @@ M.TestAllRequiredTypesProto2_NestedEnum_descriptor = pb.enum("protobuf_test_mess
    BAZ = 2,
    NEG = -1,
})
M.TestAllRequiredTypesProto2_NestedEnum_descriptor.closed = true
M.TestAllRequiredTypesProto2_NestedEnum = M.TestAllRequiredTypesProto2_NestedEnum_descriptor.by_name

-- Pre-declare message descriptors so cross-references resolve.

M runtime/pb/text.lua => runtime/pb/text.lua +30 -5
@@ 1079,8 1079,15 @@ local function parse_enum_value(S, enum_desc)
        advance(S)
        return num
    end
    -- Numeric enum value: signed int32-shape.
    return parse_int_value(S, 'int32')
    -- Numeric enum value: signed int32-shape. Closed enums (proto2) reject
    -- numbers that don't map to a declared value; open enums (proto3) accept
    -- any int32 to preserve forward-compatibility on the wire.
    local n = parse_int_value(S, 'int32')
    if enum_desc.closed and enum_desc.by_value[n] == nil then
        err(S, ('unknown enum number %d for closed enum %s'):
            format(n, enum_desc.name))
    end
    return n
end

local function parse_scalar_value(S, proto_type)


@@ 1209,7 1216,7 @@ parse_value_for_field = function(S, f, depth)
    if f.kind == 'enum' then
        return parse_enum_value(S, f.enum)
    end
    if f.kind == 'message' then
    if f.kind == 'message' or f.kind == 'group' then
        return parse_message_field_value(S, f.message, depth)
    end
    if f.kind == 'map' then


@@ 1325,6 1332,24 @@ skip_field_entry = function(S, depth, desc, result, seen)
    if desc ~= nil then
        if field_name ~= nil then
            field = desc.field_by_name and desc.field_by_name[field_name]
            if field == nil then
                -- Proto2 group reference: text format uses the group's
                -- submessage simple name (e.g. `Data` or `MultiWordGroupField`)
                -- or its lowercase ASCII fold instead of the field name.
                -- The submessage name lookup is rare enough to do a linear
                -- sweep over fields rather than precomputing an index.
                local lc = field_name:lower()
                for _, gf in ipairs(desc.fields) do
                    if gf.kind == 'group' then
                        local label = gf.message and gf.message.name or ''
                        label = label:match('[^%.]+$') or label
                        if field_name == label or lc == label:lower() then
                            field = gf
                            break
                        end
                    end
                end
            end
        elseif numeric_id ~= nil then
            field = desc.field_by_id and desc.field_by_id[numeric_id]
        end


@@ 1353,7 1378,7 @@ skip_field_entry = function(S, depth, desc, result, seen)
    -- Map / message / list / scalar handling.
    local kind = field.kind
    -- `:` is required before scalar/enum, optional before message/map.
    if kind == 'message' or kind == 'map' then
    if kind == 'message' or kind == 'map' or kind == 'group' then
        accept_punct(S, ':')
    else
        expect_punct(S, ':')


@@ 1410,7 1435,7 @@ skip_field_entry = function(S, depth, desc, result, seen)
        end
        local v = parse_value_for_field(S, field, depth)
        clear_oneof_siblings(result, field)
        if kind == 'message' and result[field.name] ~= nil then
        if (kind == 'message' or kind == 'group') and result[field.name] ~= nil then
            -- text-format spec: repeated singular sub-messages merge. We
            -- approximate by shallow-merging fields; sufficient for the
            -- conformance corpus shapes.