~bigbes/tarantool

tarantool-protobuf

ref: 798e8db974f929988200e49ada81076b406a50bd tarantool-protobuf/test d---------
798e8db9 — Eugene Blikh 3 months ago
json,text: presence-tracked fields skip proto3 default elision

Both codecs gated default-value elision on `f.optional or f.oneof` —
the proto3 implicit-presence shape. That elides proto2 `required`
fields set to zero and any other presence-tracked descriptor that
doesn't carry the proto3 explicit-optional flag.

Extend the bypass to `f.required` so a Cardinality{r=0} survives
the round-trip through pb.json.encode / pb.text.encode. 4 new
luatest cases pin the rule.
4db0366a — Eugene Blikh 3 months ago
runtime: parser + dynamic accept proto2 sources

Parser now captures `required=true` and `default_value=…` from the
proto2 keywords (instead of dropping `required` silently and ignoring
field options). Dynamic descriptor builder reads `parsed.syntax`,
flips the repeated-scalar packing default for proto2, and calls
`codec.compile_writers/compile_readers` so the per-field
required-writer specialization actually fires — without that the
generic encode_field path silently elides missing required fields.

64-bit-int defaults are coerced into the appropriate cdata type
inside `coerce_default` so the codec's value-comparison rules
match what generated code emits.

Adds 7 dynamic-mode luatest cases including a static-vs-dynamic
byte-parity check. 725 tests pass.
4fbdb65d — Eugene Blikh 3 months ago
codegen: proto2 baseline — required, optional, custom defaults

Lifts the proto3-only syntax gate in the plugin and threads three
new field-descriptor attributes through codegen and the codec:

  * required=true   — fields declared with the proto2 `required` keyword.
                      Inline codegen and the runtime codec both error when
                      a required field is missing on encode (vs the silent
                      elide that proto3 implicit-presence fields get).
  * optional=true   — already wired for proto3 explicit `optional`; in
                      proto2 every singular field carries it via the
                      existing HasOptionalKeyword() check, giving presence
                      semantics without a separate emission path.
  * default_value=… — proto2 [default = X] from the field descriptor,
                      rendered as a Lua literal (cdata for 64-bit ints,
                      symbolic name for enums) so consumers can surface
                      it; the codec itself does not auto-materialize
                      defaults on decode, matching how proto3 absent
                      fields stay nil.

Packed-by-default already flips correctly because we ask
protoreflect's `IsPacked()`, which is syntax-aware.

Adds test/proto/proto2_basic.proto with 33 luatest cases covering
required validation, optional presence, custom defaults, the proto2
unpacked-by-default repeated rule, nested-required messages, and
full-vs-runtime mode parity. `just gen-proto2-tests` regenerates the
fixture into examples/expected/{full,runtime}/.

Out of scope: extensions, extend, group; conformance harness still
skips TestAllTypesProto2.
7676fdc2 — Eugene Blikh 3 months ago
codegen: preserve descriptor options as `options = { ... }`

Every populated *Options message — FileOptions, MessageOptions,
FieldOptions, OneofOptions, EnumOptions, EnumValueOptions,
ServiceOptions, MethodOptions — surfaces on the generated descriptor as
a plain Lua sub-table named `options` (or `oneof_options` /
`value_options` for the per-member shapes). Standard fields use their
proto name as a bare Lua key; extensions use their fully-qualified
extension name as a bracket-quoted string key.

The walker is generic — no extension-specific code paths. Consumers
pull whatever they care about: `(google.api.http)` for REST routing,
`(versionpb.etcd_version_*)` for compatibility gates, `[deprecated =
true]` for migration tooling, and any in-house extension without pb
knowing about them. Standard fields sort alphabetically before
extensions (also alphabetical by full name) so codegen output stays
byte-identical across runs.

The `options` key is only emitted when at least one field is populated,
so option-free protos produce zero-diff output to before. Resolver
re-links *Options* messages so in-file extensions surface via the
protoreflect walker — protogen builds f.Desc before in-file extensions
are registered, and only f.Proto gets the post-pass fix-up, so we
rebuild the resolver manually.

