feat: add CBOR serialization and deserialization support

This commit is contained in:
munna0908 2025-05-21 03:22:35 +05:30
parent dcfec8aaa3
commit f1ec805ec0
No known key found for this signature in database
GPG Key ID: 2FFCD637E937D3E6
22 changed files with 2322 additions and 104 deletions

3
.gitignore vendored
View File

@ -5,4 +5,5 @@ nimble.develop
nimble.paths
.idea
vendor/
.vscode/
.vscode/
nimbledeps

View File

@ -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
View 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
View 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
View 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
View 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
View 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)

View File

@ -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
View 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

View File

@ -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

View File

@ -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
View 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)

View File

@ -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 == ""

View File

@ -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"

View File

@ -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

View File

@ -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
View 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

View 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
View 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"
}
]

View File

@ -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.}

View File

@ -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"