mirror of
https://github.com/logos-storage/nim-serde.git
synced 2026-01-03 14:13:07 +00:00
feat: add CBOR serialization and deserialization support
This commit is contained in:
parent
dcfec8aaa3
commit
f1ec805ec0
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,4 +5,5 @@ nimble.develop
|
||||
nimble.paths
|
||||
.idea
|
||||
vendor/
|
||||
.vscode/
|
||||
.vscode/
|
||||
nimbledeps
|
||||
@ -9,7 +9,7 @@ skipDirs = @["tests"]
|
||||
# Dependencies
|
||||
requires "nim >= 1.6.14"
|
||||
requires "chronicles >= 0.10.3 & < 0.11.0"
|
||||
requires "questionable >= 0.10.13 & < 0.11.0"
|
||||
requires "questionable >= 0.10.15"
|
||||
requires "stint"
|
||||
requires "stew"
|
||||
|
||||
|
||||
7
serde/cbor.nim
Normal file
7
serde/cbor.nim
Normal file
@ -0,0 +1,7 @@
|
||||
import ./cbor/serializer
|
||||
import ./cbor/deserializer
|
||||
import ./cbor/jsonhook
|
||||
import ./cbor/types as ctypes
|
||||
import ./utils/types
|
||||
import ./utils/errors
|
||||
export serializer, deserializer, ctypes, types, errors, jsonhook
|
||||
761
serde/cbor/deserializer.nim
Normal file
761
serde/cbor/deserializer.nim
Normal file
@ -0,0 +1,761 @@
|
||||
import std/[math, streams, options, tables, strutils, times, typetraits]
|
||||
import ./types
|
||||
import ./helpers
|
||||
import ../utils/types
|
||||
import ./errors
|
||||
import pkg/questionable
|
||||
import pkg/questionable/results
|
||||
|
||||
export results
|
||||
export types
|
||||
|
||||
|
||||
func isIndefinite*(c: CborParser): bool {.inline.} = c.minor == 31
|
||||
## Return true if the parser is positioned on an item of indefinite length.
|
||||
|
||||
{.push raises: [].}
|
||||
proc open*(c: var CborParser; s: Stream) =
|
||||
## Begin parsing a stream of CBOR in binary form.
|
||||
## The parser will be initialized in an EOF state, call
|
||||
## ``next`` to advance it before parsing.
|
||||
c.s = s
|
||||
c.kind = cborEof
|
||||
c.intVal = 0
|
||||
|
||||
proc next*(c: var CborParser): ?!void =
|
||||
## Advance the parser to the initial or next event.
|
||||
try:
|
||||
if c.s.atEnd:
|
||||
c.kind = CborEventKind.cborEof
|
||||
c.intVal = 0
|
||||
else:
|
||||
let
|
||||
ib = c.s.readUint8
|
||||
mb = ib shr 5
|
||||
c.minor = ib and 0b11111
|
||||
case c.minor
|
||||
of 0..23:
|
||||
c.intVal = c.minor.uint64
|
||||
of 24:
|
||||
c.intVal = c.s.readChar.uint64
|
||||
of 25:
|
||||
c.intVal = c.s.readChar.uint64
|
||||
c.intVal = (c.intVal shl 8) or c.s.readChar.uint64
|
||||
of 26:
|
||||
c.intVal = c.s.readChar.uint64
|
||||
for _ in 1..3:
|
||||
{.unroll.}
|
||||
c.intVal = (c.intVal shl 8) or c.s.readChar.uint64
|
||||
of 27:
|
||||
c.intVal = c.s.readChar.uint64
|
||||
for _ in 1..7:
|
||||
{.unroll.}
|
||||
c.intVal = (c.intVal shl 8) or c.s.readChar.uint64
|
||||
else:
|
||||
c.intVal = 0
|
||||
case mb
|
||||
of PositiveMajor:
|
||||
c.kind = CborEventKind.cborPositive
|
||||
of NegativeMajor:
|
||||
c.kind = CborEventKind.cborNegative
|
||||
of BytesMajor:
|
||||
c.kind = CborEventKind.cborBytes
|
||||
of TextMajor:
|
||||
c.kind = CborEventKind.cborText
|
||||
of ArrayMajor:
|
||||
c.kind = CborEventKind.cborArray
|
||||
of MapMajor:
|
||||
c.kind = CborEventKind.cborMap
|
||||
of TagMajor:
|
||||
c.kind = CborEventKind.cborTag
|
||||
of SimpleMajor:
|
||||
if c.minor in {25, 26, 27}:
|
||||
c.kind = CborEventKind.cborFloat
|
||||
elif c.isIndefinite:
|
||||
c.kind = CborEventKind.cborBreak
|
||||
else:
|
||||
c.kind = CborEventKind.cborSimple
|
||||
else:
|
||||
return failure(newCborError("unhandled major type " & $mb))
|
||||
success()
|
||||
except IOError as e:
|
||||
return failure(e)
|
||||
except OSError as e:
|
||||
return failure(e)
|
||||
|
||||
proc nextUInt*(c: var CborParser): ?!BiggestUInt =
|
||||
## Parse the integer value that the parser is positioned on.
|
||||
if c.kind != CborEventKind.cborPositive:
|
||||
return failure(newCborError("Expected positive integer, got " & $c.kind))
|
||||
let val = c.intVal.BiggestUInt
|
||||
let nextRes = c.next()
|
||||
if nextRes.isFailure:
|
||||
return failure(nextRes.error)
|
||||
return success(val)
|
||||
|
||||
|
||||
proc nextInt*(c: var CborParser): ?!BiggestInt =
|
||||
## Parse the integer value that the parser is positioned on.
|
||||
var val: BiggestInt
|
||||
case c.kind
|
||||
of CborEventKind.cborPositive:
|
||||
val = c.intVal.BiggestInt
|
||||
of CborEventKind.cborNegative:
|
||||
val = -1.BiggestInt - c.intVal.BiggestInt
|
||||
else:
|
||||
return failure(newCborError("Expected integer, got " & $c.kind))
|
||||
|
||||
let nextRes = c.next()
|
||||
if nextRes.isFailure:
|
||||
return failure(nextRes.error)
|
||||
|
||||
return success(val)
|
||||
|
||||
proc nextFloat*(c: var CborParser): ?!float64 =
|
||||
## Parse the float value that the parser is positioned on.
|
||||
var val: float64
|
||||
if c.kind != CborEventKind.cborFloat:
|
||||
return failure(newCborError("Expected float, got " & $c.kind))
|
||||
case c.minor
|
||||
of 25:
|
||||
val = floatSingle(c.intVal.uint16).float64
|
||||
of 26:
|
||||
val = cast[float32](c.intVal).float64
|
||||
of 27:
|
||||
val = cast[float64](c.intVal)
|
||||
else:
|
||||
discard
|
||||
|
||||
let nextRes = c.next()
|
||||
if nextRes.isFailure:
|
||||
return failure(nextRes.error)
|
||||
|
||||
return success(val)
|
||||
|
||||
func bytesLen*(c: CborParser): ?!int =
|
||||
## Return the length of the byte string that the parser is positioned on.
|
||||
if c.kind != CborEventKind.cborBytes:
|
||||
return failure(newCborError("Expected bytes, got " & $c.kind))
|
||||
return success(c.intVal.int)
|
||||
|
||||
template tryNext(c: var CborParser) =
|
||||
let nextRes = c.next()
|
||||
if nextRes.isFailure:
|
||||
return failure(nextRes.error)
|
||||
|
||||
template trySkip(c: var CborParser) =
|
||||
let skipRes = c.skipNode()
|
||||
if skipRes.isFailure:
|
||||
return failure(skipRes.error)
|
||||
|
||||
proc nextBytes*(c: var CborParser; buf: var openArray[byte]): ?!void =
|
||||
## Read the bytes that the parser is positioned on and advance.
|
||||
try:
|
||||
if c.kind != CborEventKind.cborBytes:
|
||||
return failure(newCborError("Expected bytes, got " & $c.kind))
|
||||
if buf.len != c.intVal.int:
|
||||
return failure(newCborError("Buffer length mismatch: expected " &
|
||||
$c.intVal.int & ", got " & $buf.len))
|
||||
if buf.len > 0:
|
||||
let n = c.s.readData(buf[0].addr, buf.len)
|
||||
if n != buf.len:
|
||||
return failure(newCborError("truncated read of CBOR data"))
|
||||
tryNext(c)
|
||||
success()
|
||||
except OSError as e:
|
||||
return failure(e.msg)
|
||||
except IOError as e:
|
||||
return failure(e.msg)
|
||||
|
||||
proc nextBytes*(c: var CborParser): ?!seq[byte] =
|
||||
## Read the bytes that the parser is positioned on into a seq and advance.
|
||||
var val = newSeq[byte](c.intVal.int)
|
||||
let nextRes = nextBytes(c, val)
|
||||
if nextRes.isFailure:
|
||||
return failure(nextRes.error)
|
||||
|
||||
return success(val)
|
||||
|
||||
func textLen*(c: CborParser): ?!int =
|
||||
## Return the length of the text that the parser is positioned on.
|
||||
if c.kind != CborEventKind.cborText:
|
||||
return failure(newCborError("Expected text, got " & $c.kind))
|
||||
return success(c.intVal.int)
|
||||
|
||||
proc nextText*(c: var CborParser; buf: var string): ?!void =
|
||||
## Read the text that the parser is positioned on into a string and advance.
|
||||
try:
|
||||
if c.kind != CborEventKind.cborText:
|
||||
return failure(newCborError("Expected text, got " & $c.kind))
|
||||
buf.setLen c.intVal.int
|
||||
if buf.len > 0:
|
||||
let n = c.s.readData(buf[0].addr, buf.len)
|
||||
if n != buf.len:
|
||||
return failure(newCborError("truncated read of CBOR data"))
|
||||
tryNext(c)
|
||||
success()
|
||||
except IOError as e:
|
||||
return failure(e.msg)
|
||||
except OSError as e:
|
||||
return failure(e.msg)
|
||||
|
||||
|
||||
proc nextText*(c: var CborParser): ?!string =
|
||||
## Read the text that the parser is positioned on into a string and advance.
|
||||
var buf: string
|
||||
let nextRes = nextText(c, buf)
|
||||
if nextRes.isFailure:
|
||||
return failure(nextRes.error)
|
||||
|
||||
return success(buf)
|
||||
|
||||
func arrayLen*(c: CborParser): int =
|
||||
## Return the length of the array that the parser is positioned on.
|
||||
assert(c.kind == CborEventKind.cborArray, $c.kind)
|
||||
c.intVal.int
|
||||
|
||||
func mapLen*(c: CborParser): int =
|
||||
## Return the length of the map that the parser is positioned on.
|
||||
assert(c.kind == CborEventKind.cborMap, $c.kind)
|
||||
c.intVal.int
|
||||
|
||||
func tag*(c: CborParser): uint64 =
|
||||
## Return the tag value the parser is positioned on.
|
||||
assert(c.kind == CborEventKind.cborTag, $c.kind)
|
||||
c.intVal
|
||||
|
||||
proc skipNode*(c: var CborParser): ?!void =
|
||||
## Skip the item the parser is positioned on.
|
||||
try:
|
||||
case c.kind
|
||||
of CborEventKind.cborEof:
|
||||
return failure(newCborError("end of CBOR stream"))
|
||||
of CborEventKind.cborPositive, CborEventKind.cborNegative,
|
||||
CborEventKind.cborSimple:
|
||||
return c.next()
|
||||
of CborEventKind.cborBytes, CborEventKind.cborText:
|
||||
if c.isIndefinite:
|
||||
tryNext(c)
|
||||
while c.kind != CborEventKind.cborBreak:
|
||||
if c.kind != CborEventKind.cborBytes:
|
||||
return failure(newCborError("expected bytes, got " & $c.kind))
|
||||
for _ in 1..c.intVal.int: discard readChar(c.s)
|
||||
return c.next()
|
||||
else:
|
||||
for _ in 1..c.intVal.int: discard readChar(c.s)
|
||||
return c.next()
|
||||
of CborEventKind.cborArray:
|
||||
if c.isIndefinite:
|
||||
tryNext(c)
|
||||
while c.kind != CborEventKind.cborBreak:
|
||||
trySkip(c)
|
||||
return c.next()
|
||||
else:
|
||||
let len = c.intVal
|
||||
tryNext(c)
|
||||
for i in 1..len:
|
||||
trySkip(c)
|
||||
of CborEventKind.cborMap:
|
||||
let mapLen = c.intVal.int
|
||||
if c.isIndefinite:
|
||||
tryNext(c)
|
||||
while c.kind != CborEventKind.cborBreak:
|
||||
trySkip(c)
|
||||
return c.next()
|
||||
else:
|
||||
tryNext(c)
|
||||
for _ in 1 .. mapLen:
|
||||
trySkip(c)
|
||||
of CborEventKind.cborTag:
|
||||
tryNext(c)
|
||||
return c.skipNode()
|
||||
of CborEventKind.cborFloat:
|
||||
without f =? c.nextFloat(), error:
|
||||
return failure(error)
|
||||
of CborEventKind.cborBreak:
|
||||
discard
|
||||
success()
|
||||
except OSError as e:
|
||||
return failure(e.msg)
|
||||
except IOError as e:
|
||||
return failure(e.msg)
|
||||
|
||||
|
||||
|
||||
proc nextNode*(c: var CborParser): ?!CborNode =
|
||||
## Parse the item the parser is positioned on into a ``CborNode``.
|
||||
## This is cheap for numbers or simple values but expensive
|
||||
## for nested types.
|
||||
try:
|
||||
var next: CborNode
|
||||
case c.kind
|
||||
of CborEventKind.cborEof:
|
||||
return failure(newCborError("end of CBOR stream"))
|
||||
of CborEventKind.cborPositive:
|
||||
next = CborNode(kind: cborUnsigned, uint: c.intVal)
|
||||
tryNext(c)
|
||||
of CborEventKind.cborNegative:
|
||||
next = CborNode(kind: cborNegative, int: -1 - c.intVal.int64)
|
||||
tryNext(c)
|
||||
of CborEventKind.cborBytes:
|
||||
if c.isIndefinite:
|
||||
next = CborNode(kind: cborBytes, bytes: newSeq[byte]())
|
||||
tryNext(c)
|
||||
while c.kind != CborEventKind.cborBreak:
|
||||
if c.kind != CborEventKind.cborBytes:
|
||||
return failure(newCborError("Expected bytes, got " & $c.kind))
|
||||
let
|
||||
chunkLen = c.intVal.int
|
||||
pos = next.bytes.len
|
||||
next.bytes.setLen(pos+chunkLen)
|
||||
let n = c.s.readData(next.bytes[pos].addr, chunkLen)
|
||||
if n != chunkLen:
|
||||
return failure(newCborError("truncated read of CBOR data"))
|
||||
tryNext(c)
|
||||
else:
|
||||
without rawBytes =? c.nextBytes(), error:
|
||||
return failure(error)
|
||||
next = CborNode(kind: cborBytes, bytes: rawBytes)
|
||||
of CborEventKind.cborText:
|
||||
if c.isIndefinite:
|
||||
next = CborNode(kind: cborText, text: "")
|
||||
tryNext(c)
|
||||
while c.kind != CborEventKind.cborBreak:
|
||||
if c.kind != CborEventKind.cborText:
|
||||
return failure(newCborError("Expected text, got " & $c.kind))
|
||||
let
|
||||
chunkLen = c.intVal.int
|
||||
pos = next.text.len
|
||||
next.text.setLen(pos+chunkLen)
|
||||
let n = c.s.readData(next.text[pos].addr, chunkLen)
|
||||
if n != chunkLen:
|
||||
return failure(newCborError("truncated read of CBOR data"))
|
||||
tryNext(c)
|
||||
tryNext(c)
|
||||
else:
|
||||
without text =? c.nextText(), error:
|
||||
return failure(error)
|
||||
next = CborNode(kind: cborText, text: text)
|
||||
of CborEventKind.cborArray:
|
||||
next = CborNode(kind: cborArray, seq: newSeq[CborNode](c.intVal))
|
||||
if c.isIndefinite:
|
||||
tryNext(c)
|
||||
while c.kind != CborEventKind.cborBreak:
|
||||
without node =? c.nextNode(), error:
|
||||
return failure(error)
|
||||
next.seq.add(node)
|
||||
tryNext(c)
|
||||
else:
|
||||
tryNext(c)
|
||||
for i in 0..next.seq.high:
|
||||
without node =? c.nextNode(), error:
|
||||
return failure(error)
|
||||
next.seq[i] = node
|
||||
of CborEventKind.cborMap:
|
||||
let mapLen = c.intVal.int
|
||||
next = CborNode(kind: cborMap, map: initOrderedTable[CborNode, CborNode](
|
||||
mapLen.nextPowerOfTwo))
|
||||
if c.isIndefinite:
|
||||
tryNext(c)
|
||||
while c.kind != CborEventKind.cborBreak:
|
||||
without key =? c.nextNode(), error:
|
||||
return failure(error)
|
||||
without val =? c.nextNode(), error:
|
||||
return failure(error)
|
||||
next.map[key] = val
|
||||
tryNext(c)
|
||||
else:
|
||||
tryNext(c)
|
||||
for _ in 1 .. mapLen:
|
||||
without key =? c.nextNode(), error:
|
||||
return failure(error)
|
||||
without val =? c.nextNode(), error:
|
||||
return failure(error)
|
||||
next.map[key] = val
|
||||
of CborEventKind.cborTag:
|
||||
let tag = c.intVal
|
||||
tryNext(c)
|
||||
without node =? c.nextNode(), error:
|
||||
return failure(error)
|
||||
next = node
|
||||
next.tag = some tag
|
||||
of CborEventKind.cborSimple:
|
||||
case c.minor
|
||||
of 24:
|
||||
next = CborNode(kind: cborSimple, simple: c.intVal.uint8)
|
||||
else:
|
||||
next = CborNode(kind: cborSimple, simple: c.minor)
|
||||
tryNext(c)
|
||||
of CborEventKind.cborFloat:
|
||||
without f =? c.nextFloat(), error:
|
||||
return failure(error)
|
||||
next = CborNode(kind: cborFloat, float: f)
|
||||
of CborEventKind.cborBreak:
|
||||
discard
|
||||
success(next)
|
||||
except OSError as e:
|
||||
return failure(e.msg)
|
||||
except IOError as e:
|
||||
return failure(e.msg)
|
||||
except Exception as e:
|
||||
return failure(e.msg)
|
||||
|
||||
|
||||
proc readCbor*(s: Stream): ?!CborNode =
|
||||
## Parse a stream into a CBOR object.
|
||||
var parser: CborParser
|
||||
parser.open(s)
|
||||
tryNext(parser)
|
||||
parser.nextNode()
|
||||
|
||||
proc parseCbor*(s: string): ?!CborNode =
|
||||
## Parse a string into a CBOR object.
|
||||
## A wrapper over stream parsing.
|
||||
readCbor(newStringStream s)
|
||||
|
||||
proc `$`*(n: CborNode): string =
|
||||
## Get a ``CborNode`` in diagnostic notation.
|
||||
result = ""
|
||||
if n.tag.isSome:
|
||||
result.add($n.tag.get)
|
||||
result.add("(")
|
||||
case n.kind
|
||||
of cborUnsigned:
|
||||
result.add $n.uint
|
||||
of cborNegative:
|
||||
result.add $n.int
|
||||
of cborBytes:
|
||||
result.add "h'"
|
||||
for c in n.bytes:
|
||||
result.add(c.toHex)
|
||||
result.add "'"
|
||||
of cborText:
|
||||
result.add escape n.text
|
||||
of cborArray:
|
||||
result.add "["
|
||||
for i in 0..<n.seq.high:
|
||||
result.add $(n.seq[i])
|
||||
result.add ", "
|
||||
if n.seq.len > 0:
|
||||
result.add $(n.seq[n.seq.high])
|
||||
result.add "]"
|
||||
of cborMap:
|
||||
result.add "{"
|
||||
let final = n.map.len
|
||||
var i = 1
|
||||
for k, v in n.map.pairs:
|
||||
result.add $k
|
||||
result.add ": "
|
||||
result.add $v
|
||||
if i != final:
|
||||
result.add ", "
|
||||
inc i
|
||||
result.add "}"
|
||||
of cborTag:
|
||||
discard
|
||||
of cborSimple:
|
||||
case n.simple
|
||||
of 20: result.add "false"
|
||||
of 21: result.add "true"
|
||||
of 22: result.add "null"
|
||||
of 23: result.add "undefined"
|
||||
of 31: discard # break code for indefinite-length items
|
||||
else: result.add "simple(" & $n.simple & ")"
|
||||
of cborFloat:
|
||||
case n.float.classify
|
||||
of fcNan:
|
||||
result.add "NaN"
|
||||
of fcInf:
|
||||
result.add "Infinity"
|
||||
of fcNegInf:
|
||||
result.add "-Infinity"
|
||||
else:
|
||||
result.add $n.float
|
||||
of cborRaw:
|
||||
without val =? parseCbor(n.raw), error:
|
||||
return error.msg
|
||||
result.add $val
|
||||
if n.tag.isSome:
|
||||
result.add(")")
|
||||
|
||||
|
||||
proc getInt*(n: CborNode; default: int = 0): int =
|
||||
## Get the numerical value of a ``CborNode`` or a fallback.
|
||||
case n.kind
|
||||
of cborUnsigned: n.uint.int
|
||||
of cborNegative: n.int.int
|
||||
else: default
|
||||
|
||||
proc parseDateText(n: CborNode): DateTime {.raises: [TimeParseError].} =
|
||||
parse(n.text, timeFormat)
|
||||
|
||||
proc parseTime(n: CborNode): Time =
|
||||
case n.kind
|
||||
of cborUnsigned, cborNegative:
|
||||
result = fromUnix n.getInt
|
||||
of cborFloat:
|
||||
result = fromUnixFloat n.float
|
||||
else:
|
||||
assert false
|
||||
|
||||
proc fromCborHook*(v: var DateTime; n: CborNode): bool =
|
||||
## Parse a `DateTime` from the tagged string representation
|
||||
## defined in RCF7049 section 2.4.1.
|
||||
if n.tag.isSome:
|
||||
try:
|
||||
if n.tag.get == 0 and n.kind == cborText:
|
||||
v = parseDateText(n)
|
||||
result = true
|
||||
elif n.tag.get == 1 and n.kind in {cborUnsigned, cborNegative, cborFloat}:
|
||||
v = parseTime(n).utc
|
||||
result = true
|
||||
except ValueError: discard
|
||||
|
||||
proc fromCborHook*(v: var Time; n: CborNode): bool =
|
||||
## Parse a `Time` from the tagged string representation
|
||||
## defined in RCF7049 section 2.4.1.
|
||||
if n.tag.isSome:
|
||||
try:
|
||||
if n.tag.get == 0 and n.kind == cborText:
|
||||
v = parseDateText(n).toTime
|
||||
result = true
|
||||
elif n.tag.get == 1 and n.kind in {cborUnsigned, cborNegative, cborFloat}:
|
||||
v = parseTime(n)
|
||||
result = true
|
||||
except ValueError: discard
|
||||
|
||||
func isTagged*(n: CborNode): bool =
|
||||
## Check if a CBOR item has a tag.
|
||||
n.tag.isSome
|
||||
|
||||
func hasTag*(n: CborNode; tag: Natural): bool =
|
||||
## Check if a CBOR item has a tag.
|
||||
n.tag.isSome and n.tag.get == (uint64)tag
|
||||
|
||||
proc `tag=`*(result: var CborNode; tag: Natural) =
|
||||
## Tag a CBOR item.
|
||||
result.tag = some(tag.uint64)
|
||||
|
||||
func tag*(n: CborNode): uint64 =
|
||||
## Get a CBOR item tag.
|
||||
n.tag.get
|
||||
|
||||
func isBool*(n: CborNode): bool =
|
||||
(n.kind == cborSimple) and (n.simple in {20, 21})
|
||||
|
||||
func getBool*(n: CborNode; default = false): bool =
|
||||
## Get the boolean value of a ``CborNode`` or a fallback.
|
||||
if n.kind == cborSimple:
|
||||
case n.simple
|
||||
of 20: false
|
||||
of 21: true
|
||||
else: default
|
||||
else:
|
||||
default
|
||||
|
||||
func isNull*(n: CborNode): bool =
|
||||
## Return true if ``n`` is a CBOR null.
|
||||
(n.kind == cborSimple) and (n.simple == 22)
|
||||
|
||||
proc getUnsigned*(n: CborNode; default: uint64 = 0): uint64 =
|
||||
## Get the numerical value of a ``CborNode`` or a fallback.
|
||||
case n.kind
|
||||
of cborUnsigned: n.uint
|
||||
of cborNegative: n.int.uint64
|
||||
else: default
|
||||
|
||||
proc getSigned*(n: CborNode; default: int64 = 0): int64 =
|
||||
## Get the numerical value of a ``CborNode`` or a fallback.
|
||||
case n.kind
|
||||
of cborUnsigned: n.uint.int64
|
||||
of cborNegative: n.int
|
||||
else: default
|
||||
|
||||
|
||||
func getFloat*(n: CborNode; default = 0.0): float =
|
||||
## Get the floating-poing value of a ``CborNode`` or a fallback.
|
||||
if n.kind == cborFloat:
|
||||
n.float
|
||||
else:
|
||||
default
|
||||
|
||||
|
||||
proc fromCbor*[T](v: var T; n: CborNode): bool =
|
||||
## Return `true` if `v` can be converted from a given `CborNode`.
|
||||
## Can be extended and overriden with `fromCborHook(v: var T; n: CborNode)`
|
||||
## for specific types of `T`.
|
||||
when T is CborNode:
|
||||
v = n
|
||||
result = true
|
||||
elif compiles(fromCborHook(v, n)):
|
||||
result = fromCborHook(v, n)
|
||||
elif T is distinct:
|
||||
result = fromCbor(distinctBase v, n)
|
||||
elif T is SomeUnsignedInt:
|
||||
if n.kind == cborUnsigned:
|
||||
v = T n.uint
|
||||
result = v.BiggestUInt == n.uint
|
||||
elif T is SomeSignedInt:
|
||||
if n.kind == cborUnsigned:
|
||||
v = T n.uint
|
||||
result = v.BiggestUInt == n.uint
|
||||
elif n.kind == cborNegative:
|
||||
v = T n.int
|
||||
result = v.BiggestInt == n.int
|
||||
elif T is bool:
|
||||
if n.isBool:
|
||||
v = n.getBool
|
||||
result = true
|
||||
elif T is SomeFloat:
|
||||
if n.kind == cborFloat:
|
||||
v = T n.float
|
||||
result = true
|
||||
elif T is seq[byte]:
|
||||
if n.kind == cborBytes:
|
||||
v = n.bytes
|
||||
result = true
|
||||
elif T is string:
|
||||
if n.kind == cborText:
|
||||
v = n.text
|
||||
result = true
|
||||
elif T is seq:
|
||||
if n.kind == cborArray:
|
||||
result = true
|
||||
v.setLen n.seq.len
|
||||
for i, e in n.seq:
|
||||
result = result and fromCbor(v[i], e)
|
||||
if not result:
|
||||
v.setLen 0
|
||||
break
|
||||
elif T is tuple:
|
||||
if n.kind == cborArray and n.seq.len == T.tupleLen:
|
||||
result = true
|
||||
var i: int
|
||||
for f in fields(v):
|
||||
result = result and fromCbor(f, n.seq[i])
|
||||
if not result: break
|
||||
inc i
|
||||
elif T is ref:
|
||||
if n.isNull:
|
||||
v = nil
|
||||
result = true
|
||||
else:
|
||||
if isNil(v): new(v)
|
||||
result = fromCbor(v[], n)
|
||||
elif T is object:
|
||||
if n.kind == cborMap:
|
||||
result = true
|
||||
var
|
||||
i: int
|
||||
key = CborNode(kind: cborText)
|
||||
for s, _ in fieldPairs(v):
|
||||
key.text = s
|
||||
if not n.map.hasKey key:
|
||||
result = false
|
||||
break
|
||||
else:
|
||||
result = fromCbor(v.dot(s), n.map[key])
|
||||
if not result: break
|
||||
inc i
|
||||
result = result and (i == n.map.len)
|
||||
|
||||
proc fromCborQ2*[T](v: var T; n: CborNode): ?!void =
|
||||
## Return a Result containing the value if `v` can be converted from a given `CborNode`,
|
||||
## or an error if conversion fails.
|
||||
## Can be extended and overriden with `fromCborHook(v: var T; n: CborNode)`
|
||||
## for specific types of `T`.
|
||||
try:
|
||||
when T is CborNode:
|
||||
v = n
|
||||
result = success()
|
||||
elif compiles(fromCborHook(v, n)):
|
||||
return fromCborHook(v, n)
|
||||
elif T is distinct:
|
||||
return fromCborQ2(distinctBase v, n)
|
||||
elif T is SomeUnsignedInt:
|
||||
exceptCborKind(T, {cborUnsigned}, n)
|
||||
v = T n.uint
|
||||
if v.BiggestUInt == n.uint:
|
||||
return success()
|
||||
else:
|
||||
return failure(newCborError("Value overflow for unsigned integer"))
|
||||
elif T is SomeSignedInt:
|
||||
exceptCborKind(T, {cborUnsigned, cborNegative}, n)
|
||||
if n.kind == cborUnsigned:
|
||||
v = T n.uint
|
||||
if v.BiggestUInt == n.uint:
|
||||
return success()
|
||||
else:
|
||||
return failure(newCborError("Value overflow for un signed integer"))
|
||||
elif n.kind == cborNegative:
|
||||
v = T n.int
|
||||
if v.BiggestInt == n.int:
|
||||
return success()
|
||||
else:
|
||||
return failure(newCborError("Value overflow for signed integer"))
|
||||
elif T is bool:
|
||||
if not n.isBool:
|
||||
return failure(newCborError("Expected boolean, got " & $n.kind))
|
||||
v = n.getBool
|
||||
return success()
|
||||
elif T is SomeFloat:
|
||||
exceptCborKind(T, {cborFloat}, n)
|
||||
v = T n.float
|
||||
return success()
|
||||
elif T is seq[byte]:
|
||||
exceptCborKind(T, {cborBytes}, n)
|
||||
v = n.bytes
|
||||
return success()
|
||||
elif T is string:
|
||||
exceptCborKind(T, {cborText}, n)
|
||||
v = n.text
|
||||
return success()
|
||||
elif T is seq:
|
||||
exceptCborKind(T, {cborArray}, n)
|
||||
v.setLen n.seq.len
|
||||
for i, e in n.seq:
|
||||
let itemResult = fromCborQ2(v[i], e)
|
||||
if itemResult.isFailure:
|
||||
v.setLen 0
|
||||
return failure(itemResult.error)
|
||||
return success()
|
||||
elif T is tuple:
|
||||
exceptCborKind(T, {cborArray}, n)
|
||||
if n.seq.len != T.tupleLen:
|
||||
return failure(newCborError("Expected tuple of length " & $T.tupleLen))
|
||||
var i: int
|
||||
for f in fields(v):
|
||||
let itemResult = fromCborQ2(f, n.seq[i])
|
||||
if itemResult.isFailure:
|
||||
return failure(itemResult.error)
|
||||
inc i
|
||||
return success()
|
||||
elif T is ref:
|
||||
if n.isNull:
|
||||
v = nil
|
||||
return success()
|
||||
else:
|
||||
if isNil(v): new(v)
|
||||
return fromCborQ2(v[], n)
|
||||
elif T is object:
|
||||
exceptCborKind(T, {cborMap}, n)
|
||||
var
|
||||
i: int
|
||||
key = CborNode(kind: cborText)
|
||||
for s, _ in fieldPairs(v):
|
||||
key.text = s
|
||||
if not n.map.hasKey key:
|
||||
return failure(newCborError("Missing field: " & s))
|
||||
else:
|
||||
let fieldResult = fromCborQ2(v.dot(s), n.map[key])
|
||||
if fieldResult.isFailure:
|
||||
return failure(fieldResult.error)
|
||||
inc i
|
||||
if i == n.map.len:
|
||||
return success()
|
||||
else:
|
||||
return failure(newCborError("Extra fields in map"))
|
||||
else:
|
||||
return failure(newCborError("Unsupported type: " & $T))
|
||||
except Exception as e:
|
||||
return failure(newCborError(e.msg))
|
||||
37
serde/cbor/errors.nim
Normal file
37
serde/cbor/errors.nim
Normal file
@ -0,0 +1,37 @@
|
||||
import ../utils/types
|
||||
import ./types
|
||||
import std/sets
|
||||
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKinds: string, cbor: CborNode
|
||||
): ref UnexpectedKindError =
|
||||
newException(
|
||||
UnexpectedKindError,
|
||||
"deserialization to " & $expectedType & " failed: expected " &
|
||||
expectedKinds &
|
||||
" but got " & $cbor.kind,
|
||||
)
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKinds: set[CborEventKind], cbor: CborNode
|
||||
): ref UnexpectedKindError =
|
||||
newUnexpectedKindError(expectedType, $expectedKinds, cbor)
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKind: CborEventKind, cbor: CborNode
|
||||
): ref UnexpectedKindError =
|
||||
newUnexpectedKindError(expectedType, {expectedKind}, cbor)
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKinds: set[CborNodeKind], cbor: CborNode
|
||||
): ref UnexpectedKindError =
|
||||
newUnexpectedKindError(expectedType, $expectedKinds, cbor)
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKind: CborNodeKind, cbor: CborNode
|
||||
): ref UnexpectedKindError =
|
||||
newUnexpectedKindError(expectedType, {expectedKind}, cbor)
|
||||
|
||||
proc newCborError*(msg: string): ref CborParseError =
|
||||
newException(CborParseError, msg)
|
||||
42
serde/cbor/helpers.nim
Normal file
42
serde/cbor/helpers.nim
Normal file
@ -0,0 +1,42 @@
|
||||
|
||||
import ./types
|
||||
import ./errors
|
||||
from macros import newDotExpr, newIdentNode, strVal
|
||||
|
||||
template exceptCborKind*(expectedType: type, expectedKinds: set[CborNodeKind],
|
||||
cbor: CborNode) =
|
||||
if cbor.kind notin expectedKinds:
|
||||
return failure(newUnexpectedKindError(expectedType, expectedKinds, cbor))
|
||||
|
||||
template exceptCborKind*(expectedType: type, expectedKind: CborNodeKind,
|
||||
cbor: CborNode) =
|
||||
exceptCborKind(expectedType, {expectedKind}, cbor)
|
||||
|
||||
template exceptCborKind*(expectedType: type, expectedKinds: set[CborEventKind],
|
||||
cbor: CborNode) =
|
||||
if cbor.kind notin expectedKinds:
|
||||
return failure(newUnexpectedKindError(expectedType, expectedKinds, cbor))
|
||||
|
||||
template exceptCborKind*(expectedType: type, expectedKind: CborEventKind,
|
||||
cbor: CborNode) =
|
||||
exceptCborKind(expectedType, {expectedKind}, cbor)
|
||||
|
||||
macro dot*(obj: object, fld: string): untyped =
|
||||
## Turn ``obj.dot("fld")`` into ``obj.fld``.
|
||||
newDotExpr(obj, newIdentNode(fld.strVal))
|
||||
|
||||
|
||||
func floatSingle*(half: uint16): float32 =
|
||||
## Convert a 16-bit float to 32-bits.
|
||||
func ldexp(x: float64; exponent: int): float64 {.importc: "ldexp",
|
||||
header: "<math.h>".}
|
||||
let
|
||||
exp = (half shr 10) and 0x1f
|
||||
mant = float64(half and 0x3ff)
|
||||
val = if exp == 0:
|
||||
ldexp(mant, -24)
|
||||
elif exp != 31:
|
||||
ldexp(mant + 1024, exp.int - 25)
|
||||
else:
|
||||
if mant == 0: Inf else: NaN
|
||||
if (half and 0x8000) == 0: val else: -val
|
||||
42
serde/cbor/jsonhook.nim
Normal file
42
serde/cbor/jsonhook.nim
Normal file
@ -0,0 +1,42 @@
|
||||
import std/[base64, tables]
|
||||
import ../json/stdjson
|
||||
import ./types
|
||||
import ./errors
|
||||
import ./deserializer
|
||||
|
||||
proc toJsonHook*(n: CborNode): JsonNode =
|
||||
case n.kind:
|
||||
of cborUnsigned:
|
||||
newJInt n.uint.BiggestInt
|
||||
of cborNegative:
|
||||
newJInt n.int.BiggestInt
|
||||
of cborBytes:
|
||||
newJString base64.encode(cast[string](n.bytes), safe = true)
|
||||
of cborText:
|
||||
newJString n.text
|
||||
of cborArray:
|
||||
let a = newJArray()
|
||||
for e in n.seq.items:
|
||||
a.add(e.toJsonHook)
|
||||
a
|
||||
of cborMap:
|
||||
let o = newJObject()
|
||||
for k, v in n.map.pairs:
|
||||
if k.kind == cborText:
|
||||
o[k.text] = v.toJsonHook
|
||||
else:
|
||||
o[$k] = v.toJsonHook
|
||||
o
|
||||
of cborTag: nil
|
||||
of cborSimple:
|
||||
if n.isBool:
|
||||
newJBool(n.getBool())
|
||||
elif n.isNull:
|
||||
newJNull()
|
||||
else: nil
|
||||
of cborFloat:
|
||||
newJFloat n.float
|
||||
of cborRaw:
|
||||
without parsed =? parseCbor(n.raw), error:
|
||||
raise newCborError(error.msg)
|
||||
toJsonHook(parsed)
|
||||
@ -1,12 +1,42 @@
|
||||
{.push checks: off.}
|
||||
|
||||
import ../utils/cbor
|
||||
import std/[streams, options, tables, typetraits, math, endians, times, base64]
|
||||
import ./types
|
||||
|
||||
func isHalfPrecise(single: float32): bool =
|
||||
# TODO: check for subnormal false-positives
|
||||
let val = cast[uint32](single)
|
||||
if val == 0 or val == (1'u32 shl 31):
|
||||
result = true
|
||||
else:
|
||||
let
|
||||
exp = int32((val and (0xff'u32 shl 23)) shr 23) - 127
|
||||
mant = val and 0x7fffff'u32
|
||||
if -25 < exp and exp < 16 and (mant and 0x1fff) == 0:
|
||||
result = true
|
||||
|
||||
func floatHalf(single: float32): uint16 =
|
||||
## Convert a 32-bit float to 16-bits.
|
||||
let
|
||||
val = cast[uint32](single)
|
||||
exp = val and 0x7f800000
|
||||
mant = val and 0x7fffff
|
||||
sign = uint16(val shr 16) and (1 shl 15)
|
||||
let
|
||||
unbiasedExp = int32(exp shr 23) - 127
|
||||
halfExp = unbiasedExp + 15
|
||||
if halfExp < 1:
|
||||
if 14 - halfExp < 25:
|
||||
result = sign or uint16((mant or 0x800000) shr uint16(14 - halfExp))
|
||||
else:
|
||||
result = sign or uint16(halfExp shl 10) or uint16(mant shr 13)
|
||||
|
||||
func initialByte(major, minor: Natural): uint8 {.inline.} =
|
||||
uint8((major shl 5) or (minor and 0b11111))
|
||||
|
||||
proc writeInitial[T: SomeInteger](str: Stream; m: uint8; n: T) =
|
||||
## Write the initial integer of a CBOR item.
|
||||
|
||||
let m = m shl 5
|
||||
when T is byte:
|
||||
if n < 24:
|
||||
@ -36,7 +66,7 @@ proc writeInitial[T: SomeInteger](str: Stream; m: uint8; n: T) =
|
||||
{.unroll.}
|
||||
str.write((uint8)n shr i)
|
||||
str.write((uint8)n)
|
||||
{.pop.}
|
||||
# {.pop.}
|
||||
|
||||
proc writeCborArrayLen*(str: Stream; len: Natural) =
|
||||
## Write a marker to the stream that initiates an array of ``len`` items.
|
||||
@ -71,6 +101,8 @@ proc writeCbor*(str: Stream; buf: pointer; len: int) =
|
||||
str.writeInitial(BytesMajor, len)
|
||||
if len > 0: str.writeData(buf, len)
|
||||
|
||||
proc isSorted*(n: CborNode): bool {.gcsafe.}
|
||||
|
||||
proc writeCbor*[T](str: Stream; v: T) =
|
||||
## Write the CBOR binary representation of a `T` to a `Stream`.
|
||||
## The behavior of this procedure can be extended or overriden
|
||||
@ -219,3 +251,119 @@ proc encode*[T](v: T): string =
|
||||
let s = newStringStream()
|
||||
s.writeCbor(v)
|
||||
s.data
|
||||
|
||||
proc toRaw*(n: CborNode): CborNode =
|
||||
## Reduce a CborNode to a string of bytes.
|
||||
if n.kind == cborRaw: n
|
||||
else: CborNode(kind: cborRaw, raw: encode(n))
|
||||
|
||||
proc isSorted(n: CborNode): bool =
|
||||
## Check if the item is sorted correctly.
|
||||
var lastRaw = ""
|
||||
for key in n.map.keys:
|
||||
let thisRaw = key.toRaw.raw
|
||||
if lastRaw != "":
|
||||
if cmp(lastRaw, thisRaw) > 0: return false
|
||||
lastRaw = thisRaw
|
||||
true
|
||||
|
||||
proc sort*(n: var CborNode) =
|
||||
## Sort a CBOR map object.
|
||||
var tmp = initOrderedTable[CborNode, CborNode](n.map.len.nextPowerOfTwo)
|
||||
for key, val in n.map.mpairs:
|
||||
tmp[key.toRaw] = move(val)
|
||||
sort(tmp) do (x, y: tuple[k: CborNode; v: CborNode]) -> int:
|
||||
result = cmp(x.k.raw, y.k.raw)
|
||||
n.map = move tmp
|
||||
|
||||
proc writeCborHook*(str: Stream; dt: DateTime) =
|
||||
## Write a `DateTime` using the tagged string representation
|
||||
## defined in RCF7049 section 2.4.1.
|
||||
writeCborTag(str, 0)
|
||||
writeCbor(str, format(dt, timeFormat))
|
||||
|
||||
proc writeCborHook*(str: Stream; t: Time) =
|
||||
## Write a `Time` using the tagged numerical representation
|
||||
## defined in RCF7049 section 2.4.1.
|
||||
writeCborTag(str, 1)
|
||||
writeCbor(str, t.toUnix)
|
||||
|
||||
func toCbor*(x: CborNode): CborNode = x
|
||||
|
||||
func toCbor*(x: SomeInteger): CborNode =
|
||||
if x > 0:
|
||||
CborNode(kind: cborUnsigned, uint: x.uint64)
|
||||
else:
|
||||
CborNode(kind: cborNegative, int: x.int64)
|
||||
|
||||
func toCbor*(x: openArray[byte]): CborNode =
|
||||
CborNode(kind: cborBytes, bytes: @x)
|
||||
|
||||
func toCbor*(x: string): CborNode =
|
||||
CborNode(kind: cborText, text: x)
|
||||
|
||||
func toCbor*(x: openArray[CborNode]): CborNode =
|
||||
CborNode(kind: cborArray, seq: @x)
|
||||
|
||||
func toCbor*(pairs: openArray[(CborNode, CborNode)]): CborNode =
|
||||
CborNode(kind: cborMap, map: pairs.toOrderedTable)
|
||||
|
||||
func toCbor*(tag: uint64; val: CborNode): CborNode =
|
||||
result = toCbor(val)
|
||||
result.tag = some(tag)
|
||||
|
||||
func toCbor*(x: bool): CborNode =
|
||||
case x
|
||||
of false:
|
||||
CborNode(kind: cborSimple, simple: 20)
|
||||
of true:
|
||||
CborNode(kind: cborSimple, simple: 21)
|
||||
|
||||
func toCbor*(x: SomeFloat): CborNode =
|
||||
CborNode(kind: cborFloat, float: x.float64)
|
||||
|
||||
func toCbor*(x: pointer): CborNode =
|
||||
## A hack to produce a CBOR null item.
|
||||
assert(x.isNil)
|
||||
CborNode(kind: cborSimple, simple: 22)
|
||||
|
||||
func initCborBytes*[T: char|byte](buf: openArray[T]): CborNode =
|
||||
## Create a CBOR byte string from `buf`.
|
||||
result = CborNode(kind: cborBytes, bytes: newSeq[byte](buf.len))
|
||||
for i in 0..<buf.len:
|
||||
result.bytes[i] = (byte)buf[i]
|
||||
|
||||
func initCborBytes*(len: int): CborNode =
|
||||
## Create a CBOR byte string of ``len`` bytes.
|
||||
CborNode(kind: cborBytes, bytes: newSeq[byte](len))
|
||||
|
||||
func initCborText*(s: string): CborNode =
|
||||
## Create a CBOR text string from ``s``.
|
||||
## CBOR text must be unicode.
|
||||
CborNode(kind: cborText, text: s)
|
||||
|
||||
func initCborArray*(): CborNode =
|
||||
## Create an empty CBOR array.
|
||||
CborNode(kind: cborArray, seq: newSeq[CborNode]())
|
||||
|
||||
func initCborArray*(len: Natural): CborNode =
|
||||
## Initialize a CBOR arrary.
|
||||
CborNode(kind: cborArray, seq: newSeq[CborNode](len))
|
||||
|
||||
func initCborMap*(initialSize = tables.defaultInitialSize): CborNode =
|
||||
## Initialize a CBOR map.
|
||||
CborNode(kind: cborMap,
|
||||
map: initOrderedTable[CborNode, CborNode](initialSize))
|
||||
|
||||
func initCbor*(items: varargs[CborNode, toCbor]): CborNode =
|
||||
## Initialize a CBOR arrary.
|
||||
CborNode(kind: cborArray, seq: @items)
|
||||
|
||||
template initCborOther*(x: untyped): CborNode =
|
||||
## Initialize a ``CborNode`` from a type where ``toCbor`` is not implemented.
|
||||
## This encodes ``x`` to binary using ``writeCbor``, so
|
||||
## ``$(initCborOther(x))`` will incur an encode and decode roundtrip.
|
||||
let s = newStringStream()
|
||||
s.writeCbor(x)
|
||||
CborNode(kind: cborRaw, raw: s.data)
|
||||
|
||||
|
||||
165
serde/cbor/types.nim
Normal file
165
serde/cbor/types.nim
Normal file
@ -0,0 +1,165 @@
|
||||
import std/[streams, tables, options, hashes, times]
|
||||
|
||||
const timeFormat* = initTimeFormat "yyyy-MM-dd'T'HH:mm:sszzz"
|
||||
|
||||
const
|
||||
PositiveMajor* = 0'u8
|
||||
NegativeMajor* = 1'u8
|
||||
BytesMajor* = 2'u8
|
||||
TextMajor* = 3'u8
|
||||
ArrayMajor* = 4'u8
|
||||
MapMajor* = 5'u8
|
||||
TagMajor* = 6'u8
|
||||
SimpleMajor* = 7'u8
|
||||
Null* = 0xf6'u8
|
||||
|
||||
type
|
||||
CborEventKind* {.pure.} = enum
|
||||
## enumeration of events that may occur while parsing
|
||||
cborEof,
|
||||
cborPositive,
|
||||
cborNegative,
|
||||
cborBytes,
|
||||
cborText,
|
||||
cborArray,
|
||||
cborMap,
|
||||
cborTag,
|
||||
cborSimple,
|
||||
cborFloat,
|
||||
cborBreak
|
||||
|
||||
CborParser* = object ## CBOR parser state.
|
||||
s*: Stream
|
||||
intVal*: uint64
|
||||
minor*: uint8
|
||||
kind*: CborEventKind
|
||||
|
||||
type
|
||||
CborNodeKind* = enum
|
||||
cborUnsigned = 0,
|
||||
cborNegative = 1,
|
||||
cborBytes = 2,
|
||||
cborText = 3,
|
||||
cborArray = 4,
|
||||
cborMap = 5,
|
||||
cborTag = 6,
|
||||
cborSimple = 7,
|
||||
cborFloat,
|
||||
cborRaw
|
||||
|
||||
CborNode* = object
|
||||
## An abstract representation of a CBOR item. Useful for diagnostics.
|
||||
tag*: Option[uint64]
|
||||
case kind*: CborNodeKind
|
||||
of cborUnsigned:
|
||||
uint*: BiggestUInt
|
||||
of cborNegative:
|
||||
int*: BiggestInt
|
||||
of cborBytes:
|
||||
bytes*: seq[byte]
|
||||
of cborText:
|
||||
text*: string
|
||||
of cborArray:
|
||||
seq*: seq[CborNode]
|
||||
of cborMap:
|
||||
map*: OrderedTable[CborNode, CborNode]
|
||||
of cborTag:
|
||||
discard
|
||||
of cborSimple:
|
||||
simple*: uint8
|
||||
of cborFloat:
|
||||
float*: float64
|
||||
of cborRaw:
|
||||
raw*: string
|
||||
|
||||
func `==`*(x, y: CborNode): bool
|
||||
|
||||
func hash*(x: CborNode): Hash
|
||||
|
||||
func `==`*(x, y: CborNode): bool =
|
||||
if x.kind == y.kind and x.tag == y.tag:
|
||||
case x.kind
|
||||
of cborUnsigned:
|
||||
x.uint == y.uint
|
||||
of cborNegative:
|
||||
x.int == y.int
|
||||
of cborBytes:
|
||||
x.bytes == y.bytes
|
||||
of cborText:
|
||||
x.text == y.text
|
||||
of cborArray:
|
||||
x.seq == y.seq
|
||||
of cborMap:
|
||||
x.map == y.map
|
||||
of cborTag:
|
||||
false
|
||||
of cborSimple:
|
||||
x.simple == y.simple
|
||||
of cborFloat:
|
||||
x.float == y.float
|
||||
of cborRaw:
|
||||
x.raw == y.raw
|
||||
else:
|
||||
false
|
||||
|
||||
func `==`*(x: CborNode; y: SomeInteger): bool =
|
||||
case x.kind
|
||||
of cborUnsigned:
|
||||
x.uint == y
|
||||
of cborNegative:
|
||||
x.int == y
|
||||
else:
|
||||
false
|
||||
|
||||
func `==`*(x: CborNode; y: string): bool =
|
||||
x.kind == cborText and x.text == y
|
||||
|
||||
func `==`*(x: CborNode; y: SomeFloat): bool =
|
||||
if x.kind == cborFloat: x.float == y
|
||||
|
||||
func hash(x: CborNode): Hash =
|
||||
var h = hash(get(x.tag, 0))
|
||||
h = h !& x.kind.int.hash
|
||||
case x.kind
|
||||
of cborUnsigned:
|
||||
h = h !& x.uint.hash
|
||||
of cborNegative:
|
||||
h = h !& x.int.hash
|
||||
of cborBytes:
|
||||
h = h !& x.bytes.hash
|
||||
of cborText:
|
||||
h = h !& x.text.hash
|
||||
of cborArray:
|
||||
for y in x.seq:
|
||||
h = h !& y.hash
|
||||
of cborMap:
|
||||
for key, val in x.map.pairs:
|
||||
h = h !& key.hash
|
||||
h = h !& val.hash
|
||||
of cborTag:
|
||||
discard
|
||||
of cborSimple:
|
||||
h = h !& x.simple.hash
|
||||
of cborFloat:
|
||||
h = h !& x.float.hash
|
||||
of cborRaw:
|
||||
assert(x.tag.isNone)
|
||||
h = x.raw.hash
|
||||
!$h
|
||||
|
||||
proc `[]`*(n, k: CborNode): CborNode = n.map[k]
|
||||
## Retrieve a value from a CBOR map.
|
||||
|
||||
proc `[]=`*(n: var CborNode; k, v: sink CborNode) = n.map[k] = v
|
||||
## Assign a pair in a CBOR map.
|
||||
|
||||
func len*(node: CborNode): int =
|
||||
## Return the logical length of a ``CborNode``, that is the
|
||||
## length of a byte or text string, or the number of
|
||||
## elements in a array or map. Otherwise it returns -1.
|
||||
case node.kind
|
||||
of cborBytes: node.bytes.len
|
||||
of cborText: node.text.len
|
||||
of cborArray: node.seq.len
|
||||
of cborMap: node.map.len
|
||||
else: -1
|
||||
@ -1,6 +1,6 @@
|
||||
import ./json/parser
|
||||
import ./json/deserializer
|
||||
import ./utils/stdjson
|
||||
import ./json/stdjson
|
||||
import ./utils/pragmas
|
||||
import ./json/serializer
|
||||
import ./utils/types
|
||||
|
||||
@ -12,10 +12,11 @@ import pkg/questionable
|
||||
import pkg/questionable/results
|
||||
|
||||
import ./parser
|
||||
import ../utils/errors
|
||||
import ../utils/stdjson
|
||||
import ./errors
|
||||
import ./stdjson
|
||||
import ../utils/pragmas
|
||||
import ../utils/types
|
||||
import ../utils/errors
|
||||
import ./helpers
|
||||
|
||||
export parser
|
||||
@ -31,28 +32,6 @@ export types
|
||||
logScope:
|
||||
topics = "nimserde json deserializer"
|
||||
|
||||
template expectJsonKind(
|
||||
expectedType: type, expectedKinds: set[JsonNodeKind], json: JsonNode
|
||||
) =
|
||||
if json.isNil or json.kind notin expectedKinds:
|
||||
return failure(newUnexpectedKindError(expectedType, expectedKinds, json))
|
||||
|
||||
template expectJsonKind*(expectedType: type, expectedKind: JsonNodeKind, json: JsonNode) =
|
||||
expectJsonKind(expectedType, {expectedKind}, json)
|
||||
|
||||
proc fieldKeys[T](obj: T): seq[string] =
|
||||
for name, _ in fieldPairs(
|
||||
when type(T) is ref:
|
||||
obj[]
|
||||
else:
|
||||
obj
|
||||
):
|
||||
result.add name
|
||||
|
||||
func keysNotIn[T](json: JsonNode, obj: T): HashSet[string] =
|
||||
let jsonKeys = json.keys.toSeq.toHashSet
|
||||
let objKeys = obj.fieldKeys.toHashSet
|
||||
difference(jsonKeys, objKeys)
|
||||
|
||||
proc fromJson*(T: type enum, json: JsonNode): ?!T =
|
||||
expectJsonKind(string, JString, json)
|
||||
@ -126,7 +105,8 @@ proc fromJson*(_: type seq[byte], json: JsonNode): ?!seq[byte] =
|
||||
expectJsonKind(seq[byte], JString, json)
|
||||
hexToSeqByte(json.getStr).catch
|
||||
|
||||
proc fromJson*[N: static[int], T: array[N, byte]](_: type T, json: JsonNode): ?!T =
|
||||
proc fromJson*[N: static[int], T: array[N, byte]](_: type T,
|
||||
json: JsonNode): ?!T =
|
||||
expectJsonKind(T, JString, json)
|
||||
T.fromHex(json.getStr).catch
|
||||
|
||||
@ -161,7 +141,8 @@ proc fromJson*(T: typedesc[StUint or StInt], json: JsonNode): ?!T =
|
||||
catch parse(jsonStr, T)
|
||||
|
||||
proc fromJson*[T](_: type Option[T], json: JsonNode): ?!Option[T] =
|
||||
if json.isNil or json.kind == JNull or json.isEmptyString or json.isNullString:
|
||||
if json.isNil or json.kind == JNull or json.isEmptyString or
|
||||
json.isNullString:
|
||||
return success(none T)
|
||||
without val =? T.fromJson(json), error:
|
||||
return failure(error)
|
||||
@ -244,7 +225,8 @@ proc fromJson*[T: ref object or object](_: type T, json: JsonNode): ?!T =
|
||||
value = parsed
|
||||
|
||||
# not Option[T]
|
||||
elif opts.key in json and jsonVal =? json{opts.key}.catch and not jsonVal.isNil:
|
||||
elif opts.key in json and jsonVal =? json{opts.key}.catch and
|
||||
not jsonVal.isNil:
|
||||
without parsed =? typeof(value).fromJson(jsonVal), e:
|
||||
trace "failed to deserialize field",
|
||||
`type` = $typeof(value), json = jsonVal, error = e.msg
|
||||
@ -322,7 +304,7 @@ proc fromJson*(T: typedesc[StUint or StInt], json: string): ?!T =
|
||||
T.fromJson(newJString(json))
|
||||
|
||||
proc fromJson*[T: ref object or object](_: type ?T, json: string): ?!Option[T] =
|
||||
when T is (StUInt or StInt):
|
||||
when T is (StUint or StInt):
|
||||
let jsn = newJString(json)
|
||||
else:
|
||||
let jsn = ?JsonNode.parse(json) # full qualification required in-module only
|
||||
|
||||
28
serde/json/errors.nim
Normal file
28
serde/json/errors.nim
Normal file
@ -0,0 +1,28 @@
|
||||
import ./stdjson
|
||||
import ../utils/types
|
||||
import std/sets
|
||||
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKinds: string, json: JsonNode
|
||||
): ref UnexpectedKindError =
|
||||
let kind =
|
||||
if json.isNil:
|
||||
"nil"
|
||||
else:
|
||||
$json.kind
|
||||
newException(
|
||||
UnexpectedKindError,
|
||||
"deserialization to " & $expectedType & " failed: expected " & expectedKinds &
|
||||
" but got " & $kind,
|
||||
)
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKinds: set[JsonNodeKind], json: JsonNode
|
||||
): ref UnexpectedKindError =
|
||||
newUnexpectedKindError(expectedType, $expectedKinds, json)
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKind: JsonNodeKind, json: JsonNode
|
||||
): ref UnexpectedKindError =
|
||||
newUnexpectedKindError(expectedType, {expectedKind}, json)
|
||||
@ -1,4 +1,31 @@
|
||||
import std/json
|
||||
import ./errors
|
||||
import std/[macros, tables, sets, sequtils]
|
||||
|
||||
template expectJsonKind*(
|
||||
expectedType: type, expectedKinds: set[JsonNodeKind], json: JsonNode
|
||||
) =
|
||||
if json.isNil or json.kind notin expectedKinds:
|
||||
return failure(newUnexpectedKindError(expectedType, expectedKinds, json))
|
||||
|
||||
template expectJsonKind*(expectedType: type, expectedKind: JsonNodeKind,
|
||||
json: JsonNode) =
|
||||
expectJsonKind(expectedType, {expectedKind}, json)
|
||||
|
||||
proc fieldKeys*[T](obj: T): seq[string] =
|
||||
for name, _ in fieldPairs(
|
||||
when type(T) is ref:
|
||||
obj[]
|
||||
else:
|
||||
obj
|
||||
):
|
||||
result.add name
|
||||
|
||||
func keysNotIn*[T](json: JsonNode, obj: T): HashSet[string] =
|
||||
let jsonKeys = json.keys.toSeq.toHashSet
|
||||
let objKeys = obj.fieldKeys.toHashSet
|
||||
difference(jsonKeys, objKeys)
|
||||
|
||||
|
||||
func isEmptyString*(json: JsonNode): bool =
|
||||
return json.kind == JString and json.getStr == ""
|
||||
|
||||
@ -9,7 +9,7 @@ import pkg/questionable
|
||||
import pkg/stew/byteutils
|
||||
import pkg/stint
|
||||
|
||||
import ../utils/stdjson
|
||||
import ./stdjson
|
||||
import ../utils/pragmas
|
||||
import ../utils/types
|
||||
|
||||
@ -18,8 +18,6 @@ export stdjson
|
||||
export pragmas
|
||||
export types
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
logScope:
|
||||
topics = "nimserde json serializer"
|
||||
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
import std/[tables]
|
||||
|
||||
type CborNodeKind* = enum
|
||||
cborUnsigned = 0,
|
||||
cborNegative = 1,
|
||||
cborBytes = 2,
|
||||
cborText = 3,
|
||||
cborArray = 4,
|
||||
cborMap = 5,
|
||||
cborTag = 6,
|
||||
cborSimple = 7,
|
||||
cborFloat,
|
||||
cborRaw
|
||||
|
||||
CborNode* = object
|
||||
## An abstract representation of a CBOR item. Useful for diagnostics.
|
||||
tag: Option[uint64]
|
||||
case kind*: CborNodeKind
|
||||
of cborUnsigned:
|
||||
uint*: BiggestUInt
|
||||
of cborNegative:
|
||||
int*: BiggestInt
|
||||
of cborBytes:
|
||||
bytes*: seq[byte]
|
||||
of cborText:
|
||||
text*: string
|
||||
of cborArray:
|
||||
seq*: seq[CborNode]
|
||||
of cborMap:
|
||||
map*: OrderedTable[CborNode, CborNode]
|
||||
of cborTag:
|
||||
discard
|
||||
of cborSimple:
|
||||
simple*: uint8
|
||||
of cborFloat:
|
||||
float*: float64
|
||||
of cborRaw:
|
||||
raw*: string
|
||||
|
||||
func `==`*(x, y: CborNode): bool
|
||||
|
||||
func hash*(x: CborNode): Hash
|
||||
@ -1,6 +1,3 @@
|
||||
import std/sets
|
||||
|
||||
import ./stdjson
|
||||
import ./types
|
||||
|
||||
{.push raises: [].}
|
||||
@ -13,28 +10,5 @@ proc mapErrTo*[E1: ref CatchableError, E2: SerdeError](
|
||||
proc newSerdeError*(msg: string): ref SerdeError =
|
||||
newException(SerdeError, msg)
|
||||
|
||||
# proc newUnexpectedKindError*
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKinds: string, json: JsonNode
|
||||
): ref UnexpectedKindError =
|
||||
let kind =
|
||||
if json.isNil:
|
||||
"nil"
|
||||
else:
|
||||
$json.kind
|
||||
newException(
|
||||
UnexpectedKindError,
|
||||
"deserialization to " & $expectedType & " failed: expected " & expectedKinds &
|
||||
" but got " & $kind,
|
||||
)
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKinds: set[JsonNodeKind], json: JsonNode
|
||||
): ref UnexpectedKindError =
|
||||
newUnexpectedKindError(expectedType, $expectedKinds, json)
|
||||
|
||||
proc newUnexpectedKindError*(
|
||||
expectedType: type, expectedKind: JsonNodeKind, json: JsonNode
|
||||
): ref UnexpectedKindError =
|
||||
newUnexpectedKindError(expectedType, {expectedKind}, json)
|
||||
|
||||
100
tests/cbor/test.nim
Normal file
100
tests/cbor/test.nim
Normal file
@ -0,0 +1,100 @@
|
||||
import
|
||||
std/[base64, os, random, times, json, unittest]
|
||||
import pkg/serde/cbor
|
||||
import pkg/questionable
|
||||
import pkg/questionable/results
|
||||
|
||||
proc findVectorsFile: string =
|
||||
var parent = getCurrentDir()
|
||||
while parent != "/":
|
||||
result = parent / "tests" / "cbor" / "test_vector.json"
|
||||
if fileExists result: return
|
||||
parent = parent.parentDir
|
||||
raiseAssert "Could not find test vectors"
|
||||
|
||||
let js = findVectorsFile().readFile.parseJson()
|
||||
|
||||
suite "decode":
|
||||
for v in js.items:
|
||||
if v.hasKey "decoded":
|
||||
let
|
||||
control = $v["decoded"]
|
||||
name = v["name"].getStr
|
||||
test name:
|
||||
let
|
||||
controlCbor = base64.decode v["cbor"].getStr
|
||||
without c =? parseCbor(controlCbor), error:
|
||||
fail()
|
||||
let js = c.toJsonHook()
|
||||
if js.isNil:
|
||||
fail()
|
||||
else:
|
||||
check(control == $js)
|
||||
|
||||
suite "diagnostic":
|
||||
for v in js.items:
|
||||
if v.hasKey "diagnostic":
|
||||
let
|
||||
control = v["diagnostic"].getStr
|
||||
name = v["name"].getStr
|
||||
test name:
|
||||
let
|
||||
controlCbor = base64.decode v["cbor"].getStr
|
||||
without c =? parseCbor(controlCbor), error:
|
||||
fail()
|
||||
check($c == control)
|
||||
|
||||
suite "roundtrip":
|
||||
for v in js.items:
|
||||
if v["roundtrip"].getBool:
|
||||
let
|
||||
controlB64 = v["cbor"].getStr
|
||||
controlCbor = base64.decode controlB64
|
||||
name = v["name"].getStr
|
||||
without c =? parseCbor(controlCbor), error:
|
||||
fail()
|
||||
test name:
|
||||
let testCbor = encode(c)
|
||||
if controlCbor != testCbor:
|
||||
let testB64 = base64.encode(testCbor)
|
||||
check(controlB64 == testB64)
|
||||
|
||||
suite "hooks":
|
||||
test "DateTime":
|
||||
let dt = now()
|
||||
var
|
||||
bin = encode(dt)
|
||||
without node =? parseCbor(bin), error:
|
||||
fail()
|
||||
check(node.text == $dt)
|
||||
test "Time":
|
||||
let t = now().toTime
|
||||
var
|
||||
bin = encode(t)
|
||||
without node =? parseCbor(bin), error:
|
||||
fail()
|
||||
|
||||
check(node.getInt == t.toUnix)
|
||||
|
||||
test "tag":
|
||||
var c = toCbor("foo")
|
||||
c.tag = some(99'u64)
|
||||
check c.tag == some(99'u64)
|
||||
|
||||
test "sorting":
|
||||
var map = initCborMap()
|
||||
var keys = @[
|
||||
toCbor(10),
|
||||
toCbor(100),
|
||||
toCbor(-1),
|
||||
toCbor("z"),
|
||||
toCbor("aa"),
|
||||
toCbor([toCbor(100)]),
|
||||
toCbor([toCbor(-1)]),
|
||||
toCbor(false),
|
||||
]
|
||||
shuffle(keys)
|
||||
for k in keys: map[k] = toCbor(0)
|
||||
check not map.isSorted
|
||||
sort(map)
|
||||
check map.isSorted
|
||||
260
tests/cbor/testDeserialize.nim
Normal file
260
tests/cbor/testDeserialize.nim
Normal file
@ -0,0 +1,260 @@
|
||||
# File: /Users/rahul/Work/repos/nim-serde/tests/cbor_questionable.nim
|
||||
|
||||
import std/unittest
|
||||
import std/options
|
||||
import std/streams
|
||||
import pkg/serde
|
||||
import pkg/questionable
|
||||
import pkg/questionable/results
|
||||
|
||||
# Custom type for testing
|
||||
type
|
||||
CustomPoint = object
|
||||
x: int
|
||||
y: int
|
||||
|
||||
CustomColor = enum
|
||||
Red, Green, Blue
|
||||
|
||||
CustomObject = object
|
||||
name: string
|
||||
point: CustomPoint
|
||||
color: CustomColor
|
||||
|
||||
Person = object
|
||||
name: string
|
||||
age: int
|
||||
isActive: bool
|
||||
|
||||
Inner = object
|
||||
s: string
|
||||
nums: seq[int]
|
||||
|
||||
CompositeNested = object
|
||||
u: uint64
|
||||
n: int
|
||||
b: seq[byte]
|
||||
t: string
|
||||
arr: seq[int]
|
||||
tag: float
|
||||
flag: bool
|
||||
inner: Inner
|
||||
innerArr: seq[Inner]
|
||||
coordinates: tuple[x: int, y: int, label: string]
|
||||
refInner: ref Inner
|
||||
|
||||
proc fromCborHook*(v: var CustomColor, n: CborNode): ?!void =
|
||||
if n.kind == cborNegative:
|
||||
v = CustomColor(n.int)
|
||||
result = success()
|
||||
else:
|
||||
result = failure(newSerdeError("Expected signed integer, got " & $n.kind))
|
||||
|
||||
# Custom fromCborHook for CustomPoint
|
||||
proc fromCborHook*(v: var CustomPoint, n: CborNode): ?!void =
|
||||
if n.kind == cborArray and n.seq.len == 2:
|
||||
var x, y: int
|
||||
let xResult = fromCborQ2(x, n.seq[0])
|
||||
if xResult.isFailure:
|
||||
return failure(xResult.error)
|
||||
|
||||
let yResult = fromCborQ2(y, n.seq[1])
|
||||
if yResult.isFailure:
|
||||
return failure(yResult.error)
|
||||
|
||||
v = CustomPoint(x: x, y: y)
|
||||
result = success()
|
||||
else:
|
||||
result = failure(newSerdeError("Expected array of length 2 for CustomPoint"))
|
||||
|
||||
# Helper function to create CBOR data for testing
|
||||
proc createPointCbor(x, y: int): CborNode =
|
||||
result = CborNode(kind: cborArray)
|
||||
result.seq = @[
|
||||
CborNode(kind: cborUnsigned, uint: x.uint64),
|
||||
CborNode(kind: cborUnsigned, uint: y.uint64)
|
||||
]
|
||||
|
||||
proc createObjectCbor(name: string, point: CustomPoint,
|
||||
color: CustomColor): CborNode =
|
||||
result = CborNode(kind: cborMap)
|
||||
result.map = initOrderedTable[CborNode, CborNode]()
|
||||
|
||||
# Add name field
|
||||
result.map[CborNode(kind: cborText, text: "name")] =
|
||||
CborNode(kind: cborText, text: name)
|
||||
|
||||
# Add point field
|
||||
result.map[CborNode(kind: cborText, text: "point")] =
|
||||
createPointCbor(point.x, point.y)
|
||||
|
||||
# Add color field
|
||||
result.map[CborNode(kind: cborText, text: "color")] =
|
||||
CborNode(kind: cborNegative, int: color.int)
|
||||
|
||||
suite "CBOR deserialization with Questionable":
|
||||
test "fromCborQ2 with primitive types":
|
||||
# Test with integer
|
||||
block:
|
||||
var intValue: int
|
||||
let node = CborNode(kind: cborUnsigned, uint: 42.uint64)
|
||||
let result = fromCborQ2(intValue, node)
|
||||
|
||||
check result.isSuccess
|
||||
check intValue == 42
|
||||
|
||||
# Test with string
|
||||
block:
|
||||
var strValue: string
|
||||
let node = CborNode(kind: cborText, text: "hello")
|
||||
let result = fromCborQ2(strValue, node)
|
||||
|
||||
check result.isSuccess
|
||||
check strValue == "hello"
|
||||
|
||||
# Test with error case
|
||||
block:
|
||||
var intValue: int
|
||||
let node = CborNode(kind: cborText, text: "not an int")
|
||||
let result = fromCborQ2(intValue, node)
|
||||
|
||||
check result.isFailure
|
||||
check $result.error.msg == "deserialization to int failed: expected {cborUnsigned, cborNegative} but got cborText"
|
||||
|
||||
test "parseCborAs with valid input":
|
||||
# Create a valid CBOR object for a Person
|
||||
var mapNode = CborNode(kind: cborMap)
|
||||
mapNode.map = initOrderedTable[CborNode, CborNode]()
|
||||
mapNode.map[CborNode(kind: cborText, text: "a")] = CborNode(
|
||||
kind: cborText, text: "John Doe")
|
||||
mapNode.map[CborNode(kind: cborText, text: "b")] = CborNode(
|
||||
kind: cborUnsigned, uint: 30)
|
||||
mapNode.map[CborNode(kind: cborText, text: "c")] = CborNode(
|
||||
kind: cborSimple, simple: 21) # true
|
||||
var p1: Person
|
||||
p1.name = "John Doe"
|
||||
p1.age = 30
|
||||
p1.isActive = true
|
||||
|
||||
let stream = newStringStream()
|
||||
stream.writeCbor(p1)
|
||||
let cborData = stream.data
|
||||
|
||||
# var cborNode = parseCbor(cborData)
|
||||
# check cborNode.isSuccess
|
||||
# echo cborNode.tryError.msg
|
||||
|
||||
without parsedNode =? parseCbor(cborData), error:
|
||||
echo error.msg
|
||||
|
||||
# Parse directly to Person object
|
||||
var person: Person
|
||||
let result = fromCborQ2(person, parsedNode)
|
||||
|
||||
check result.isSuccess
|
||||
check person.name == "John Doe"
|
||||
check person.age == 30
|
||||
check person.isActive == true
|
||||
|
||||
test "fromCborQ2 with custom hook":
|
||||
# Test with valid point data
|
||||
block:
|
||||
var point: CustomPoint
|
||||
let node = createPointCbor(10, 20)
|
||||
let result = fromCborQ2(point, node)
|
||||
|
||||
check result.isSuccess
|
||||
check point.x == 10
|
||||
check point.y == 20
|
||||
|
||||
# Test with invalid point data
|
||||
block:
|
||||
var point: CustomPoint
|
||||
let elements = @[toCbor(10)]
|
||||
let node = toCbor(elements)
|
||||
let result = fromCborQ2(point, node)
|
||||
|
||||
check result.isFailure
|
||||
# check "Expected array of length 2" in $result.error.msg
|
||||
|
||||
test "fromCborQ2 with complex object":
|
||||
# Create a complex object
|
||||
let point = CustomPoint(x: 15, y: 25)
|
||||
# let obj = CustomObject(name: "Test Object", point: point, color: Green)
|
||||
|
||||
# Create CBOR representation
|
||||
let node = createObjectCbor("Test Object", point, Green)
|
||||
|
||||
# Deserialize
|
||||
var deserializedObj: CustomObject
|
||||
# Check result
|
||||
let result = fromCborQ2(deserializedObj, node)
|
||||
check result.isSuccess
|
||||
check deserializedObj.name == "Test Object"
|
||||
check deserializedObj.point.x == 15
|
||||
check deserializedObj.point.y == 25
|
||||
check deserializedObj.color == Green
|
||||
|
||||
suite "CBOR round-trip for nested composite object":
|
||||
test "serialize and parse nested composite type":
|
||||
var refObj = new Inner
|
||||
refObj.s = "refInner"
|
||||
refObj.nums = @[30, 40]
|
||||
var original = CompositeNested(
|
||||
u: 42,
|
||||
n: -99,
|
||||
b: @[byte 1, byte 2],
|
||||
t: "hi",
|
||||
arr: @[1, 2, 3],
|
||||
tag: 1.5,
|
||||
flag: true,
|
||||
inner: Inner(s: "inner!", nums: @[10, 20]),
|
||||
innerArr: @[
|
||||
Inner(s: "first", nums: @[1, 2]),
|
||||
Inner(s: "second", nums: @[3, 4, 5])
|
||||
],
|
||||
coordinates: (x: 10, y: 20, label: "test"),
|
||||
refInner: refObj
|
||||
)
|
||||
|
||||
# Serialize to CBOR
|
||||
let stream = newStringStream()
|
||||
stream.writeCbor(original)
|
||||
let cborData = stream.data
|
||||
# Parse CBOR back to CborNode
|
||||
let parseResult = parseCbor(cborData)
|
||||
check parseResult.isSuccess
|
||||
let node = parseResult.tryValue
|
||||
|
||||
# Deserialize to CompositeNested object
|
||||
var roundtrip: CompositeNested
|
||||
let deserResult = fromCborQ2(roundtrip, node)
|
||||
check deserResult.isSuccess
|
||||
|
||||
# Check top-level fields
|
||||
check roundtrip.u == original.u
|
||||
check roundtrip.n == original.n
|
||||
check roundtrip.b == original.b
|
||||
check roundtrip.t == original.t
|
||||
check roundtrip.arr == original.arr
|
||||
check abs(roundtrip.tag - original.tag) < 1e-6
|
||||
check roundtrip.flag == original.flag
|
||||
|
||||
# Check nested object
|
||||
check roundtrip.inner.s == original.inner.s
|
||||
check roundtrip.inner.nums == original.inner.nums
|
||||
|
||||
# Check nested array of objects
|
||||
check roundtrip.innerArr.len == original.innerArr.len
|
||||
for i in 0..<roundtrip.innerArr.len:
|
||||
check roundtrip.innerArr[i].s == original.innerArr[i].s
|
||||
check roundtrip.innerArr[i].nums == original.innerArr[i].nums
|
||||
|
||||
check roundtrip.coordinates.x == original.coordinates.x
|
||||
check roundtrip.coordinates.y == original.coordinates.y
|
||||
check roundtrip.coordinates.label == original.coordinates.label
|
||||
|
||||
|
||||
check not roundtrip.refInner.isNil
|
||||
check roundtrip.refInner.s == original.refInner.s
|
||||
check roundtrip.refInner.nums == original.refInner.nums
|
||||
685
tests/cbor/test_vector.json
Normal file
685
tests/cbor/test_vector.json
Normal file
@ -0,0 +1,685 @@
|
||||
[
|
||||
{
|
||||
"cbor": "AA==",
|
||||
"hex": "00",
|
||||
"roundtrip": true,
|
||||
"decoded": 0,
|
||||
"name": "uint_0"
|
||||
},
|
||||
{
|
||||
"cbor": "AQ==",
|
||||
"hex": "01",
|
||||
"roundtrip": true,
|
||||
"decoded": 1,
|
||||
"name": "uint_1"
|
||||
},
|
||||
{
|
||||
"cbor": "Cg==",
|
||||
"hex": "0a",
|
||||
"roundtrip": true,
|
||||
"decoded": 10,
|
||||
"name": "uint_10"
|
||||
},
|
||||
{
|
||||
"cbor": "Fw==",
|
||||
"hex": "17",
|
||||
"roundtrip": true,
|
||||
"decoded": 23,
|
||||
"name": "uint_23"
|
||||
},
|
||||
{
|
||||
"cbor": "GBg=",
|
||||
"hex": "1818",
|
||||
"roundtrip": true,
|
||||
"decoded": 24,
|
||||
"name": "uint_24"
|
||||
},
|
||||
{
|
||||
"cbor": "GBk=",
|
||||
"hex": "1819",
|
||||
"roundtrip": true,
|
||||
"decoded": 25,
|
||||
"name": "uint_25"
|
||||
},
|
||||
{
|
||||
"cbor": "GGQ=",
|
||||
"hex": "1864",
|
||||
"roundtrip": true,
|
||||
"decoded": 100,
|
||||
"name": "uint_100"
|
||||
},
|
||||
{
|
||||
"cbor": "GQPo",
|
||||
"hex": "1903e8",
|
||||
"roundtrip": true,
|
||||
"decoded": 1000,
|
||||
"name": "uint_1000"
|
||||
},
|
||||
{
|
||||
"cbor": "GgAPQkA=",
|
||||
"hex": "1a000f4240",
|
||||
"roundtrip": true,
|
||||
"decoded": 1000000,
|
||||
"name": "uint_1000000"
|
||||
},
|
||||
{
|
||||
"cbor": "GwAAAOjUpRAA",
|
||||
"hex": "1b000000e8d4a51000",
|
||||
"roundtrip": true,
|
||||
"decoded": 1000000000000,
|
||||
"name": "uint_1000000000000"
|
||||
},
|
||||
{
|
||||
"cbor": "IA==",
|
||||
"hex": "20",
|
||||
"roundtrip": true,
|
||||
"decoded": -1,
|
||||
"name": "nint_1"
|
||||
},
|
||||
{
|
||||
"cbor": "KQ==",
|
||||
"hex": "29",
|
||||
"roundtrip": true,
|
||||
"decoded": -10,
|
||||
"name": "nint_10"
|
||||
},
|
||||
{
|
||||
"cbor": "OGM=",
|
||||
"hex": "3863",
|
||||
"roundtrip": true,
|
||||
"decoded": -100,
|
||||
"name": "nint_100"
|
||||
},
|
||||
{
|
||||
"cbor": "OQPn",
|
||||
"hex": "3903e7",
|
||||
"roundtrip": true,
|
||||
"decoded": -1000,
|
||||
"name": "nint_1000"
|
||||
},
|
||||
{
|
||||
"cbor": "+QAA",
|
||||
"hex": "f90000",
|
||||
"roundtrip": true,
|
||||
"decoded": 0.0,
|
||||
"name": "float16_0"
|
||||
},
|
||||
{
|
||||
"cbor": "+YAA",
|
||||
"hex": "f98000",
|
||||
"roundtrip": true,
|
||||
"decoded": -0.0,
|
||||
"name": "float16_neg0"
|
||||
},
|
||||
{
|
||||
"cbor": "+TwA",
|
||||
"hex": "f93c00",
|
||||
"roundtrip": true,
|
||||
"decoded": 1.0,
|
||||
"name": "float16_1"
|
||||
},
|
||||
{
|
||||
"cbor": "+z/xmZmZmZma",
|
||||
"hex": "fb3ff199999999999a",
|
||||
"roundtrip": true,
|
||||
"decoded": 1.1,
|
||||
"name": "float64_1_1"
|
||||
},
|
||||
{
|
||||
"cbor": "+T4A",
|
||||
"hex": "f93e00",
|
||||
"roundtrip": true,
|
||||
"decoded": 1.5,
|
||||
"name": "float16_1_5"
|
||||
},
|
||||
{
|
||||
"cbor": "+Xv/",
|
||||
"hex": "f97bff",
|
||||
"roundtrip": true,
|
||||
"decoded": 65504.0,
|
||||
"name": "float16_65504"
|
||||
},
|
||||
{
|
||||
"cbor": "+kfDUAA=",
|
||||
"hex": "fa47c35000",
|
||||
"roundtrip": true,
|
||||
"decoded": 100000.0,
|
||||
"name": "float32_100000"
|
||||
},
|
||||
{
|
||||
"cbor": "+n9///8=",
|
||||
"hex": "fa7f7fffff",
|
||||
"roundtrip": true,
|
||||
"decoded": 3.4028234663852886e+38,
|
||||
"name": "float32_max"
|
||||
},
|
||||
{
|
||||
"cbor": "+3435DyIAHWc",
|
||||
"hex": "fb7e37e43c8800759c",
|
||||
"roundtrip": true,
|
||||
"decoded": 1e+300,
|
||||
"name": "float64_1e300"
|
||||
},
|
||||
{
|
||||
"cbor": "+QAB",
|
||||
"hex": "f90001",
|
||||
"roundtrip": true,
|
||||
"decoded": 5.960464477539063e-08,
|
||||
"name": "float16_min"
|
||||
},
|
||||
{
|
||||
"cbor": "+QQA",
|
||||
"hex": "f90400",
|
||||
"roundtrip": true,
|
||||
"decoded": 6.103515625e-05,
|
||||
"name": "float16_min_exp"
|
||||
},
|
||||
{
|
||||
"cbor": "+cQA",
|
||||
"hex": "f9c400",
|
||||
"roundtrip": true,
|
||||
"decoded": -4.0,
|
||||
"name": "float32_neg4"
|
||||
},
|
||||
{
|
||||
"cbor": "+8AQZmZmZmZm",
|
||||
"hex": "fbc010666666666666",
|
||||
"roundtrip": true,
|
||||
"decoded": -4.1,
|
||||
"name": "float64_neg4_1"
|
||||
},
|
||||
{
|
||||
"cbor": "+XwA",
|
||||
"hex": "f97c00",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "Infinity",
|
||||
"name": "float16_inf"
|
||||
},
|
||||
{
|
||||
"cbor": "+X4A",
|
||||
"hex": "f97e00",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "NaN",
|
||||
"name": "float16_nan"
|
||||
},
|
||||
{
|
||||
"cbor": "+fwA",
|
||||
"hex": "f9fc00",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "-Infinity",
|
||||
"name": "float16_neginf"
|
||||
},
|
||||
{
|
||||
"cbor": "+n+AAAA=",
|
||||
"hex": "fa7f800000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "Infinity",
|
||||
"name": "float32_inf"
|
||||
},
|
||||
{
|
||||
"cbor": "+n/AAAA=",
|
||||
"hex": "fa7fc00000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "NaN",
|
||||
"name": "float32_nan"
|
||||
},
|
||||
{
|
||||
"cbor": "+v+AAAA=",
|
||||
"hex": "faff800000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "-Infinity",
|
||||
"name": "float32_neginf"
|
||||
},
|
||||
{
|
||||
"cbor": "+3/wAAAAAAAA",
|
||||
"hex": "fb7ff0000000000000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "Infinity",
|
||||
"name": "float64_inf"
|
||||
},
|
||||
{
|
||||
"cbor": "+3/4AAAAAAAA",
|
||||
"hex": "fb7ff8000000000000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "NaN",
|
||||
"name": "float64_nan"
|
||||
},
|
||||
{
|
||||
"cbor": "+//wAAAAAAAA",
|
||||
"hex": "fbfff0000000000000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "-Infinity",
|
||||
"name": "float64_neginf"
|
||||
},
|
||||
{
|
||||
"cbor": "9A==",
|
||||
"hex": "f4",
|
||||
"roundtrip": true,
|
||||
"decoded": false,
|
||||
"name": "false"
|
||||
},
|
||||
{
|
||||
"cbor": "9Q==",
|
||||
"hex": "f5",
|
||||
"roundtrip": true,
|
||||
"decoded": true,
|
||||
"name": "true"
|
||||
},
|
||||
{
|
||||
"cbor": "9g==",
|
||||
"hex": "f6",
|
||||
"roundtrip": true,
|
||||
"decoded": null,
|
||||
"name": "null"
|
||||
},
|
||||
{
|
||||
"cbor": "9w==",
|
||||
"hex": "f7",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "undefined",
|
||||
"name": "undefined"
|
||||
},
|
||||
{
|
||||
"cbor": "8A==",
|
||||
"hex": "f0",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "simple(16)",
|
||||
"name": "simple_16"
|
||||
},
|
||||
{
|
||||
"cbor": "+Bg=",
|
||||
"hex": "f818",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "simple(24)",
|
||||
"name": "simple_24"
|
||||
},
|
||||
{
|
||||
"cbor": "+P8=",
|
||||
"hex": "f8ff",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "simple(255)",
|
||||
"name": "simple_255"
|
||||
},
|
||||
{
|
||||
"cbor": "wHQyMDEzLTAzLTIxVDIwOjA0OjAwWg==",
|
||||
"hex": "c074323031332d30332d32315432303a30343a30305a",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "0(\"2013-03-21T20:04:00Z\")",
|
||||
"name": "tag0_datetime"
|
||||
},
|
||||
{
|
||||
"cbor": "wRpRS2ew",
|
||||
"hex": "c11a514b67b0",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "1(1363896240)",
|
||||
"name": "tag1_epoch"
|
||||
},
|
||||
{
|
||||
"cbor": "wftB1FLZ7CAAAA==",
|
||||
"hex": "c1fb41d452d9ec200000",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "1(1363896240.5)",
|
||||
"name": "tag1_epoch_float"
|
||||
},
|
||||
{
|
||||
"cbor": "10QBAgME",
|
||||
"hex": "d74401020304",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "23(h'01020304')",
|
||||
"name": "tag23_h64"
|
||||
},
|
||||
{
|
||||
"cbor": "2BhFZElFVEY=",
|
||||
"hex": "d818456449455446",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "24(h'6449455446')",
|
||||
"name": "tag24_b64url"
|
||||
},
|
||||
{
|
||||
"cbor": "2CB2aHR0cDovL3d3dy5leGFtcGxlLmNvbQ==",
|
||||
"hex": "d82076687474703a2f2f7777772e6578616d706c652e636f6d",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "32(\"http://www.example.com\")",
|
||||
"name": "tag32_uri"
|
||||
},
|
||||
{
|
||||
"cbor": "QA==",
|
||||
"hex": "40",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "h''",
|
||||
"name": "bstr_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "RAECAwQ=",
|
||||
"hex": "4401020304",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "h'01020304'",
|
||||
"name": "bstr_bytes"
|
||||
},
|
||||
{
|
||||
"cbor": "YA==",
|
||||
"hex": "60",
|
||||
"roundtrip": true,
|
||||
"decoded": "",
|
||||
"name": "tstr_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "YWE=",
|
||||
"hex": "6161",
|
||||
"roundtrip": true,
|
||||
"decoded": "a",
|
||||
"name": "tstr_a"
|
||||
},
|
||||
{
|
||||
"cbor": "ZElFVEY=",
|
||||
"hex": "6449455446",
|
||||
"roundtrip": true,
|
||||
"decoded": "IETF",
|
||||
"name": "tstr_ietf"
|
||||
},
|
||||
{
|
||||
"cbor": "YiJc",
|
||||
"hex": "62225c",
|
||||
"roundtrip": true,
|
||||
"decoded": "\"\\",
|
||||
"name": "tstr_escaped"
|
||||
},
|
||||
{
|
||||
"cbor": "YsO8",
|
||||
"hex": "62c3bc",
|
||||
"roundtrip": true,
|
||||
"decoded": "\u00fc",
|
||||
"name": "tstr_u00fc"
|
||||
},
|
||||
{
|
||||
"cbor": "Y+awtA==",
|
||||
"hex": "63e6b0b4",
|
||||
"roundtrip": true,
|
||||
"decoded": "\u6c34",
|
||||
"name": "tstr_u6c34"
|
||||
},
|
||||
{
|
||||
"cbor": "ZPCQhZE=",
|
||||
"hex": "64f0908591",
|
||||
"roundtrip": true,
|
||||
"decoded": "\ud800\udd51",
|
||||
"name": "tstr_u10151"
|
||||
},
|
||||
{
|
||||
"cbor": "gA==",
|
||||
"hex": "80",
|
||||
"roundtrip": true,
|
||||
"decoded": [],
|
||||
"name": "array_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "gwECAw==",
|
||||
"hex": "83010203",
|
||||
"roundtrip": true,
|
||||
"decoded": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"name": "array_123"
|
||||
},
|
||||
{
|
||||
"cbor": "gwGCAgOCBAU=",
|
||||
"hex": "8301820203820405",
|
||||
"roundtrip": true,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "array_nested"
|
||||
},
|
||||
{
|
||||
"cbor": "mBkBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgYGBk=",
|
||||
"hex": "98190102030405060708090a0b0c0d0e0f101112131415161718181819",
|
||||
"roundtrip": true,
|
||||
"decoded": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25
|
||||
],
|
||||
"name": "array_25items"
|
||||
},
|
||||
{
|
||||
"cbor": "oA==",
|
||||
"hex": "a0",
|
||||
"roundtrip": true,
|
||||
"decoded": {},
|
||||
"name": "map_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "ogECAwQ=",
|
||||
"hex": "a201020304",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "{1: 2, 3: 4}",
|
||||
"name": "map_pairs"
|
||||
},
|
||||
{
|
||||
"cbor": "omFhAWFiggID",
|
||||
"hex": "a26161016162820203",
|
||||
"roundtrip": true,
|
||||
"decoded": {
|
||||
"a": 1,
|
||||
"b": [
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"name": "map_nested"
|
||||
},
|
||||
{
|
||||
"cbor": "gmFhoWFiYWM=",
|
||||
"hex": "826161a161626163",
|
||||
"roundtrip": true,
|
||||
"decoded": [
|
||||
"a",
|
||||
{
|
||||
"b": "c"
|
||||
}
|
||||
],
|
||||
"name": "map_mixed"
|
||||
},
|
||||
{
|
||||
"cbor": "pWFhYUFhYmFCYWNhQ2FkYURhZWFF",
|
||||
"hex": "a56161614161626142616361436164614461656145",
|
||||
"roundtrip": true,
|
||||
"decoded": {
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"e": "E"
|
||||
},
|
||||
"name": "map_strings"
|
||||
},
|
||||
{
|
||||
"cbor": "X0IBAkMDBAX/",
|
||||
"hex": "5f42010243030405ff",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "h'0102030405'",
|
||||
"name": "indef_tstr"
|
||||
},
|
||||
{
|
||||
"cbor": "f2VzdHJlYWRtaW5n/w==",
|
||||
"hex": "7f657374726561646d696e67ff",
|
||||
"roundtrip": false,
|
||||
"decoded": "streaming",
|
||||
"name": "indef_array_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "n/8=",
|
||||
"hex": "9fff",
|
||||
"roundtrip": false,
|
||||
"decoded": [],
|
||||
"name": "indef_array_1"
|
||||
},
|
||||
{
|
||||
"cbor": "nwGCAgOfBAX//w==",
|
||||
"hex": "9f018202039f0405ffff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "indef_array_2"
|
||||
},
|
||||
{
|
||||
"cbor": "nwGCAgOCBAX/",
|
||||
"hex": "9f01820203820405ff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "indef_array_3"
|
||||
},
|
||||
{
|
||||
"cbor": "gwGCAgOfBAX/",
|
||||
"hex": "83018202039f0405ff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "indef_array_4"
|
||||
},
|
||||
{
|
||||
"cbor": "gwGfAgP/ggQF",
|
||||
"hex": "83019f0203ff820405",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "indef_array_long"
|
||||
},
|
||||
{
|
||||
"cbor": "nwECAwQFBgcICQoLDA0ODxAREhMUFRYXGBgYGf8=",
|
||||
"hex": "9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25
|
||||
],
|
||||
"name": "indef_map_1"
|
||||
},
|
||||
{
|
||||
"cbor": "v2FhAWFinwID//8=",
|
||||
"hex": "bf61610161629f0203ffff",
|
||||
"roundtrip": false,
|
||||
"decoded": {
|
||||
"a": 1,
|
||||
"b": [
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"name": "indef_map_2"
|
||||
},
|
||||
{
|
||||
"cbor": "gmFhv2FiYWP/",
|
||||
"hex": "826161bf61626163ff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
"a",
|
||||
{
|
||||
"b": "c"
|
||||
}
|
||||
],
|
||||
"name": "indef_map_3"
|
||||
},
|
||||
{
|
||||
"cbor": "v2NGdW71Y0FtdCH/",
|
||||
"hex": "bf6346756ef563416d7421ff",
|
||||
"roundtrip": false,
|
||||
"decoded": {
|
||||
"Fun": true,
|
||||
"Amt": -2
|
||||
},
|
||||
"name": "indef_map_4"
|
||||
}
|
||||
]
|
||||
@ -3,5 +3,7 @@ import ./json/testDeserializeModes
|
||||
import ./json/testPragmas
|
||||
import ./json/testSerialize
|
||||
import ./json/testSerializeModes
|
||||
import ./cbor/testDeserialize
|
||||
import ./cbor/test
|
||||
|
||||
{.warning[UnusedImport]: off.}
|
||||
|
||||
@ -9,3 +9,4 @@ requires "questionable >= 0.10.13 & < 0.11.0"
|
||||
task test, "Run the test suite":
|
||||
exec "nimble install -d -y"
|
||||
exec "nim c -r test"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user