UninterpretedOption is treated as a codegen-time error: a populated
entry means protoc couldn't resolve the extension, and emitting
opaque parser state would hide the problem.
14e19819 — Eugene Blikh 3 months ago
codegen: bracket-quote Lua-keyword field names

The plugin emitted bare-identifier field names in three positions:
the `M.<Type>_fields` table key, the inline encoder's `v = t.<name>`
load, and the inline decoder's `result.<name> = ...` store. When a
proto field name collided with a Lua reserved word the generated
`*_pb.lua` failed to load with `'(' expected near '<keyword>'`.

The real-world hit is pprof's profile.proto, which declares
`repeated Function function = 5` — that breaks all three emit sites.

Route every user-named identifier through new luaTableKey /
luaFieldAccess helpers that bracket-quote reserved words. Covers
field_names + oneof descriptors, oneof presence pre-pass, inline
encode/decode field bodies (including repeated and map paths), and
optional has/clear accessors.
7e9e3e43 — Eugene Blikh 3 months ago
json: M.encode(desc, t, opts) for use_proto_names / emit_defaults / indent

Canonical protojson options were silently ignored. Plumb a single opts
table through encode_message via a CURRENT_ENCODE_OPTS state (mirroring
the decode-side CURRENT_OPTS):

  * use_proto_names         — emit snake_case field names (proto wire
                              names) instead of the spec-default
                              lowerCamelCase.
  * emit_defaults           — emit zero-valued implicit-presence scalars,
    (alias: always_emit_zero_value)  empty repeated lists, and empty
                              maps. Explicit-presence fields (optional /
                              oneof) and singular message fields remain
                              absent — matches protobuf-go's behavior.
  * indent                  — pretty-print with the given indent string;
                              empty arrays/objects stay compact.

Tests parameterize over both codegen modes (descriptor-table contract is
shared between full and runtime) plus a dedicated parity group that
asserts byte-identical JSON across modes for each option.
06b2978a — Eugene Blikh 3 months ago
codegen: resolve (tarantool.lua_package) via global type registry

The custom file option was being dropped silently: protoc encoded it
correctly into the FileDescriptorProto, but protobuf-go parked the
unknown extension in the message's unknown-fields tail because
E_LuaPackage was never registered with protoregistry.GlobalTypes. As a
result proto.GetExtension returned "" and every override the README
documented was a no-op — every caller fell through to the default
"<pkg>.<file>_pb" path resolution.

Register E_LuaPackage in init() and add a luatest regression that
asserts both the output path and the cross-file require strings honor
the option, parameterized over both codegen modes.
76140b72 — Eugene Blikh 3 months ago
json: emit unbroken base64 for bytes fields ({nowrap = true})

