diff --git a/protobuf_serialization/internal.nim b/protobuf_serialization/internal.nim index d281cca..3275673 100644 --- a/protobuf_serialization/internal.nim +++ b/protobuf_serialization/internal.nim @@ -4,6 +4,7 @@ import std/[options, sets, tables] import stew/shims/macros +from std/strutils import parseEnum #Depending on the situation, one of these two are used. #Sometimes, one works where the other doesn't. #It all comes down to bugs in Nim and managing them. @@ -56,6 +57,24 @@ proc fieldNumberOf*(T: type, fieldName: static string): int {.compileTime.} = else: fieldNum +proc isOneof*(T: type, fieldName: static string): bool {.compileTime.} = + T.hasCustomPragmaFixed(fieldName, oneof) + +template setOneof*(obj: object, headerNum: int): untyped = + type T = typeof(obj) + for fieldName, fieldVar in fieldPairs(obj): + when fieldName == "kind": + enumInstanceSerializedFields(obj, fieldName2, fieldVar2): + if headerNum == T.fieldNumberOf(fieldName2): + try: + fieldVar = parseEnum[typeof(fieldVar)](fieldName2) + except ValueError: + raiseAssert "unexpected oneof field " & fieldName2 + else: + # XXX skip dontSerialize + if headerNum != T.fieldNumberOf(fieldName): + fieldVar = default(typeof(fieldVar)) + template tableObject*(TableObject, K, V) = when K is SomePBInt and V is SomePBInt: type diff --git a/protobuf_serialization/reader.nim b/protobuf_serialization/reader.nim index 016a4ce..f7852af 100644 --- a/protobuf_serialization/reader.nim +++ b/protobuf_serialization/reader.nim @@ -11,6 +11,8 @@ import serialization, "."/[codec, internal, types] +from std/strutils import parseEnum + export inputs, serialization, codec, types proc readValueInternal[T: object](stream: InputStream, value: var T, silent: bool = false) {.raises: [SerializationError, IOError].} @@ -142,18 +144,46 @@ proc readValueInternal[T: object](stream: InputStream, value: var T, silent: boo while stream.readable(): let header = stream.readHeader() + let headerNum = header.number() var i = -1 var knownField = false - if not header.number().validFieldNumber(true): - raise newException(ProtobufReadError, "Invalid field number: " & $header.number()) + if not headerNum.validFieldNumber(true): + raise newException(ProtobufReadError, "Invalid field number: " & $headerNum) + + for fieldNameX, fieldVarX in fieldPairs(value): + when T.isOneof(fieldNameX): + type TT = typeof(fieldVarX) + enumInstanceSerializedFields(fieldVarX, fieldName, fieldVar): + const fieldNum = TT.fieldNumberOf(fieldName) + if headerNum == fieldNum: + protoType(ProtoType, TT, typeof(fieldVar), fieldName) + # TODO should we allow reading packed fields into non-repeated fields? + when ProtoType is SomePrimitive and fieldVar is seq and fieldVar isnot seq[byte]: + if header.kind() == WireKind.LengthDelim: + knownField = true + stream.readFieldPackedInto(fieldVar, header, ProtoType) + elif header.kind() == wireKind(ProtoType): + knownField = true + stream.readFieldInto(fieldVar, header, ProtoType) + elif typeof(fieldVar) is ref and defined(ConformanceTest): + if header.kind() == wireKind(ProtoType): + knownField = true + fieldVar = new typeof(fieldVar) + stream.readFieldInto(fieldVar[], header, ProtoType) + else: + if header.kind() == wireKind(ProtoType): + knownField = true + stream.readFieldInto(fieldVar, header, ProtoType) + if knownField: + setOneof(fieldVarX, headerNum) enumInstanceSerializedFields(value, fieldName, fieldVar): inc i const fieldNum = T.fieldNumberOf(fieldName) - if header.number() == fieldNum: + if headerNum == fieldNum: protoType(ProtoType, T, typeof(fieldVar), fieldName) # TODO should we allow reading packed fields into non-repeated fields? when ProtoType is SomePrimitive and fieldVar is seq and fieldVar isnot seq[byte]: diff --git a/protobuf_serialization/types.nim b/protobuf_serialization/types.nim index a406efa..ec57371 100644 --- a/protobuf_serialization/types.nim +++ b/protobuf_serialization/types.nim @@ -43,6 +43,7 @@ template packed*(v: bool) {.pragma.} template pint*() {.pragma.} # encode as `intXX` template sint*() {.pragma.} # encode as `sintXX` template fixed*() {.pragma.} # encode as `fixedXX` +template oneof*() {.pragma.} func init*( T: type ProtobufWriter, diff --git a/tests/test_oneof.nim b/tests/test_oneof.nim new file mode 100644 index 0000000..42750b7 --- /dev/null +++ b/tests/test_oneof.nim @@ -0,0 +1,47 @@ +# nim-protobuf-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import unittest2 +import stew/byteutils +import ../protobuf_serialization + +type + OneOfKind {.pure.} = enum + unset + x + y + OneOf {.proto3.} = object + kind {.dontSerialize.}: OneOfKind + x {.fieldNumber: 1, pint.}: int64 + y {.fieldNumber: 2, pint.}: int64 + OneOfObj {.proto3.} = object + one {.oneof, dontSerialize.}: OneOf + +suite "Test oneof": + test "oneof unset": + let encoded = "".hexToSeqByte + check Protobuf.decode(encoded, OneOfObj) == OneOfObj(one: OneOf(kind: OneOfKind.unset)) + + test "oneof field 1 set": + # echo 'x: 1' | protoc --encode=OneOfObj test_oneof.proto | hexdump -ve '1/1 "%.2x"' + # 0801 + let encoded = "0801".hexToSeqByte + check Protobuf.decode(encoded, OneOfObj) == OneOfObj(one: OneOf(kind: OneOfKind.x, x: 1)) + + test "oneof field 2 set": + let encoded = "1001".hexToSeqByte + check Protobuf.decode(encoded, OneOfObj) == OneOfObj(one: OneOf(kind: OneOfKind.y, y: 1)) + + test "oneof field 1 and 2 set": + let encoded = "08011001".hexToSeqByte + check Protobuf.decode(encoded, OneOfObj) == OneOfObj(one: OneOf(kind: OneOfKind.y, y: 1)) + + test "oneof field 1 and 2 set variant": + let encoded = "10010801".hexToSeqByte + check Protobuf.decode(encoded, OneOfObj) == OneOfObj(one: OneOf(kind: OneOfKind.x, x: 1)) diff --git a/tests/test_oneof.proto b/tests/test_oneof.proto new file mode 100644 index 0000000..63ff930 --- /dev/null +++ b/tests/test_oneof.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +message OneOfObj { + int64 x = 1; + int64 y = 2; +}