protobuf: add oneof combinator
Protobuf [oneof] groups a set of mutually exclusive optional fields at
distinct tags. Encoding emits whichever case matches; decoding picks
the case with the highest wire-order sequence (protobuf "last wins").
API:
val case : int -> 'a t -> inject:('a -> 'b) -> extract:('b -> 'a option)
-> 'b case
val oneof : default:'a -> ('o -> 'a) -> 'a case list -> ('o, 'a) field
Typical usage lifts the oneof alternatives into an OCaml polymorphic
variant:
type payload = [ `None | `Text of string | `Num of int32 ]
let msg_codec =
finish
(let* payload =
oneof ~default:`None (fun r -> r.payload)
[ case 1 string ~inject:(fun s -> `Text s)
~extract:(function `Text s -> Some s | _ -> None);
case 2 int32 ~inject:(fun n -> `Num n)
~extract:(function `Num n -> Some n | _ -> None) ] in
return { payload })
Internals:
- [parse_wire] now stamps each wire entry with a sequence counter so
[take_oneof_last] can find the case whose tag came last in wire
order. Hashtbl buckets still record per-tag wire order (reversed,
prepend-on-insert); the counter adds cross-tag ordering.
- [decode_fields] handles the new [Oneof] GADT constructor.
- [encode_fields] iterates the case list, picks the first whose
[extract] returns [Some], and emits that tag. If every extractor
returns [None] (e.g. value is the default variant), no wire bytes
are written -- matching protoc's behaviour for unset oneofs.
- [take_oneof_last] consumes every case tag from the table on exit
so oneof fields don't leak into the unknown-fields bag.
Tests: roundtrip through each case variant, empty-wire for the
default variant, and a "last wins" test where three consecutive
oneof tags appear on the wire and the decoder picks the third.
All 53 unit + 17 fuzz + 2 protoc interop tests pass.