Canonical proto3 JSON expects RFC 4648 unwrapped base64, but Tarantool's
`digest.base64_encode` defaults to RFC 2045 MIME-style 76-char line
wrapping. Any `bytes` payload past ~57 bytes used to land in the JSON
string with an embedded `\n`, which breaks every spec-compliant
consumer (grpc-gateway, protojson, protobuf-go's JSON, ...).

Pass `{nowrap = true}` at the two encode sites: the per-field bytes
encoder and the `google.protobuf.Any` opaque-fallback `value` encoder.

Surfaced by tarantool-etcd's `TestJSONGatewayBytesUnwrapped` over Range
responses whose `value` is >=57 bytes; its reference grpc-gateway never
emits the wrapped form.
ab214a39 — Eugene Blikh 3 months ago
codegen: emit strict <Type>_fields / _oneofs constants for lazy view

Lazy-view callers passing a typo'd field name to :get / :has / :set /
:clear / :which got `nil` back, indistinguishable from a legitimately-
absent optional field. Failures surfaced as missing data downstream.

Each generated message now exports a M.<Type>_fields table mapping
each field name to itself (and M.<Type>_oneofs for oneof groups),
wrapped by a new pb.field_names() helper that errors on unknown-key
reads and on any write. Routing field-name arguments through these
tables turns a typo into a load-time error at the read site.

Eager _encode / _decode keep round-tripping plain Lua tables — the
constants table is a lazy-view contract, documented in
docs/api-modes.md. README and lazy_test.lua converted to the new
pattern; three new tests cover typo / read-only / oneof-typo errors.
428ee1f6 — Eugene Blikh 3 months ago
codegen: propagate proto comments into generated _pb.lua

Leading `//` comments on messages, enums, enum values, fields, services,
and RPC methods now surface in the generated Lua:

- Message / enum descriptions become `---` blocks above `---@class` /
  `---@alias`, so LuaLS shows them on hover.
- Per-field comments collapse onto a trailing `@ description` on the
  matching `---@field` line.
- Enum values, service banners, and per-method entries get plain `--`
  lines inside the table literals so readers without an LSP still see
  the proto-side context.

Covers both `mode=full` and `mode=runtime` (emission is mode-independent
but the new luatest group runs against both).
37a18dad — Eugene Blikh 3 months ago
json: strict validation pass closes the proto3 conformance suite

Six classes of relaxation that the proto3 JSON conformance corpus
flagged are now enforced. All as Recommended.* tests; combined with
the -0.0 codec fix this empties known_failures.txt and brings the
proto3 binary+JSON suite to 1493 ✓ / 1313 skipped / 0 expected
failures / 0 unexpected failures.

  1. Duplicate JSON keys. Tarantool's json.decode is hash-backed and
     silently collapses `{"foo":1,"foo":2}` to one entry. A small
     byte-walker `find_duplicate_json_keys` runs before json.decode,
     tracks per-object brace frames and key sets, errors on the
     second occurrence. Closes Recommended.FieldNameDuplicate.

  2. camelCase / snake_case aliases of the same proto field appearing
     side-by-side. Detected inside decode_message via a `field_seen`
     set keyed by proto-name; second hit errors. Closes
     FieldNameDuplicateDifferentCasing{1,2}.

  3. JSON null inside repeated arrays and map values. Previously
     silently dropped; now errors before decode_field_value. Closes
     RepeatedField{Message,Primitive}ElementIsNull and
     MapFieldValueIsNull.

  4. Unknown enum *names* (not integers). decode_enum used to return
     nil so callers silently dropped them; now raises by default and
     returns nil only when M.decode's `ignore_unknown_fields=true`
     opt is set. Conformance dispatch in cmd/conformance/core.lua
     forwards this flag when req.test_category ==
     JSON_IGNORE_UNKNOWN_PARSING_TEST. Closes
     RejectUnknownEnumStringValueIn{Optional,Repeated,Map} and the
     paired IgnoreUnknownEnumStringValueIn* tests.

  5. google.protobuf.NullValue JSON canonical form. The single enum
     value renders as the literal JSON `null` (not the string
     "NULL_VALUE"); decode accepts either, encode emits null. The
     decode_field_value null-handling path also treats a JSON null
     on a NullValue-typed field as "set" rather than "absent" so a
     oneof gets marked active. Closes
     NullValueInOtherOneof{New,Old}Format.Validator.

  6. FieldMask strict round-trip. Path validity is checked on both
     sides: the snake_case wire form rejects uppercase letters,
     consecutive underscores, trailing underscore, and underscore
     followed by anything other than a lowercase letter — these
     break the snake↔camel round-trip. The JSON form rejects any
     underscore in the input (must be lowerCamelCase). Closes
     FieldMask{TooManyUnderscore,PathsDontRoundTrip,
     NumbersDontRoundTrip}.JsonOutput and JsonInput.FieldMaskInvalidCharacter.

The pre-existing "drop unknown enum strings" unit regressions in
test/conformance_test.lua were inverted to assert the new error
shape. New strict-validation regressions in test/json_test.lua pin
all six categories so they don't regress; the `json.strict` group
runs across both codegen modes via the shared descriptor table.
f5c5ee6c — Eugene Blikh 3 months ago
codec: preserve -0.0 for proto3 float/double scalars

Proto3 default-elision dropped any singular float/double whose value
compared equal to 0 — `-0.0 == 0.0` in IEEE so the `if v ~= 0` guard
silently elided negative zero. The wire bytes for -0 differ from +0
and the TextFormatInput conformance corpus pins that -0 must survive
a round-trip; the failure surfaced as 10 unexpected text-suite
regressions covering FloatFieldNegativeZero (3 spellings × 2 outputs)
and Neg{Float,Double}FieldLargeNegativeExponentParsesAsNegZero
(2 types × 2 outputs).

Sign-bit guard via `1/v == math.huge` (positive zero yields +inf,
negative zero yields -inf). Applied in three layers:
  * runtime/pb/codec.lua: is_default_scalar + the specialized
    monomorphic writer for non-optional numeric scalars
  * runtime/pb/text.lua: is_proto3_default (text encoder elision)
  * inline codegen: scalarNotDefaultExpr emits the guard for
    float/double fields in full-mode `_encode` functions

Proto3 text-format conformance now sits at 416 ✓ / 18 skipped / 0
expected failures; binary+JSON holds at 1478 ✓. Unit-test regressions
cover both directions (preserved on encode + decode round-trip, +0
still elided) and use a runtime-computed -0.0 sentinel because LuaJIT
can constant-fold the literal `-0.0` to a sign-less zero in some
load paths.
7ec8f2b4 — Eugene Blikh 3 months ago
text: add pb.text.decode and wire it into conformance dispatch

Hand-written recursive-descent parser for the textproto grammar
covering every bucket the proto3 conformance suite exercises:
decimal/hex/octal integer literals with full 32/64-bit range checks,
float specials (inf/infinity/nan any case, oversize exponents
saturating to ±inf, underflows to ±0), C-style + \u/\U string escapes
with adjacent-literal concat and surrogate rejection, aggregate {} /
<> bodies, repeated short-form `[a, b, c]`, `key: K value: V` map
entries, the `[type.googleapis.com/...]` inline Any form alongside
the direct `type_url:`/`value:` form, enum-by-name-or-number,
reserved-name silent drop, numeric-field-ID tolerance, and
duplicate-singular-field rejection.

Plugin gains a small reserved_names emitter so the parser can match
mainline TextFormat::Parser's "silently drop reserved" rule. The Any
WKT descriptor advertises its real fields (type_url + string,
value + bytes) so the generic body walker can populate it directly
when the input doesn't use the inline-URL form.

cmd/conformance/core.lua stops short-circuiting text_payload to
`skipped` and runs it through pb.text.decode. The proto3 TextFormat
input suite climbs from 8 ✓ / 426 skipped to 406 ✓ / 18 skipped / 10
expected failures. The 10 surviving failures all share one cause
(proto3 -0.0 elision in the codec, not a parser bug — documented in
test/conformance/known_failures_text.txt). Binary+JSON conformance
holds at 1478 ✓. 591 unit tests pass across both codegen modes.

Closes the text-conformance-output branch.
343e4ba4 — Eugene Blikh 3 months ago
text: render captured unknown fields, tolerate SGROUP in skip

Two changes close the proto3 text-format conformance suite:

  1. `wire.skip_field` learns SGROUP/EGROUP. Wire 3 recurses through
     inner tags until a matching EGROUP, with field_id checked against
     the SGROUP's id. Callers (codec.lua, lazy.lua, wkt.lua, generated
     full-mode `_pb.lua`) now pass the tag's field_id so groups inside
     unknown-field skips don't error.

  2. `pb.text.encode` walks the captured `_unknown_fields` buffer when
     `opts.print_unknown_fields=true` and emits each entry in
     TextFormat numeric-field form:
       VARINT  -> "<id>: <uint64>"
       I64/I32 -> "<id>: 0x<hex>"
       LEN     -> speculative "<id> { <recurse> }"; rolls back to
                  byte-string form if the inner bytes don't parse as a
                  sub-message
       SGROUP  -> "<id> { <recurse> }" through matching EGROUP

