writer.nim
  1  #Writes the specified type into a buffer using the Protobuf binary wire format.
  2  
  3  {.push raises: [], gcsafe.}
  4  
  5  import
  6    std/[typetraits, tables],
  7    stew/shims/macros,
  8    stew/objects,
  9    faststreams/outputs,
 10    serialization,
 11    "."/[codec, internal, sizer, types]
 12  
 13  export outputs, serialization, codec, types
 14  
 15  proc writeObject[T: object](stream: OutputStream, value: T) {.raises: [IOError].}
 16  
 17  proc writeField*(
 18      stream: OutputStream, fieldNum: int, fieldVal: auto,
 19      ProtoType: type UnsupportedType, _: static bool = false) {.raises: [IOError].} =
 20    # TODO turn this into an extension point
 21    unsupportedProtoType ProtoType.FieldType, ProtoType.RootType, ProtoType.fieldName
 22  
 23  proc writeField*[T: object and not PBOption and not Table](
 24      stream: OutputStream, fieldNum: int, fieldVal: T, ProtoType: type pbytes,
 25      skipDefault: static bool = false) {.raises: [IOError].} =
 26    let
 27      size = computeObjectSize(fieldVal)
 28  
 29    when skipDefault:
 30      if size == 0:
 31        return
 32  
 33    stream.writeValue(FieldHeader.init(fieldNum, ProtoType.wireKind()))
 34    stream.writeValue(puint64(size))
 35    stream.writeObject(fieldVal)
 36  
 37  proc writeField*[T: not object and not enum](
 38      stream: OutputStream, fieldNum: int, fieldVal: T,
 39      ProtoType: type SomeScalar, skipDefault: static bool = false) {.raises: [IOError].} =
 40    when skipDefault:
 41      const def = default(typeof(fieldVal))
 42      if fieldVal == def:
 43        return
 44  
 45    stream.writeField(fieldNum, ProtoType(fieldVal))
 46  
 47  proc writeField*(
 48      stream: OutputStream, fieldNum: int, fieldVal: PBOption, ProtoType: type,
 49      skipDefault: static bool = false) {.raises: [IOError].} =
 50    if fieldVal.isSome():
 51      stream.writeField(fieldNum, fieldVal.get(), ProtoType, skipDefault)
 52  
 53  proc writeFieldPacked*[T: not byte, ProtoType: SomePrimitive](
 54      output: OutputStream, field: int, values: openArray[T], _: type ProtoType) {.raises: [IOError].} =
 55    # Packed encoding uses a length-delimited field byte length of the sum of the
 56    # byte lengths of each field followed by the header-free contents
 57    output.write(
 58      toBytes(FieldHeader.init(field, WireKind.LengthDelim)))
 59  
 60    const canCopyMem =
 61      ProtoType is SomeFixed32 or ProtoType is SomeFixed64 or ProtoType is pbool
 62    let
 63      dataSize = computeSizePacked(values, ProtoType)
 64    output.write(toBytes(puint64(dataSize)))
 65  
 66    when canCopyMem:
 67      if values.len > 0:
 68        output.write(
 69          cast[ptr UncheckedArray[byte]](
 70            unsafeAddr values[0]).toOpenArray(0, dataSize - 1))
 71    else:
 72      for value in values:
 73        output.write(toBytes(ProtoType(value)))
 74  
 75  when defined(ConformanceTest):
 76    proc writeField[T: enum](
 77        stream: OutputStream,
 78        fieldNum: int,
 79        fieldVal: T,
 80        ProtoType: type,
 81        skipDefault: static bool = false
 82    ) {.raises: [IOError].} =
 83      when 0 notin T:
 84        {.fatal: $T & " definition must contain a constant that maps to zero".}
 85      stream.writeField(fieldNum, pint32(fieldVal.ord()))
 86  
 87    proc writeField[K, V](
 88      stream: OutputStream,
 89      fieldNum: int,
 90      value: Table[K, V],
 91      ProtoType: type,
 92      skipDefault: static bool = false
 93    ) {.raises: [IOError].} =
 94      tableObject(TableObject, K, V)
 95      for k, v in value.pairs():
 96        let tmp = TableObject(key: k, value: v)
 97        stream.writeField(fieldNum, tmp, ProtoType)
 98  
 99  proc writeObject[T: object](stream: OutputStream, value: T) {.raises: [IOError].} =
100    const
101      isProto2: bool = T.isProto2()
102      isProto3: bool = T.isProto3()
103    static: doAssert isProto2 xor isProto3
104  
105    enumInstanceSerializedFields(value, fieldName, fieldVal):
106      const
107        fieldNum = T.fieldNumberOf(fieldName)
108  
109      type
110        FlatType = flatType(fieldVal)
111  
112      protoType(ProtoType, T, FlatType, fieldName)
113  
114      when FlatType is seq and FlatType isnot seq[byte]:
115        const
116          isPacked = T.isPacked(fieldName).get(isProto3)
117        when isPacked and ProtoType is SomePrimitive:
118          stream.writeFieldPacked(fieldNum, fieldVal, ProtoType)
119        else:
120          for i in 0..<fieldVal.len:
121            # don't skip defaults so as to preserve length
122            stream.writeField(fieldNum, fieldVal[i], ProtoType, false)
123  
124      elif FlatType is ref and defined(ConformanceTest):
125        if not fieldVal.isNil():
126          stream.writeField(fieldNum, fieldVal[], ProtoType)
127      else:
128        stream.writeField(fieldNum, fieldVal, ProtoType, isProto3)
129  
130  proc writeValue*[T: object](writer: ProtobufWriter, value: T) {.raises: [IOError].} =
131    static: verifySerializable(T)
132  
133    if ProtobufFlags.VarIntLengthPrefix in writer.flags:
134      let
135        size = computeObjectSize(value)
136      writer.stream.writeValue(puint64(size))
137  
138    writer.stream.writeObject(value)