`cmd/conformance/core.lua` threads `req.print_unknown_fields` into
`pb.text.encode` so `_Drop` tests drop unknowns and `_Print` tests
render them.

Conformance: text-format suite goes from 2 ✓ / 6 expected fails to
8 ✓ / 0 expected fails. All eight regression tests in
`conformance_test.lua` (one per upstream test, plus the fixed field-1011
tag bytes that were miscomputed earlier) now assert the target output.
b1d9c758 — Eugene Blikh 3 months ago
test: pin unknown-fields text-format conformance regressions

Adds eight tests under conformance.core mirroring the
Recommended.Proto3.ProtobufInput.*UnknownFields_*.TextFormatOutput tests
in the Google harness. Each sends the byte-exact upstream payload (field
IDs 1001..1011 from UnknownToTestAllTypes) through cmd/conformance/core
and pins current output, with the target assertion + needed runtime fix
documented in each failure message.

Two blockers surface:
  1. wire.skip_field rejects SGROUP/EGROUP (wire 3/4), so the four
     Group/Repeated tests fail at decode.
  2. pb.text.encode doesn't walk _unknown_fields, so the four *_Print
     tests serialize empty.

Lets us iterate on those fixes locally without the Docker round-trip.
39233ed5 — Eugene Blikh 3 months ago
text: wire pb.text into conformance runner TEXT_FORMAT output

The runner short-circuited every TEXT_FORMAT request to `skipped`, even
though pb.text.encode has been encode-capable since M7. Plug it into
cmd/conformance/core.lua so protobuf/JSON input → text output exercises
the existing encoder end-to-end. Text-format input remains deferred
(pb.text is encode-only).

Text-format suite: 0 ✓ / 430 skipped / 4 expected fails →
                   2 ✓ / 426 skipped / 6 expected fails (Scalar/Message
*_Print added — unknown-field rendering still missing).
e53089dd — Eugene Blikh 3 months ago
json: strict proto3 JSON conformance — close all Required failures

The conformance suite's Required JSON failures collapse to zero with
this pass. Local tests grow from 456 to 497 to pin every fix.

Decode side — `runtime/pb/json.lua`:
  * Strict scalar validators per type. Numeric strings must match the
    JSON number grammar (no leading whitespace, no partial numerics);
    out-of-range values, NaN/Inf surrogates from JSON literals, and
    type mismatches all become parse_error.
  * Quoted exponential ints ("1e5" -> 100000) are accepted via the
    JSON-number grammar path, matching Int32FieldQuotedExponentialValue.
  * Timestamp parser: strict RFC 3339 (uppercase T/Z, ±HH:MM offset,
    ≤9 frac digits, range check). Output uses a portable Hinnant-style
    epoch_to_ymdhms so year 0001 zero-pads correctly — glibc's POSIX
    %Y emits "1" for that year, breaking round-trip.
  * Duration parser/formatter: mandatory `s` suffix, ±10000-year range,
    sign-matching nanos, 0/3/6/9-digit fractional output.
  * Any: WKT-aware nesting under "value"; empty Any -> {}; Empty WKT
    inside Any omits "value" (reference parser rejects {"value":{}});
    @type URL without `/` rejected; empty @type with sibling fields
    rejected.
  * Reject NaN/Infinity in google.protobuf.Value.number_value (no JSON
    literal for these).
  * Duplicate oneof branches rejected; a null oneof branch does NOT
    count as set, so a sibling non-null branch is unambiguous.
  * Top-level JSON null rejected for messages; preserved for Value.
  * Repeated/map values must be JSON array/object (not bare scalar).

Encode side — hand-rolled JSON emitter:
  Tarantool's `json.encode` uses a fixed global precision so doubles
  like 0.1 don't round-trip and we can't change it per-value without
  polluting other users. Replace with a minimal emitter that picks the
  shortest-round-tripping precision (15 -> 16 -> 17) per double and
  handles NaN/Inf as quoted sentinel strings.

`runtime/pb/wkt.lua`: `timestamp_decode` now keeps invalid Timestamps as
a raw {seconds, nanos} table when `datetime.new` rejects them (negative
nanos, year > 9999, ...) so the JSON encoder can produce
serialize_error rather than the binary decoder raising parse_error.
Required by the Timestamp conformance suite.

`test/conformance_test.lua`: 41 new regression tests grouped under
Fixes 12-19, pinning every code path touched. Strict scalar
validators (one per rejection shape per type), Timestamp/Duration
strict parsing and canonical output, Any WKT/non-WKT/Empty handling,
Value NaN/Inf rejection, ValueAcceptNull round-trip, LuaJIT
NaN-boxing collision cases (0x7FFBCBA987654321, all-ones), shortest
double round-trip, oneof-null semantics.

`test/conformance/known_failures.txt`: refreshed. 15 Recommended-only
failures remain (FieldMask round-trip quirks, duplicate-field-name
detection, unknown-enum-string rejection, null-element-in-list,
NullValue oneof validator).
37c56322 — Eugene Blikh 3 months ago
json: enable conformance JSON output + close 459 tests

Three coupled changes that turn the JSON output path on for the
conformance harness:

1. string_to_int64 accepts cdata: Tarantool's json.decode parses raw
   JSON integer literals outside double range as int64_t/uint64_t cdata,
   not Lua numbers. The decoder errored "expected JSON string or number
   for int64" on any unquoted 64-bit value (Int64FieldMaxValueNotQuoted
   et al). cdata is now cast through directly, preserving precision.

2. encode_message marks output as a map: an empty proto3 message
   serialized as `[]` because Tarantool's json defaults empty tables to
   array shape. jsoncpp's strict comparator threw Json::LogicError and
   aborted the whole suite. Setting __serialize='map' on the output
   gives `{}` and unblocks all JsonOutput tests.

3. PB_CONFORMANCE_SKIP_JSON gate is opt-in by default. core.lua now
   matches exactly "1" (so docker -e VAR= disables it), and the
   Dockerfile no longer hard-codes "=1" — JSON output runs end-to-end
   for everyone unless they re-enable the gate.

Conformance moves from 930 / 1869 / 11 to 1389 / 1313 / 79
(successes / skipped / expected fails). The 75 new expected fails
are canonical-form edge cases (Duration formatting sign handling,
Timestamp out-of-range rejection, double precision digits, NaN
canonicalization, JSON-input strict rejection) — left for a follow-up.

Drops 7 entries from test/conformance/known_failures.txt that this
change closes; adds 75 newly-visible ones.
b1273f1b — Eugene Blikh 3 months ago
json: canonical lowerCamelCase + NullValue WKT descriptor

Two unrelated JSON-decoder gaps captured under the conformance triage:

1. to_camel's gsub pattern '_(%w)' didn't match consecutive underscores
   and didn't drop trailing underscores, so proto names like
   __field_name13 / field__name4_ / field_name17__ generated JSON keys
   that didn't match what protoc produces. The fix strips trailing _+
   and collapses '_+%w' to a capitalized letter; a leading underscore
   thus capitalizes the next character, matching the spec.

2. pb.wkt didn't export a descriptor for google.protobuf.NullValue, so
   any enum field whose type is NullValue (oneof_null_value, or the
   implicit one inside Value) crashed decode_enum with "attempt to
   index a nil value." Adds a minimal {by_name, by_value} descriptor.

Drops 3 entries from test/conformance/known_failures.txt
(FieldNameInSnakeCase, FieldNameWithDoubleUnderscores,
NullValueInOtherOneofOldFormat). Adds 4 regression tests covering
double underscore, leading underscore, trailing underscore, and the
NullValue enum decode path.
96ed328e — Eugene Blikh 3 months ago
json: treat null fields as absent (and Value's null as a real value)

Per the proto3 JSON spec, a null on any field means "use the field's
default" — encoded as missing — with the lone exception of
google.protobuf.Value, where JSON null is itself a Value carrying
NullValue.NULL_VALUE.

Three coupled bugs surfaced together:

1. decode_field_value used to fall through with v = box.NULL, leaving a
   useless box.NULL sitting in the result table for scalars. Now it
   returns nil for non-Value fields, PB_NULL for Value fields.

2. decode_message's repeated and map branches called `#jv` and
   `pairs(jv)` unconditionally; a JSON-null on either type crashed with
   "attempt to get length of 'void *'". Now both branches short-circuit
   when jv is box.NULL.

3. The nil-skip checks in the decode loop (`if dv ~= nil`) and in the
   codec / inline message encoders (`if v == nil then return end`)
   evaluated TRUE on box.NULL because Tarantool's cdata __eq aliases
   it to nil. Decode now uses rawequal(dv, nil); encode special-cases
   message kind by also accepting cdata, so the Value field's
   box.NULL sentinel survives all the way through to value_encode.

Drops 3 entries from test/conformance/known_failures.txt
(AllFieldAcceptNull, WrapperTypesWithNullValue, ValueAcceptNull).
Adds 5 regression tests covering scalar / repeated / map / wrapper
null treatment and the Value-NULL_VALUE exception.
Next