Merge 2d8fa4d940a19a59551a59d79ca4dc38ce6ef104 into 5ced7c88b97d99c582285ce796957fb71fd42434

This commit is contained in:
munna0908 2025-06-17 07:07:20 +00:00 committed by GitHub
commit 82252b8eb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 3626 additions and 439 deletions

3
.gitignore vendored
View File

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

481
README.md
View File

@ -1,412 +1,131 @@
# nim-serde
Easy-to-use json serialization capabilities, and a drop-in replacement for `std/json`.
A serialization and deserialization library for Nim supporting multiple formats.
## Quick examples
## Supported Serialization Formats
Opt-in serialization by default:
nim-serde currently supports the following serialization formats:
- **JSON**: A text-based data interchange format. [See JSON](serde/json/README.md) for details.
- **CBOR**: A binary data format following RFC 8949. [See CBOR](serde/cbor/README.md) for details.
## Quick Examples
### JSON Serialization and Deserialization
```nim
import pkg/serde/json
import ./serde/json
import questionable/results
type MyType = object
field1 {.serialize.}: bool
field2: bool
# Define a type
type Person = object
name {.serialize.}: string
age {.serialize.}: int
address: string # By default, serde will not serialize non-annotated fields (OptIn mode)
assert MyType(field1: true, field2: true).toJson == """{"field1":true}"""
```
# Create an instance
let person = Person(name: "John Doe", age: 30, address: "123 Main St")
Opt-out deserialization by default:
# Serialization
echo "JSON Serialization Example"
let jsonString = person.toJson(pretty = true)
echo jsonString
```nim
import pkg/serde/json
# All fields deserialized, as none are ignored
type MyType1 = object
field1: bool
field2: bool
let jsn1 = """{
"field1": true,
"field2": true
}"""
assert !MyType1.fromJson(jsn1) == MyType1(field1: true, field2: true)
# Don't deserialize ignored fields in OptOut mode
type MyType2 = object
field1 {.deserialize(ignore=true).}: bool
field2: bool
let jsn2 = """{
"field1": true,
"field2": true,
"extra": "extra fields don't error in OptOut mode"
}"""
assert !MyType2.fromJson(jsn2) == MyType2(field1: false, field2: true)
# Note, the ! operator is part of https://github.com/codex-storage/questionable, which retrieves a value if set
```
Serialize all fields of a type (OptOut mode):
```nim
import pkg/serde/json
type MyType {.serialize.} = object
field1: int
field2: int
assert MyType(field1: 1, field2: 2).toJson == """{"field1":1,"field2":2}"""
```
Alias field names in both directions!
```nim
import pkg/serde/json
type MyType {.serialize.} = object
field1 {.serialize("othername"),deserialize("takesprecedence").}: int
field2: int
assert MyType(field1: 1, field2: 2).toJson == """{"othername":1,"field2":2}"""
let jsn = """{
"othername": 1,
"field2": 2,
"takesprecedence": 3
}"""
assert !MyType.fromJson(jsn) == MyType(field1: 3, field2: 2)
```
Supports strict mode, where type fields and json fields must match
```nim
import pkg/serde/json
type MyType {.deserialize(mode=Strict).} = object
field1: int
field2: int
let jsn = """{
"field1": 1,
"field2": 2,
"extra": 3
}"""
let res = MyType.fromJson(jsn)
assert res.isFailure
assert res.error of SerdeError
assert res.error.msg == "json field(s) missing in object: {\"extra\"}"
```
## Serde modes
`nim-serde` uses three different modes to control de/serialization:
```nim
OptIn
OptOut
Strict
```
Modes can be set in the `{.serialize.}` and/or `{.deserialize.}` pragmas on type
definitions. Each mode has a different meaning depending on if the type is being
serialized or deserialized. Modes can be set by setting `mode` in the `serialize` or
`deserialize` pragma annotation, eg:
```nim
type MyType {.serialize(mode=Strict).} = object
field1: bool
field2: bool
```
### Modes reference
| | serialize | deserialize |
|:-------------------|:------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `SerdeMode.OptOut` | All object fields will be serialized, except fields marked with `{.serialize(ignore=true).}`. | All json keys will be deserialized, except fields marked with `{.deserialize(ignore=true).}`. No error if extra json fields exist. |
| `SerdeMode.OptIn` | Only fields marked with `{.serialize.}` will be serialized. Fields marked with `{.serialize(ignore=true).}` will not be serialized. | Only fields marked with `{.deserialize.}` will be deserialized. Fields marked with `{.deserialize(ignore=true).}` will not be deserialized. A `SerdeError` error is raised if the field is missing in json. |
| `SerdeMode.Strict` | All object fields will be serialized, regardless if the field is marked with `{.serialize(ignore=true).}`. | Object fields and json fields must match exactly, otherwise a `SerdeError` is raised. |
## Default modes
`nim-serde` will de/serialize types if they are not annotated with `serialize` or
`deserialize`, but will assume a default mode. By default, with no pragmas specified,
`serde` will always serialize in `OptIn` mode, meaning any fields to b Additionally, if
the types are annotated, but a mode is not specified, `serde` will assume a (possibly
different) default mode.
```nim
# Type is not annotated
# A default mode of OptIn (for serialize) and OptOut (for deserialize) is assumed.
type MyObj = object
field1: bool
field2: bool
# Type is annotated, but mode not specified
# A default mode of OptOut is assumed for both serialize and deserialize.
type MyObj {.serialize, deserialize.} = object
field1: bool
field2: bool
```
### Default mode reference
| | serialize | deserialize |
|:------------------------------|:----------|:------------|
| Default (no pragma) | `OptIn` | `OptOut` |
| Default (pragma, but no mode) | `OptOut` | `OptOut` |
## Serde field options
Type fields can be annotated with `{.serialize.}` and `{.deserialize.}` and properties
can be set on these pragmas, determining de/serialization behavior.
For example,
```nim
import pkg/serde/json
type
Person {.serialize(mode=OptOut), deserialize(mode=OptIn).} = object
id {.serialize(ignore=true), deserialize(key="personid").}: int
name: string
birthYear: int
address: string
phone: string
let person = Person(
name: "Lloyd Christmas",
birthYear: 1970,
address: "123 Sesame Street, Providence, Rhode Island 12345",
phone: "555-905-justgivemethedamnnumber!⛽️🔥")
let createRequest = """{
"name": "Lloyd Christmas",
"birthYear": 1970,
"address": "123 Sesame Street, Providence, Rhode Island 12345",
"phone": "555-905-justgivemethedamnnumber!⛽️🔥"
# Verify serialization output
let expectedJson = """{
"name": "John Doe",
"age": 30
}"""
assert person.toJson(pretty=true) == createRequest
assert jsonString == expectedJson
# Deserialization
echo "\nJSON Deserialization Example"
let jsonData = """{"name":"Jane Doe","age":28,"address":"456 Oak Ave"}"""
let result = Person.fromJson(jsonData)
# check if deserialization was successful
assert result.isSuccess
# get the deserialized value
let parsedPerson = !result
echo parsedPerson
#[
Expected Output:
Person(
name: "Jane Doe",
age: 28,
address: "456 Oak Ave"
)
]#
let createResponse = """{
"personid": 1,
"name": "Lloyd Christmas",
"birthYear": 1970,
"address": "123 Sesame Street, Providence, Rhode Island 12345",
"phone": "555-905-justgivemethedamnnumber!⛽️🔥"
}"""
assert !Person.fromJson(createResponse) == Person(id: 1)
```
### `key`
Specifying a `key`, will alias the field name. When seriazlizing, json will be written
with `key` instead of the field name. When deserializing, the json must contain `key`
for the field to be deserialized.
### `ignore`
Specifying `ignore`, will prevent de/serialization on the field.
### Serde field options reference
| | serialize | deserialize |
|:---------|:-----------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------|
| `key` | aliases the field name in json | deserializes the field if json contains `key` |
| `ignore` | <li>**OptOut:** field not serialized</li><li>**OptIn:** field not serialized</li><li>**Strict:** field serialized</li> | <li>**OptOut:** field not deserialized</li><li>**OptIn:** field not deserialized</li><li>**Strict:** field deserialized</li> |
## Deserialization
`serde` deserializes using `fromJson`, and in all instances returns `Result[T,
CatchableError]`, where `T` is the type being deserialized. For example:
### CBOR Serialization and Deserialization
```nim
type MyType = object
field1: bool
field2: bool
import ./serde/cbor
import questionable/results
import std/streams
let jsn1 = """{
"field1": true,
"field2": true
}"""
# Define a type
type Person = object
name: string # Unlike JSON, CBOR always serializes all fields, and they do not need to be annotated
age: int
address: string
# Create an instance
let person = Person(name: "John Doe", age: 30, address: "123 Main St")
# Serialization using Stream API
echo "CBOR Stream API Serialization"
let stream = newStringStream()
let writeResult = stream.writeCbor(person)
assert writeResult.isSuccess
# Serialization using toCbor function
echo "\nCBOR toCbor Function Serialization"
let cborResult = toCbor(person)
assert cborResult.isSuccess
let serializedCbor = !cborResult
# Deserialization
echo "\nCBOR Deserialization"
let personResult = Person.fromCbor(serializedCbor)
# check if deserialization was successful
assert personResult.isSuccess
# get the deserialized value
let parsedPerson = !personResult
echo parsedPerson
#[
Expected Output:
Person(
name: "John Doe",
age: 30,
address: "123 Main St"
)
]#
assert !MyType.fromJson(jsn1) == MyType(field1: true, field2: true)
```
If there was an error during deserialization, the result of `fromJson` will contain it:
Refer to the [json](serde/json/README.md) and [cbor](serde/cbor/README.md) files for more comprehensive examples.
```nim
import pkg/serde/json
## Known Issues
type MyType {.deserialize(mode=Strict).} = object
field1: int
field2: int
There is a known issue when using mixins with generic overloaded procs like `fromJson`. At the time of mixin call, only the `fromJson` overloads in scope of the called mixin are available to be dispatched at runtime. There could be other `fromJson` overloads declared in other modules, but are not in scope at the time the mixin was called.
let jsn = """{
"field1": 1,
"field2": 2,
"extra": 3
}"""
Therefore, anytime `fromJson` is called targeting a declared overload, it may or may not be dispatchable. This can be worked around by forcing the `fromJson` overload into scope at compile time. For example, in your application where the `fromJson` overload is defined, at the bottom of the module add:
let res = MyType.fromJson(jsn)
assert res.isFailure
assert res.error of SerdeError
assert res.error.msg == "json field(s) missing in object: {\"extra\"}"
```
## Custom types
If `serde` can't de/serialize a custom type, de/serialization can be supported by
overloading `%` and `fromJson`. For example:
```nim
type
Address* = distinct array[20, byte]
SerializationError* = object of CatchableError
func `%`*(address: Address): JsonNode =
%($address)
func fromJson(_: type Address, json: JsonNode): ?!Address =
expectJsonKind(Address, JString, json)
without address =? Address.init(json.getStr), error:
return failure newException(SerializationError,
"Failed to convert '" & $json & "' to Address: " & error.msg)
success address
```
## Serializing to string (`toJson`)
`toJson` is a shortcut for serializing an object into its serialized string
representation:
```nim
import pkg/serde/json
type MyType {.serialize.} = object
field1: string
field2: bool
let mt = MyType(field1: "hw", field2: true)
assert mt.toJson == """{"field1":"hw","field2":true}"""
```
This comes in handy, for example, when sending API responses:
```nim
let availability = getAvailability(...)
return RestApiResponse.response(availability.toJson,
contentType="application/json")
```
## `std/json` drop-in replacment
`nim-serde` can be used as a drop-in replacement for the [standard library's `json`
module](https://nim-lang.org/docs/json.html), with a few notable improvements.
Instead of importing `std/json` into your application, `pkg/serde/json` can be imported
instead:
```diff
- import std/json
+ import pkg/serde/json
```
As with `std/json`, `%` can be used to serialize a type into a `JsonNode`:
```nim
import pkg/serde/json
assert %"hello" == newJString("hello")
```
And `%*` can be used to serialize objects:
```nim
import pkg/serde/json
let expected = newJObject()
expected["hello"] = newJString("world")
assert %*{"hello": "world"} == expected
```
As well, serialization of types can be overridden, and serialization of custom types can
be introduced. Here, we are overriding the serialization of `int`:
```nim
import pkg/serde/json
func `%`(i: int): JsonNode =
newJInt(i + 1)
assert 1.toJson == "2"
```
## `parseJson` and exception tracking
Unfortunately, `std/json`'s `parseJson` can raise an `Exception`, so proper exception
tracking breaks, eg
```nim
## Fails to compile:
## Error: parseJson(me, false, false) can raise an unlisted exception: Exception
import std/json
{.push raises:[].}
type
MyAppError = object of CatchableError
proc parseMe(me: string): JsonNode =
try:
return me.parseJson
except CatchableError as error:
raise newException(MyAppError, error.msg)
assert """{"hello":"world"}""".parseMe == %* { "hello": "world" }
```
This is due to `std/json`'s `parseJson` incorrectly raising `Exception`. This can be
worked around by instead importing `serde` and calling its `JsonNode.parse` routine.
Note that `serde`'s `JsonNode.parse` returns a `Result[JsonNode, CatchableError]`
instead of just a plain `JsonNode` object as in `std/json`'s `parseJson`:
```nim
import pkg/serde/json
{.push raises:[].}
type
MyAppError = object of CatchableError
proc parseMe(me: string): JsonNode {.raises: [MyAppError].} =
without parsed =? JsonNode.parse(me), error:
raise newException(MyAppError, error.msg)
parsed
assert """{"hello":"world"}""".parseMe == %* { "hello": "world" }
```
## Known issues
There is a known issue when using mixins with generic overloaded procs like
`fromJson`. At the time of mixin call, only the `fromJson` overloads in scope of
the called mixin are available to be dispatched at runtime. There could be other
`fromJson` overloads declared in other modules, but are not in scope at the time
the mixin was called. Therefore, anytime `fromJson` is called targeting a
declared overload, it may or may not be dispatchable. This can be worked around
by forcing the `fromJson` overload into scope at compile time. For example, in
your application where the `fromJson` overload is defined, at the bottom of the
module add:
```nim
static: MyType.fromJson("")
```
This will ensure that the `MyType.fromJson` overload is dispatchable.
The basic types that serde supports should already have their overloads forced
in scope in [the `deserializer`
module](./serde/json/deserializer.nim#L340-L356).
The basic types that serde supports should already have their overloads forced in scope in [the `deserializer` module](./serde/json/deserializer.nim#L340-L356).
For an illustration of the problem, please see this [narrow example](https://github.com/gmega/serialization-bug/tree/main/narrow) by [@gmega](https://github.com/gmega).

View File

@ -1,5 +1,7 @@
--styleCheck:usages
--styleCheck:error
--styleCheck:
usages
--styleCheck:
error
# begin Nimble config (version 1)
when fileExists("nimble.paths"):

View File

@ -1,3 +1,5 @@
import ./serde/json
import ./serde/cbor
export json
export cbor

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

379
serde/cbor/README.md Normal file
View File

@ -0,0 +1,379 @@
# nim-serde CBOR
The CBOR module in nim-serde provides serialization and deserialization for Nim values following [RFC 8949](https://www.rfc-editor.org/rfc/rfc8949.html). Unlike the JSON implementation, CBOR offers a stream-based API that enables direct binary serialization without intermediate representations.
> Note: While the JSON implementation supports serde modes via pragmas, the current CBOR implementation does not support the `serialize` and `deserialize` pragmas. The library will raise an assertion error if you try to use these pragmas with CBOR serialization.
## Table of Contents
- [nim-serde CBOR](#nim-serde-cbor)
- [Table of Contents](#table-of-contents)
- [Serialization API](#serialization-api)
- [Stream API: Primitive Type and Sequence Serialization](#stream-api-primitive-type-and-sequence-serialization)
- [Stream API: Object Serialization](#stream-api-object-serialization)
- [Stream API: Custom Type Serialization](#stream-api-custom-type-serialization)
- [Serialization without Stream API: `toCbor`](#serialization-without-stream-api-tocbor)
- [Working with CborNode](#working-with-cbornode)
- [Convenience Functions for CborNode](#convenience-functions-for-cbornode)
- [Deserialization API](#deserialization-api)
- [Basic Deserialization with `fromCbor`](#basic-deserialization-with-fromcbor)
- [Error Handling](#error-handling)
- [Parsing CBOR with `parseCbor`](#parsing-cbor-with-parsecbor)
- [Custom Type Deserialization](#custom-type-deserialization)
- [Implementation Details](#implementation-details)
- [Current Limitations](#current-limitations)
## Serialization API
The nim-serde CBOR serialization API provides several ways to convert Nim values to CBOR.
### Stream API: Primitive Type and Sequence Serialization
The `writeCbor` function writes Nim values to a stream in CBOR format:
```nim
import pkg/serde/cbor
import pkg/questionable/results
import std/streams
# Create a stream to write to
let stream = newStringStream()
# Basic types
discard stream.writeCbor(42) # Unsigned integer
discard stream.writeCbor(-10) # Negative integer
discard stream.writeCbor(3.14) # Float
discard stream.writeCbor("hello") # String
discard stream.writeCbor(true) # Boolean
# Arrays and sequences
discard stream.writeCbor(@[1, 2, 3]) # Sequence
# Get the serialized CBOR data
let cborData = stream.data
```
### Stream API: Object Serialization
Objects can be serialized to CBOR format using the stream API:
```nim
import pkg/serde/cbor
import pkg/questionable/results
import std/streams
type Person = object
name: string
age: int
isActive: bool
let person = Person(
name: "John",
age: 30,
isActive: true
)
# Serialize the object to CBOR
let stream = newStringStream()
discard stream.writeCbor(person)
# Get the serialized CBOR data
let cborData = stream.data
```
### Stream API: Custom Type Serialization
You can extend nim-serde to support custom types by defining your own `writeCbor` procs:
```nim
import pkg/serde/cbor
import pkg/questionable/results
import std/streams
import std/strutils
# Define a custom type
type
UserId = distinct int
# Custom serialization for UserId
proc writeCbor*(str: Stream, id: UserId): ?!void =
# Write as a CBOR text string with a prefix
str.writeCbor("user-" & $int(id))
# Test serialization
let userId = UserId(42)
let stream = newStringStream()
discard stream.writeCbor(userId)
let cborData = stream.data
# Test in object context
type User = object
id: UserId
name: string
let user = User(id: UserId(123), name: "John")
let userStream = newStringStream()
discard userStream.writeCbor(user)
let userCborData = userStream.data
```
### Serialization without Stream API: `toCbor`
The `toCbor` function can be used to directly convert a Nim value to CBOR binary data:
```nim
import pkg/serde/cbor
import pkg/questionable/results
type Person = object
name: string
age: int
isActive: bool
let person = Person(
name: "John",
age: 30,
isActive: true
)
# Convert to CBOR binary data
let result = toCbor(person)
assert result.isSuccess
let cborData = !result
```
### Working with CborNode
The `CborNode` type represents CBOR data in memory and can be manipulated directly:
```nim
import pkg/serde/cbor
import pkg/questionable/results
import std/tables
# Create CBOR nodes
let textNode = CborNode(kind: cborText, text: "hello")
let intNode = CborNode(kind: cborUnsigned, uint: 42'u64)
let floatNode = CborNode(kind: cborFloat, float: 3.14)
# Create an array
var arrayNode = CborNode(kind: cborArray)
arrayNode.seq = @[textNode, intNode, floatNode]
# Create a map with text keys and boolean values
var mapNode = CborNode(kind: cborMap)
mapNode.map = initOrderedTable[CborNode, CborNode]()
# Boolean values are represented as simple values (21 for true, 20 for false)
mapNode.map[CborNode(kind: cborText, text: "a")] = CborNode(kind: cborSimple, simple: 21) # true
mapNode.map[CborNode(kind: cborText, text: "b")] = CborNode(kind: cborSimple, simple: 20) # false
# Convert to CBOR binary data
let result = toCbor(mapNode)
assert result.isSuccess
let cborData = !result
```
### Convenience Functions for CborNode
The library provides convenience functions for creating CBOR nodes:
```nim
import pkg/serde/cbor
import pkg/questionable/results
# Initialize CBOR nodes
let bytesNode = initCborBytes(@[byte 1, byte 2, byte 3])
let textNode = initCborText("hello")
let arrayNode = initCborArray()
let mapNode = initCborMap()
# Convert values to CborNode
let intNodeResult = toCborNode(42)
assert intNodeResult.isSuccess
let intNode = !intNodeResult
let strNodeResult = toCborNode("hello")
assert strNodeResult.isSuccess
let strNode = !strNodeResult
let boolNodeResult = toCborNode(true)
assert boolNodeResult.isSuccess
let boolNode = !boolNodeResult
```
## Deserialization API
The nim-serde CBOR deserialization API provides ways to convert CBOR data back to Nim values.
### Basic Deserialization with `fromCbor`
The `fromCbor` function converts CBOR data to Nim values:
```nim
import pkg/serde/cbor
import pkg/questionable/results
import std/streams
# Create some CBOR data
let stream = newStringStream()
discard stream.writeCbor(42)
let cborData = stream.data
# Parse the CBOR data into a CborNode
try:
let node = parseCbor(cborData)
# Deserialize the CborNode to a Nim value
let intResult = int.fromCbor(node)
assert intResult.isSuccess
let value = !intResult
assert value == 42
# You can also deserialize to other types
# For example, if cborData contained a string:
# let strResult = string.fromCbor(node)
# assert strResult.isSuccess
# let strValue = !strResult
# Deserialize to an object
type Person = object
name: string
age: int
isActive: bool
let personResult = Person.fromCbor(node)
assert personResult.isSuccess
let person = !personResult
# Verify the deserialized data
assert person.name == "John"
assert person.age == 30
assert person.isActive == true
```
### Error Handling
Deserialization returns a `Result` type from the `questionable` library, allowing for safe error handling:
```nim
import pkg/serde/cbor
import pkg/questionable/results
# Invalid CBOR data for an integer
let invalidNode = CborNode(kind: cborText, text: "not an int")
let result = int.fromCbor(invalidNode)
# Check for failure
assert result.isFailure
echo result.error.msg
# Output: "deserialization to int failed: expected {cborUnsigned, cborNegative} but got cborText"
```
### Parsing CBOR with `parseCbor`
The `parseCbor` function parses CBOR binary data into a `CborNode`:
```nim
import pkg/serde/cbor
import pkg/questionable/results
# Parse CBOR data
let node = parseCbor(cborData)
# Check node type and access data
case node.kind
of cborUnsigned:
echo "Unsigned integer: ", node.uint
of cborNegative:
echo "Negative integer: ", node.int
of cborText:
echo "Text: ", node.text
of cborArray:
echo "Array with ", node.seq.len, " items"
of cborMap:
echo "Map with ", node.map.len, " pairs"
else:
echo "Other CBOR type: ", node.kind
```
### Custom Type Deserialization
You can extend nim-serde to support custom type deserialization by defining your own `fromCbor` procs:
```nim
import pkg/serde/cbor
import pkg/questionable/results
import std/strutils
# Define a custom type
type
UserId = distinct int
# Custom deserialization for UserId
proc fromCbor*(_: type UserId, n: CborNode): ?!UserId =
if n.kind != cborText:
return failure(newSerdeError("Expected string for UserId, got " & $n.kind))
let str = n.text
if str.startsWith("user-"):
let idStr = str[5..^1]
try:
let id = parseInt(idStr)
success(UserId(id))
except ValueError:
failure(newSerdeError("Invalid UserId format: " & str))
else:
failure(newSerdeError("UserId must start with 'user-' prefix"))
# Test deserialization
let node = parseCbor(cborData) # Assuming cborData contains a serialized UserId
let result = UserId.fromCbor(node)
assert result.isSuccess
assert int(!result) == 42
# Test deserialization in object context
type User = object
id: UserId
name: string
let userNode = parseCbor(userCborData) # Assuming userCborData contains a serialized User
let userResult = User.fromCbor(userNode)
assert userResult.isSuccess
assert int((!userResult).id) == 123
assert (!userResult).name == "John"
```
## Implementation Details
The CBOR serialization in nim-serde follows a stream-based approach:
```
# Serialization flow
Nim value → writeCbor → CBOR binary data
# Deserialization flow
CBOR binary data → parseCbor (CborNode) → fromCbor → Nim value
```
Unlike the JSON implementation which uses the `%` operator pattern, the CBOR implementation uses a hook-based approach:
1. The `writeCbor` function writes Nim values directly to a stream in CBOR format
2. Custom types can be supported by defining `writeCbor` procs for those types
3. The `toCbor` function provides a convenient way to convert values to CBOR binary data
For deserialization, the library parses CBOR data into a `CborNode` representation, which can then be converted to Nim values using the `fromCbor` function. This approach allows for flexible handling of CBOR data while maintaining type safety.
### Current Limitations
This implementation does not support the `serialize` and `deserialize` pragmas. The library will raise an assertion error if you try to use these pragmas with CBOR serialization.
```nim
import pkg/serde/cbor
type Person {.serialize(mode = OptOut).} = object # This will raise an assertion error
name: string
age: int
isActive: bool
```

645
serde/cbor/deserializer.nim Normal file
View File

@ -0,0 +1,645 @@
# This file is a modified version of Emery Hemingways CBOR library for Nim,
# originally available at https://github.com/ehmry/cbor-nim and released under The Unlicense.
import std/[math, streams, options, tables, strutils, times, typetraits, macros]
import ./types as cborTypes
import ./helpers
import ../utils/types
import ../utils/pragmas
import ./errors
import pkg/questionable
import pkg/questionable/results
export results
export types
export pragmas
export cborTypes
export macros
{.push raises: [].}
func isIndefinite*(c: CborParser): bool {.inline.} =
## Return true if the parser is positioned on an item of indefinite length.
c.minor == 31
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) {.raises: [CborParseError].} =
## 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:
raise newCborError("unhandled major type " & $mb)
except IOError as e:
raise newException(CborParseError, e.msg, e)
except OSError as e:
raise newException(CborParseError, e.msg, e)
proc nextUInt*(c: var CborParser): BiggestUInt {.raises: [CborParseError].} =
## Parse the integer value that the parser is positioned on.
parseAssert(
c.kind == CborEventKind.cborPositive, "Expected positive integer, got " & $c.kind
)
result = c.intVal.BiggestUInt
c.next()
proc nextInt*(c: var CborParser): BiggestInt {.raises: [CborParseError].} =
## Parse the integer value that the parser is positioned on.
case c.kind
of CborEventKind.cborPositive:
result = c.intVal.BiggestInt
of CborEventKind.cborNegative:
result = -1.BiggestInt - c.intVal.BiggestInt
else:
raise newCborError("Expected integer, got " & $c.kind)
c.next()
proc nextFloat*(c: var CborParser): float64 {.raises: [CborParseError].} =
## Parse the float value that the parser is positioned on.
parseAssert(c.kind == CborEventKind.cborFloat, "Expected float, got " & $c.kind)
case c.minor
of 25:
result = floatSingle(c.intVal.uint16).float64
of 26:
result = cast[float32](c.intVal).float64
of 27:
result = cast[float64](c.intVal)
else:
discard
c.next()
func bytesLen*(c: CborParser): int {.raises: [CborParseError].} =
## Return the length of the byte string that the parser is positioned on.
parseAssert(c.kind == CborEventKind.cborBytes, "Expected bytes, got " & $c.kind)
c.intVal.int
proc nextBytes*(
c: var CborParser, buf: var openArray[byte]
) {.raises: [CborParseError].} =
## Read the bytes that the parser is positioned on and advance.
try:
parseAssert(c.kind == CborEventKind.cborBytes, "Expected bytes, got " & $c.kind)
parseAssert(
buf.len == c.intVal.int,
"Buffer length mismatch: expected " & $c.intVal.int & ", got " & $buf.len,
)
if buf.len > 0:
let n = c.s.readData(buf[0].addr, buf.len)
parseAssert(n == buf.len, "truncated read of CBOR data")
c.next()
except OSError as e:
raise newException(CborParseError, e.msg, e)
except IOError as e:
raise newException(CborParseError, e.msg, e)
proc nextBytes*(c: var CborParser): seq[byte] {.raises: [CborParseError].} =
## Read the bytes that the parser is positioned on into a seq and advance.
result = newSeq[byte](c.intVal.int)
nextBytes(c, result)
func textLen*(c: CborParser): int {.raises: [CborParseError].} =
## Return the length of the text that the parser is positioned on.
parseAssert(c.kind == CborEventKind.cborText, "Expected text, got " & $c.kind)
c.intVal.int
proc nextText*(c: var CborParser, buf: var string) {.raises: [CborParseError].} =
## Read the text that the parser is positioned on into a string and advance.
try:
parseAssert(c.kind == CborEventKind.cborText, "Expected text, got " & $c.kind)
buf.setLen c.intVal.int
if buf.len > 0:
let n = c.s.readData(buf[0].addr, buf.len)
parseAssert(n == buf.len, "truncated read of CBOR data")
c.next()
except IOError as e:
raise newException(CborParseError, e.msg, e)
except OSError as e:
raise newException(CborParseError, e.msg, e)
proc nextText*(c: var CborParser): string {.raises: [CborParseError].} =
## Read the text that the parser is positioned on into a string and advance.
nextText(c, result)
func arrayLen*(c: CborParser): int {.raises: [CborParseError].} =
## Return the length of the array that the parser is positioned on.
parseAssert(c.kind == CborEventKind.cborArray, "Expected array, got " & $c.kind)
c.intVal.int
func mapLen*(c: CborParser): int {.raises: [CborParseError].} =
## Return the length of the map that the parser is positioned on.
parseAssert(c.kind == CborEventKind.cborMap, "Expected map, got " & $c.kind)
c.intVal.int
func tag*(c: CborParser): uint64 {.raises: [CborParseError].} =
## Return the tag value the parser is positioned on.
parseAssert(c.kind == CborEventKind.cborTag, "Expected tag, got " & $c.kind)
c.intVal
proc skipNode*(c: var CborParser) {.raises: [CborParseError].} =
## Skip the item the parser is positioned on.
try:
case c.kind
of CborEventKind.cborEof:
raise newCborError("end of CBOR stream")
of CborEventKind.cborPositive, CborEventKind.cborNegative, CborEventKind.cborSimple:
c.next()
of CborEventKind.cborBytes, CborEventKind.cborText:
if c.isIndefinite:
c.next()
while c.kind != CborEventKind.cborBreak:
parseAssert(
c.kind == CborEventKind.cborBytes, "expected bytes, got " & $c.kind
)
for _ in 1 .. c.intVal.int:
discard readChar(c.s)
c.next()
else:
for _ in 1 .. c.intVal.int:
discard readChar(c.s)
c.next()
of CborEventKind.cborArray:
if c.isIndefinite:
c.next()
while c.kind != CborEventKind.cborBreak:
c.skipNode()
c.next()
else:
let len = c.intVal
c.next()
for i in 1 .. len:
c.skipNode()
of CborEventKind.cborMap:
let mapLen = c.intVal.int
if c.isIndefinite:
c.next()
while c.kind != CborEventKind.cborBreak:
c.skipNode()
c.next()
else:
c.next()
for _ in 1 .. mapLen:
c.skipNode()
of CborEventKind.cborTag:
c.next()
c.skipNode()
of CborEventKind.cborFloat:
discard c.nextFloat()
of CborEventKind.cborBreak:
discard
except OSError as os:
raise newException(CborParseError, os.msg, os)
except IOError as io:
raise newException(CborParseError, io.msg, io)
proc nextNode*(c: var CborParser): CborNode {.raises: [CborParseError].} =
## 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:
case c.kind
of CborEventKind.cborEof:
raise newCborError("end of CBOR stream")
of CborEventKind.cborPositive:
result = CborNode(kind: cborUnsigned, uint: c.intVal)
c.next()
of CborEventKind.cborNegative:
result = CborNode(kind: cborNegative, int: -1 - c.intVal.int64)
c.next()
of CborEventKind.cborBytes:
if c.isIndefinite:
result = CborNode(kind: cborBytes, bytes: newSeq[byte]())
c.next()
while c.kind != CborEventKind.cborBreak:
parseAssert(
c.kind == CborEventKind.cborBytes, "Expected bytes, got " & $c.kind
)
let
chunkLen = c.intVal.int
pos = result.bytes.len
result.bytes.setLen(pos + chunkLen)
let n = c.s.readData(result.bytes[pos].addr, chunkLen)
parseAssert(n == chunkLen, "truncated read of CBOR data")
c.next()
else:
result = CborNode(kind: cborBytes, bytes: c.nextBytes())
of CborEventKind.cborText:
if c.isIndefinite:
result = CborNode(kind: cborText, text: "")
c.next()
while c.kind != CborEventKind.cborBreak:
parseAssert(c.kind == CborEventKind.cborText, "Expected text, got " & $c.kind)
let
chunkLen = c.intVal.int
pos = result.text.len
result.text.setLen(pos + chunkLen)
let n = c.s.readData(result.text[pos].addr, chunkLen)
parseAssert(n == chunkLen, "truncated read of CBOR data")
c.next()
c.next()
else:
result = CborNode(kind: cborText, text: c.nextText())
of CborEventKind.cborArray:
result = CborNode(kind: cborArray, seq: newSeq[CborNode](c.intVal))
if c.isIndefinite:
c.next()
while c.kind != CborEventKind.cborBreak:
result.seq.add(c.nextNode())
c.next()
else:
c.next()
for i in 0 .. result.seq.high:
result.seq[i] = c.nextNode()
of CborEventKind.cborMap:
let mapLen = c.intVal.int
result = CborNode(
kind: cborMap, map: initOrderedTable[CborNode, CborNode](mapLen.nextPowerOfTwo)
)
if c.isIndefinite:
c.next()
while c.kind != CborEventKind.cborBreak:
result.map[c.nextNode()] = c.nextNode()
c.next()
else:
c.next()
for _ in 1 .. mapLen:
result.map[c.nextNode()] = c.nextNode()
of CborEventKind.cborTag:
let tag = c.intVal
c.next()
result = c.nextNode()
result.tag = some tag
of CborEventKind.cborSimple:
case c.minor
of 24:
result = CborNode(kind: cborSimple, simple: c.intVal.uint8)
else:
result = CborNode(kind: cborSimple, simple: c.minor)
c.next()
of CborEventKind.cborFloat:
result = CborNode(kind: cborFloat, float: c.nextFloat())
of CborEventKind.cborBreak:
discard
except OSError as e:
raise newException(CborParseError, e.msg, e)
except IOError as e:
raise newException(CborParseError, e.msg, e)
except CatchableError as e:
raise newException(CborParseError, e.msg, e)
except Exception as e:
raise newException(Defect, e.msg, e)
proc readCbor*(s: Stream): CborNode {.raises: [CborParseError].} =
## Parse a stream into a CBOR object.
var parser: CborParser
parser.open(s)
parser.next()
parser.nextNode()
proc parseCbor*(s: string): CborNode {.raises: [CborParseError].} =
## Parse a string into a CBOR object.
## A wrapper over stream parsing.
readCbor(newStringStream s)
proc `$`*(n: CborNode): string {.raises: [CborParseError].} =
## 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:
result.add $parseCbor(n.raw)
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, dateTimeFormat)
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 fromCbor*(_: type DateTime, n: CborNode): ?!DateTime =
## Parse a `DateTime` from the tagged string representation
## defined in RCF7049 section 2.4.1.
var v: DateTime
if n.tag.isSome:
try:
if n.tag.get == 0 and n.kind == cborText:
v = parseDateText(n)
return success(v)
elif n.tag.get == 1 and n.kind in {cborUnsigned, cborNegative, cborFloat}:
v = parseTime(n).utc
return success(v)
except ValueError as e:
return failure(e)
proc fromCbor*(_: type Time, n: CborNode): ?!Time =
## Parse a `Time` from the tagged string representation
## defined in RCF7049 section 2.4.1.
var v: Time
if n.tag.isSome:
try:
if n.tag.get == 0 and n.kind == cborText:
v = parseDateText(n).toTime
return success(v)
elif n.tag.get == 1 and n.kind in {cborUnsigned, cborNegative, cborFloat}:
v = parseTime(n)
return success(v)
except ValueError as e:
return failure(e)
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: distinct](_: type T, n: CborNode): ?!T =
success T(?T.distinctBase.fromCbor(n))
proc fromCbor*[T: SomeUnsignedInt](_: type T, n: CborNode): ?!T =
expectCborKind(T, {cborUnsigned}, n)
var v = T(n.uint)
if v.BiggestUInt == n.uint:
return success(v)
else:
return failure(newCborError("Value overflow for unsigned integer"))
proc fromCbor*[T: SomeSignedInt](_: type T, n: CborNode): ?!T =
expectCborKind(T, {cborUnsigned, cborNegative}, n)
if n.kind == cborUnsigned:
var v = T(n.uint)
if v.BiggestUInt == n.uint:
return success(v)
else:
return failure(newCborError("Value overflow for signed integer"))
elif n.kind == cborNegative:
var v = T(n.int)
if v.BiggestInt == n.int:
return success(v)
else:
return failure(newCborError("Value overflow for signed integer"))
proc fromCbor*[T: SomeFloat](_: type T, n: CborNode): ?!T =
expectCborKind(T, {cborFloat}, n)
return success(T(n.float))
proc fromCbor*(_: type seq[byte], n: CborNode): ?!seq[byte] =
expectCborKind(seq[byte], cborBytes, n)
return success(n.bytes)
proc fromCbor*(_: type string, n: CborNode): ?!string =
expectCborKind(string, cborText, n)
return success(n.text)
proc fromCbor*(_: type bool, n: CborNode): ?!bool =
if not n.isBool:
return failure(newCborError("Expected boolean, got " & $n.kind))
return success(n.getBool)
proc fromCbor*[T](_: type seq[T], n: CborNode): ?!seq[T] =
expectCborKind(seq[T], cborArray, n)
var arr = newSeq[T](n.seq.len)
for i, elem in n.seq:
arr[i] = ?T.fromCbor(elem)
success arr
proc fromCbor*[T: tuple](_: type T, n: CborNode): ?!T =
expectCborKind(T, cborArray, n)
var res = T.default
if n.seq.len != T.tupleLen:
return failure(newCborError("Expected tuple of length " & $T.tupleLen))
var i: int
for f in fields(res):
f = ?typeof(f).fromCbor(n.seq[i])
inc i
success res
proc fromCbor*[T: ref](_: type T, n: CborNode): ?!T =
when T is ref:
if n.isNull:
return success(T.default)
else:
var resRef = T.new()
let res = typeof(resRef[]).fromCbor(n)
if res.isFailure:
return failure(newCborError(res.error.msg))
resRef[] = res.value
return success(resRef)
proc fromCbor*[T: object](_: type T, n: CborNode): ?!T =
when T is CborNode:
return success T(n)
expectCborKind(T, {cborMap}, n)
var res = T.default
# Added because serde {serialize, deserialize} pragmas and options are not supported for cbor
assertNoPragma(T, deserialize, "deserialize pragma not supported")
try:
var
i: int
key = CborNode(kind: cborText)
for name, value in fieldPairs(
when type(T) is ref:
res[]
else:
res
):
assertNoPragma(value, deserialize, "deserialize pragma not supported")
key.text = name
if not n.map.hasKey key:
return failure(newCborError("Missing field: " & name))
else:
value = ?typeof(value).fromCbor(n.map[key])
inc i
if i == n.map.len:
return success(res)
else:
return failure(newCborError("Extra fields in map"))
except CatchableError as e:
return failure newCborError(e.msg)
except Exception as e:
raise newException(Defect, e.msg, e)
proc fromCbor*[T: ref object or object](_: type T, str: string): ?!T =
var n = ?(parseCbor(str)).catch
T.fromCbor(n)

38
serde/cbor/errors.nim Normal file
View File

@ -0,0 +1,38 @@
# This file is a modified version of Emery Hemingways CBOR library for Nim,
# originally available at https://github.com/ehmry/cbor-nim and released under The Unlicense.
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)

57
serde/cbor/helpers.nim Normal file
View File

@ -0,0 +1,57 @@
# This file is a modified version of Emery Hemingways CBOR library for Nim,
# originally available at https://github.com/ehmry/cbor-nim and released under The Unlicense.
import ./types
import ./errors
from macros import newDotExpr, newIdentNode, strVal
template expectCborKind*(
expectedType: type, expectedKinds: set[CborNodeKind], cbor: CborNode
) =
if cbor.kind notin expectedKinds:
return failure(newUnexpectedKindError(expectedType, expectedKinds, cbor))
template expectCborKind*(
expectedType: type, expectedKind: CborNodeKind, cbor: CborNode
) =
expectCborKind(expectedType, {expectedKind}, cbor)
template expectCborKind*(
expectedType: type, expectedKinds: set[CborEventKind], cbor: CborNode
) =
if cbor.kind notin expectedKinds:
return failure(newUnexpectedKindError(expectedType, expectedKinds, cbor))
template expectCborKind*(
expectedType: type, expectedKind: CborEventKind, cbor: CborNode
) =
expectCborKind(expectedType, {expectedKind}, cbor)
template parseAssert*(check: bool, msg = "") =
if not check:
raise newException(CborParseError, msg)
template assertNoPragma*(value, pragma, msg) =
static:
when value.hasCustomPragma(pragma):
raiseAssert(msg)
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

45
serde/cbor/jsonhook.nim Normal file
View File

@ -0,0 +1,45 @@
# This file is a modified version of Emery Hemingways CBOR library for Nim,
# originally available at https://github.com/ehmry/cbor-nim and released under The Unlicense.
import std/[base64, tables]
import ../json/stdjson
import ./types
import ./errors
import ./deserializer
proc toJson*(n: CborNode): JsonNode {.raises: [CborParseError].} =
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.toJson)
a
of cborMap:
let o = newJObject()
for k, v in n.map.pairs:
if k.kind == cborText:
o[k.text] = v.toJson
else:
o[$k] = v.toJson
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:
toJson(parseCbor(n.raw))

433
serde/cbor/serializer.nim Normal file
View File

@ -0,0 +1,433 @@
# This file is a modified version of Emery Hemingways CBOR library for Nim,
# originally available at https://github.com/ehmry/cbor-nim and released under The Unlicense.
import std/[streams, options, tables, typetraits, math, endians, times]
import pkg/questionable
import pkg/questionable/results
import ../utils/errors
import ../utils/pragmas
import ./types
import ./helpers
{.push raises: [].}
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): ?!void =
## Write the initial integer of a CBOR item.
try:
let m = m shl 5
when T is byte:
if n < 24:
str.write(m or n.uint8)
else:
str.write(m or 24'u8)
str.write(n)
else:
if n < 24:
str.write(m or n.uint8)
elif uint64(n) <= uint64(uint8.high):
str.write(m or 24'u8)
str.write(n.uint8)
elif uint64(n) <= uint64(uint16.high):
str.write(m or 25'u8)
str.write((uint8) n shr 8)
str.write((uint8) n)
elif uint64(n) <= uint64(uint32.high):
str.write(m or 26'u8)
for i in countdown(24, 8, 8):
{.unroll.}
str.write((uint8) n shr i)
str.write((uint8) n)
else:
str.write(m or 27'u8)
for i in countdown(56, 8, 8):
{.unroll.}
str.write((uint8) n shr i)
str.write((uint8) n)
success()
except IOError as e:
return failure(e.msg)
except OSError as o:
return failure(o.msg)
proc writeCborArrayLen*(str: Stream, len: Natural): ?!void =
## Write a marker to the stream that initiates an array of ``len`` items.
str.writeInitial(4, len)
proc writeCborIndefiniteArrayLen*(str: Stream): ?!void =
## Write a marker to the stream that initiates an array of indefinite length.
## Indefinite length arrays are composed of an indefinite amount of arrays
## of definite lengths.
catch str.write(initialByte(4, 31))
proc writeCborMapLen*(str: Stream, len: Natural): ?!void =
## Write a marker to the stream that initiates an map of ``len`` pairs.
str.writeInitial(5, len)
proc writeCborIndefiniteMapLen*(str: Stream): ?!void =
## Write a marker to the stream that initiates a map of indefinite length.
## Indefinite length maps are composed of an indefinite amount of maps
## of definite length.
catch str.write(initialByte(5, 31))
proc writeCborBreak*(str: Stream): ?!void =
## Write a marker to the stream that ends an indefinite array or map.
catch str.write(initialByte(7, 31))
proc writeCborTag*(str: Stream, tag: Natural): ?!void {.inline.} =
## Write a tag for the next CBOR item to a binary stream.
str.writeInitial(6, tag)
proc writeCbor*(str: Stream, buf: pointer, len: int): ?!void =
## Write a raw buffer to a CBOR `Stream`.
?str.writeInitial(BytesMajor, len)
if len > 0:
return catch str.writeData(buf, len)
success()
proc isSorted*(n: CborNode): ?!bool {.gcsafe.}
proc writeCbor(str: Stream, v: SomeUnsignedInt): ?!void =
str.writeInitial(0, v)
proc writeCbor*(str: Stream, v: SomeSignedInt): ?!void =
if v < 0:
?str.writeInitial(1, -1 - v)
else:
?str.writeInitial(0, v)
success()
proc writeCbor*(str: Stream, v: seq[byte]): ?!void =
?str.writeInitial(BytesMajor, v.len)
if v.len > 0:
return catch str.writeData(unsafeAddr v[0], v.len)
success()
proc writeCbor*(str: Stream, v: string): ?!void =
?str.writeInitial(TextMajor, v.len)
return catch str.write(v)
proc writeCbor*[T: char or uint8 or int8](str: Stream, v: openArray[T]): ?!void =
?str.writeInitial(BytesMajor, v.len)
if v.len > 0:
return catch str.writeData(unsafeAddr v[0], v.len)
success()
proc writeCbor*[T: array or seq](str: Stream, v: T): ?!void =
?str.writeInitial(4, v.len)
for e in v.items:
?str.writeCbor(e)
success()
proc writeCbor*(str: Stream, v: tuple): ?!void =
?str.writeInitial(4, v.tupleLen)
for e in v.fields:
?str.writeCbor(e)
success()
proc writeCbor*[T: ptr | ref](str: Stream, v: T): ?!void =
if system.`==`(v, nil):
# Major type 7
return catch str.write(Null)
else:
?str.writeCbor(v[])
success()
proc writeCbor*(str: Stream, v: bool): ?!void =
return catch str.write(initialByte(7, (if v: 21 else: 20)))
proc writeCbor*[T: SomeFloat](str: Stream, v: T): ?!void =
try:
case v.classify
of fcNormal, fcSubnormal:
let single = v.float32
if single.float64 == v.float64:
if single.isHalfPrecise:
let half = floatHalf(single)
str.write(initialByte(7, 25))
when system.cpuEndian == bigEndian:
str.write(half)
else:
var be: uint16
swapEndian16 be.addr, half.unsafeAddr
str.write(be)
else:
str.write initialByte(7, 26)
when system.cpuEndian == bigEndian:
str.write(single)
else:
var be: uint32
swapEndian32 be.addr, single.unsafeAddr
str.write(be)
else:
str.write initialByte(7, 27)
when system.cpuEndian == bigEndian:
str.write(v)
else:
var be: uint64
swapEndian64 be.addr, v.unsafeAddr
str.write(be)
return success()
of fcZero:
str.write initialByte(7, 25)
str.write((char) 0x00)
of fcNegZero:
str.write initialByte(7, 25)
str.write((char) 0x80)
of fcInf:
str.write initialByte(7, 25)
str.write((char) 0x7c)
of fcNan:
str.write initialByte(7, 25)
str.write((char) 0x7e)
of fcNegInf:
str.write initialByte(7, 25)
str.write((char) 0xfc)
str.write((char) 0x00)
success()
except IOError as io:
return failure(io.msg)
except OSError as os:
return failure(os.msg)
proc writeCbor*(str: Stream, v: CborNode): ?!void =
try:
if v.tag.isSome:
?str.writeCborTag(v.tag.get)
case v.kind
of cborUnsigned:
?str.writeCbor(v.uint)
of cborNegative:
?str.writeCbor(v.int)
of cborBytes:
?str.writeInitial(cborBytes.uint8, v.bytes.len)
for b in v.bytes.items:
str.write(b)
of cborText:
?str.writeInitial(cborText.uint8, v.text.len)
str.write(v.text)
of cborArray:
?str.writeInitial(4, v.seq.len)
for e in v.seq:
?str.writeCbor(e)
of cborMap:
without isSortedRes =? v.isSorted, error:
return failure(error)
if not isSortedRes:
return failure(newSerdeError("refusing to write unsorted map to stream"))
?str.writeInitial(5, v.map.len)
for k, f in v.map.pairs:
?str.writeCbor(k)
?str.writeCbor(f)
of cborTag:
discard
of cborSimple:
if v.simple > 31'u or v.simple == 24:
str.write(initialByte(cborSimple.uint8, 24))
str.write(v.simple)
else:
str.write(initialByte(cborSimple.uint8, v.simple))
of cborFloat:
?str.writeCbor(v.float)
of cborRaw:
str.write(v.raw)
success()
except CatchableError as e:
return failure(e.msg)
proc writeCbor*[T: object](str: Stream, v: T): ?!void =
var n: uint
# Added because serde {serialize, deserialize} pragma and options are not supported for cbor
assertNoPragma(T, serialize, "serialize pragma not supported")
for _, _ in v.fieldPairs:
inc n
?str.writeInitial(5, n)
for k, f in v.fieldPairs:
assertNoPragma(f, serialize, "serialize pragma not supported")
?str.writeCbor(k)
?str.writeCbor(f)
success()
proc writeCborArray*(str: Stream, args: varargs[CborNode, toCborNode]): ?!void =
## Encode to a CBOR array in binary form. This magic doesn't
## always work, some arguments may need to be explicitly
## converted with ``toCborNode`` before passing.
?str.writeCborArrayLen(args.len)
for x in args:
?str.writeCbor(x)
success()
proc toCbor*[T](v: T): ?!string =
## Encode an arbitrary value to CBOR binary representation.
## A wrapper over ``writeCbor``.
let s = newStringStream()
let res = s.writeCbor(v)
if res.isFailure:
return failure(res.error)
success(s.data)
proc toRaw*(n: CborNode): ?!CborNode =
## Reduce a CborNode to a string of bytes.
if n.kind == cborRaw:
return success(n)
else:
without res =? toCbor(n), error:
return failure(error)
return success(CborNode(kind: cborRaw, raw: res))
proc isSorted(n: CborNode): ?!bool =
## Check if the item is sorted correctly.
var lastRaw = ""
for key in n.map.keys:
without res =? key.toRaw, error:
return failure(error.msg)
let thisRaw = res.raw
if lastRaw != "":
if cmp(lastRaw, thisRaw) > 0:
return success(false)
lastRaw = thisRaw
success(true)
proc sort*(n: var CborNode): ?!void =
## Sort a CBOR map object.
try:
var tmp = initOrderedTable[CborNode, CborNode](n.map.len.nextPowerOfTwo)
for key, val in n.map.mpairs:
without res =? key.toRaw, error:
return failure(error)
if tmp.hasKey(res):
tmp[res] = 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
success()
except CatchableError as e:
return failure(e.msg)
except Exception as e:
raise newException(Defect, e.msg, e)
proc writeCbor*(str: Stream, dt: DateTime): ?!void =
## Write a `DateTime` using the tagged string representation
## defined in RCF7049 section 2.4.1.
?writeCborTag(str, 0)
?writeCbor(str, format(dt, dateTimeFormat))
success()
proc writeCbor*(str: Stream, t: Time): ?!void =
## Write a `Time` using the tagged numerical representation
## defined in RCF7049 section 2.4.1.
?writeCborTag(str, 1)
?writeCbor(str, t.toUnix)
success()
func toCborNode*(x: CborNode): ?!CborNode =
success(x)
func toCborNode*(x: SomeInteger): ?!CborNode =
if x > 0:
success(CborNode(kind: cborUnsigned, uint: x.uint64))
else:
success(CborNode(kind: cborNegative, int: x.int64))
func toCborNode*(x: openArray[byte]): ?!CborNode =
success(CborNode(kind: cborBytes, bytes: @x))
func toCborNode*(x: string): ?!CborNode =
success(CborNode(kind: cborText, text: x))
func toCborNode*(x: openArray[CborNode]): ?!CborNode =
success(CborNode(kind: cborArray, seq: @x))
func toCborNode*(pairs: openArray[(CborNode, CborNode)]): ?!CborNode =
try:
return success(CborNode(kind: cborMap, map: pairs.toOrderedTable))
except CatchableError as e:
return failure(e.msg)
except Exception as e:
raise newException(Defect, e.msg, e)
func toCborNode*(tag: uint64, val: CborNode): ?!CborNode =
without res =? toCborNode(val), error:
return failure(error.msg)
var cnode = res
cnode.tag = some(tag)
return success(cnode)
func toCborNode*(x: bool): ?!CborNode =
case x
of false:
success(CborNode(kind: cborSimple, simple: 20))
of true:
success(CborNode(kind: cborSimple, simple: 21))
func toCborNode*(x: SomeFloat): ?!CborNode =
success(CborNode(kind: cborFloat, float: x.float64))
func toCborNode*(x: pointer): ?!CborNode =
## A hack to produce a CBOR null item.
if not x.isNil:
return failure("pointer is not nil")
success(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, toCborNode]): CborNode =
## Initialize a CBOR arrary.
CborNode(kind: cborArray, seq: @items)

170
serde/cbor/types.nim Normal file
View File

@ -0,0 +1,170 @@
# This file is a modified version of Emery Hemingways CBOR library for Nim,
# originally available at https://github.com/ehmry/cbor-nim and released under The Unlicense.
import std/[streams, tables, options, hashes, times]
# This format is defined in RCF8949 section 3.4.1.
const dateTimeFormat* = 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 = ## Retrieve a value from a CBOR map.
n.map[k]
proc `[]=`*(n: var CborNode, k, v: sink CborNode) = ## Assign a pair in a CBOR map.
n.map[k] = v
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,9 +1,9 @@
import ./json/parser
import ./json/deserializer
import ./json/stdjson
import ./json/pragmas
import ./utils/pragmas
import ./json/serializer
import ./json/types
import ./utils/types
export parser
export deserializer

533
serde/json/README.md Normal file
View File

@ -0,0 +1,533 @@
# nim-serde JSON
The JSON module in nim-serde provides serialization and deserialization for Nim values, offering an improved alternative to the standard `std/json` library. Unlike the standard library, nim-serde JSON implements a flexible system of serialization/deserialization modes that give developers precise control over how Nim objects are converted to and from JSON.
## Table of Contents
- [nim-serde JSON](#nim-serde-json)
- [Table of Contents](#table-of-contents)
- [Serde Modes](#serde-modes)
- [Modes Overview](#modes-overview)
- [Default Modes](#default-modes)
- [Field Options](#field-options)
- [Serialization API](#serialization-api)
- [Basic Serialization with `%` operator](#basic-serialization-with--operator)
- [Object Serialization](#object-serialization)
- [Inlining JSON Directly in Code with `%*`](#inlining-json-directly-in-code-with-)
- [Converting to JSON String with `toJson`](#converting-to-json-string-with-tojson)
- [Serialization Modes](#serialization-modes)
- [Field Customization for Serialization](#field-customization-for-serialization)
- [Custom Type Serialization](#custom-type-serialization)
- [Deserialization API](#deserialization-api)
- [Basic Deserialization with `fromJson`](#basic-deserialization-with-fromjson)
- [Error Handling](#error-handling)
- [Parsing JSON with `JsonNode.parse`](#parsing-json-with-jsonnodeparse)
- [Deserialization Modes](#deserialization-modes)
- [Field Customization for Deserialization](#field-customization-for-deserialization)
- [Using as a Drop-in Replacement for std/json](#using-as-a-drop-in-replacement-for-stdjson)
- [Implementation Details](#implementation-details)
## Serde Modes
This implementation supports three different modes to control de/serialization:
```nim
OptIn
OptOut
Strict
```
Modes can be set in the `{.serialize.}` and/or `{.deserialize.}` pragmas on type
definitions. Each mode has a different meaning depending on if the type is being
serialized or deserialized. Modes can be set by setting `mode` in the `serialize` or
`deserialize` pragma annotation, eg:
```nim
type MyType {.serialize(mode=Strict).} = object
field1: bool
field2: bool
```
### Modes Overview
| Mode | Serialize | Deserialize |
|:-----|:----------|:------------|
| `OptOut` | All object fields will be serialized, except fields marked with `{.serialize(ignore=true).}`. | All JSON keys will be deserialized, except fields marked with `{.deserialize(ignore=true).}`. No error if extra JSON fields exist. |
| `OptIn` | Only fields marked with `{.serialize.}` will be serialized. Fields marked with `{.serialize(ignore=true).}` will not be serialized. | Only fields marked with `{.deserialize.}` will be deserialized. Fields marked with `{.deserialize(ignore=true).}` will not be deserialized. A `SerdeError` is raised if the field is missing in JSON. |
| `Strict` | All object fields will be serialized, regardless if the field is marked with `{.serialize(ignore=true).}`. | Object fields and JSON fields must match exactly, otherwise a `SerdeError` is raised. |
### Default Modes
Types can be serialized and deserialized even without explicit annotations, using default modes. Without any pragmas, types are serialized in OptIn mode and deserialized in OptOut mode. When types have pragmas but no specific mode is set, OptOut mode is used for both serialization and deserialization.
| Context | Serialize | Deserialize |
|:--------|:----------|:------------|
| Default (no pragma) | `OptIn` | `OptOut` |
| Default (pragma, but no mode) | `OptOut` | `OptOut` |
```nim
# Type is not annotated
# If you don't annotate the type, serde assumes OptIn by default for serialization, and OptOut for
# deserialization. This means your types will be serialized to an empty string, which is probably not what you want:
type MyObj1 = object
field1: bool
field2: bool
# If you annotate your type but do not specify the mode, serde will default to OptOut for
# both serialize and de-serialize, meaning all fields get serialized/de-serialized by default:
# A default mode of OptOut is assumed for both serialize and deserialize.
type MyObj2 {.serialize, deserialize.} = object
field1: bool
field2: bool
```
### Field Options
Individual fields can be customized using the `{.serialize.}` and `{.deserialize.}` pragmas with additional options that control how each field is processed during serialization and deserialization
| | serialize | deserialize |
|:---------|:-----------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------|
| `key` | aliases the field name in json | deserializes the field if json contains `key` |
| `ignore` | <li>**OptOut:** field not serialized</li><li>**OptIn:** field not serialized</li><li>**Strict:** field serialized</li> | <li>**OptOut:** field not deserialized</li><li>**OptIn:** field not deserialized</li><li>**Strict:** field deserialized</li> |
Example with field options:
```nim
import pkg/serde/json
type
Person {.serialize(mode=OptOut), deserialize(mode=OptIn).} = object
id {.serialize(ignore=true), deserialize(key="personid").}: int
name: string
birthYear: int
address: string
phone: string
let person = Person(
name: "Lloyd Christmas",
birthYear: 1970,
address: "123 Sesame Street, Providence, Rhode Island 12345",
phone: "555-905-justgivemethedamnnumber!⛽️🔥")
let createRequest = """{
"name": "Lloyd Christmas",
"birthYear": 1970,
"address": "123 Sesame Street, Providence, Rhode Island 12345",
"phone": "555-905-justgivemethedamnnumber!⛽️🔥"
}"""
assert person.toJson(pretty=true) == createRequest
let createResponse = """{
"personid": 1,
"name": "Lloyd Christmas",
"birthYear": 1970,
"address": "123 Sesame Street, Providence, Rhode Island 12345",
"phone": "555-905-justgivemethedamnnumber!⛽️🔥"
}"""
assert !Person.fromJson(createResponse) == Person(id: 1)
```
More examples can be found in [Serialization Modes](#serialization-modes) and [Deserialization Modes](#deserialization-modes).
## Serialization API
The nim-serde JSON serialization API provides several ways to convert Nim values to JSON.
### Basic Serialization with `%` operator
The `%` operator converts Nim values to `JsonNode` objects, which can then be converted to JSON strings:
```nim
import pkg/serde/json
# Basic types
assert %42 == newJInt(42)
assert %"hello" == newJString("hello")
assert %true == newJBool(true)
# Arrays and sequences
let arr = newJArray()
arr.add(newJInt(1))
arr.add(newJInt(2))
arr.add(newJInt(3))
assert $(%[1, 2, 3]) == $arr
```
### Object Serialization
Objects can be serialized using the `%` operator, which automatically handles field serialization based on the object's configuration:
```nim
import pkg/serde/json
type Person = object
name {.serialize.}: string
age {.serialize.}: int
address: string # Not serialized by default in OptIn mode
let person = Person(name: "John", age: 30, address: "123 Main St")
let jsonNode = %person
assert jsonNode.kind == JObject
assert jsonNode.len == 2
assert jsonNode["name"].getStr == "John"
assert jsonNode["age"].getInt == 30
assert "address" notin jsonNode
```
### Inlining JSON Directly in Code with `%*`
The `%*` macro provides a more convenient way to create JSON objects:
```nim
import pkg/serde/json
let
name = "John"
age = 30
jsonObj = %*{
"name": name,
"age": age,
"hobbies": ["reading", "coding"],
"address": {
"street": "123 Main St",
"city": "Anytown"
}
}
assert jsonObj.kind == JObject
assert jsonObj["name"].getStr == name
assert jsonObj["age"].getInt == age
assert jsonObj["hobbies"].kind == JArray
assert jsonObj["hobbies"][0].getStr == "reading"
assert jsonObj["address"]["street"].getStr == "123 Main St"
```
### Converting to JSON String with `toJson`
The `toJson` function converts any serializable value directly to a JSON string:
```nim
import pkg/serde/json
type Person = object
name {.serialize.}: string
age {.serialize.}: int
let person = Person(name: "John", age: 30)
assert person.toJson == """{"name":"John","age":30}"""
```
### Serialization Modes
nim-serde offers three modes to control which fields are serialized:
```nim
import pkg/serde/json
# OptIn mode (default): Only fields with {.serialize.} are included
type Person1 = object
name {.serialize.}: string
age {.serialize.}: int
address: string # Not serialized
assert Person1(name: "John", age: 30, address: "123 Main St").toJson == """{"name":"John","age":30}"""
# OptOut mode: All fields are included except those marked to ignore
type Person2 {.serialize.} = object
name: string
age: int
ssn {.serialize(ignore=true).}: string # Not serialized
assert Person2(name: "John", age: 30, ssn: "123-45-6789").toJson == """{"name":"John","age":30}"""
# Strict mode: All fields are included, and an error is raised if any fields are missing
type Person3 {.serialize(mode=Strict).} = object
name: string
age: int
assert Person3(name: "John", age: 30).toJson == """{"name":"John","age":30}"""
```
### Field Customization for Serialization
Fields can be customized with various options:
```nim
import pkg/serde/json
# Field customization for serialization
type Person {.serialize(mode = OptOut).} = object
firstName {.serialize(key = "first_name").}: string
lastName {.serialize(key = "last_name").}: string
age: int # Will be included because we're using OptOut mode
ssn {.serialize(ignore = true).}: string # Sensitive data not serialized
let person = Person(
firstName: "John",
lastName: "Doe",
age: 30,
ssn: "123-45-6789"
)
let jsonNode = %person
assert jsonNode.kind == JObject
assert jsonNode["first_name"].getStr == "John"
assert jsonNode["last_name"].getStr == "Doe"
assert jsonNode["age"].getInt == 30
assert "ssn" notin jsonNode
# Convert to JSON string
let jsonStr = toJson(person)
assert jsonStr == """{"first_name":"John","last_name":"Doe","age":30}"""
```
## Custom Type Serialization
You can extend nim-serde to support custom types by defining your own `%` operator overloads and `fromJson` procs:
```nim
import pkg/serde/json
import pkg/serde/utils/errors
import pkg/questionable/results
import std/strutils
# Define a custom type
type
UserId = distinct int
# Custom serialization for UserId
proc `%`*(id: UserId): JsonNode =
%("user-" & $int(id))
# Custom deserialization for UserId
proc fromJson*(_: type UserId, json: JsonNode): ?!UserId =
if json.kind != JString:
return failure(newSerdeError("Expected string for UserId, got " & $json.kind))
let str = json.getStr()
if str.startsWith("user-"):
let idStr = str[5..^1]
try:
let id = parseInt(idStr)
success(UserId(id))
except ValueError:
failure(newSerdeError("Invalid UserId format: " & str))
else:
failure(newSerdeError("UserId must start with 'user-' prefix"))
# Test serialization
let userId = UserId(42)
let jsonNode = %userId
assert jsonNode.kind == JString
assert jsonNode.getStr() == "user-42"
# Test deserialization
let jsonStr = "\"user-42\""
let parsedJson = !JsonNode.parse(jsonStr)
let result = UserId.fromJson(parsedJson)
assert result.isSuccess
assert int(!result) == 42
# Test in object context
type User {.serialize(mode = OptOut).} = object
id: UserId
name: string
let user = User(id: UserId(123), name: "John")
let userJson = %user
assert userJson.kind == JObject
assert userJson["id"].getStr() == "user-123"
assert userJson["name"].getStr() == "John"
# Test deserialization of object with custom type
let userJsonStr = """{"id":"user-123","name":"John"}"""
let userResult = User.fromJson(userJsonStr)
assert userResult.isSuccess
assert int((!userResult).id) == 123
assert (!userResult).name == "John"
```
## Deserialization API
nim-serde provides a type-safe way to convert JSON data back into Nim types.
### Basic Deserialization with `fromJson`
The `fromJson` function converts JSON strings or `JsonNode` objects to Nim types:
```nim
import pkg/serde/json
import pkg/questionable/results
type Person = object
name: string
age: int
let jsonStr = """{"name":"John","age":30}"""
let result = Person.fromJson(jsonStr)
# Using the ! operator from questionable to extract the value
assert !result == Person(name: "John", age: 30)
```
### Error Handling
Deserialization returns a `Result` type from the `questionable` library, allowing for safe error handling:
```nim
import pkg/serde/json
import pkg/questionable/results
type Person = object
name: string
age: int
let invalidJson = """{"name":"John","age":"thirty"}"""
let errorResult = Person.fromJson(invalidJson)
assert errorResult.isFailure
assert errorResult.error of UnexpectedKindError
```
### Parsing JSON with `JsonNode.parse`
To parse JSON string into a `JsonNode` tree instead of a deserializing to a concrete type, use `JsonNode.parse`:
```nim
import pkg/serde/json
import pkg/questionable/results
let jsonStr = """{"name":"John","age":30,"hobbies":["reading","coding"]}"""
let parseResult = JsonNode.parse(jsonStr)
assert parseResult.isSuccess
let jsonNode = !parseResult
assert jsonNode["name"].getStr == "John"
assert jsonNode["age"].getInt == 30
assert jsonNode["hobbies"].kind == JArray
assert jsonNode["hobbies"][0].getStr == "reading"
assert jsonNode["hobbies"][1].getStr == "coding"
```
### Deserialization Modes
nim-serde offers three modes to control how JSON is deserialized:
```nim
import pkg/serde/json
import pkg/questionable/results
# OptOut mode (default for deserialization)
type PersonOptOut = object
name: string
age: int
let jsonOptOut = """{"name":"John","age":30,"address":"123 Main St"}"""
let resultOptOut = PersonOptOut.fromJson(jsonOptOut)
assert resultOptOut.isSuccess
assert !resultOptOut == PersonOptOut(name: "John", age: 30)
# OptIn mode
type PersonOptIn {.deserialize(mode = OptIn).} = object
name {.deserialize.}: string
age {.deserialize.}: int
address: string # Not deserialized by default in OptIn mode
let jsonOptIn = """{"name":"John","age":30,"address":"123 Main St"}"""
let resultOptIn = PersonOptIn.fromJson(jsonOptIn)
assert resultOptIn.isSuccess
assert (!resultOptIn).name == "John"
assert (!resultOptIn).age == 30
assert (!resultOptIn).address == "" # address is not deserialized
# Strict mode
type PersonStrict {.deserialize(mode = Strict).} = object
name: string
age: int
let jsonStrict = """{"name":"John","age":30}"""
let resultStrict = PersonStrict.fromJson(jsonStrict)
assert resultStrict.isSuccess
assert !resultStrict == PersonStrict(name: "John", age: 30)
# Strict mode with extra field (should fail)
let jsonStrictExtra = """{"name":"John","age":30,"address":"123 Main St"}"""
let resultStrictExtra = PersonStrict.fromJson(jsonStrictExtra)
assert resultStrictExtra.isFailure
```
### Field Customization for Deserialization
Fields can be customized with various options for deserialization:
```nim
import pkg/serde/json
import pkg/questionable/results
type User = object
firstName {.deserialize(key = "first_name").}: string
lastName {.deserialize(key = "last_name").}: string
age: int
internalId {.deserialize(ignore = true).}: int
let userJsonStr = """{"first_name":"Jane","last_name":"Smith","age":25,"role":"admin"}"""
let result = User.fromJson(userJsonStr)
assert result.isSuccess
assert (!result).firstName == "Jane"
assert (!result).lastName == "Smith"
assert (!result).age == 25
assert (!result).internalId == 0 # Default value, not deserialized
```
## Using as a Drop-in Replacement for std/json
nim-serde can be used as a drop-in replacement for the standard library's `json` module with improved exception handling.
```nim
# Instead of:
# import std/json
import pkg/serde/json
import pkg/questionable/results
# Using nim-serde's JSON API which is compatible with std/json
let jsonNode = %* {
"name": "John",
"age": 30,
"isActive": true,
"hobbies": ["reading", "swimming"]
}
# Accessing JSON fields using the same API as std/json
assert jsonNode.kind == JObject
assert jsonNode["name"].getStr == "John"
assert jsonNode["age"].getInt == 30
assert jsonNode["isActive"].getBool == true
assert jsonNode["hobbies"].kind == JArray
assert jsonNode["hobbies"][0].getStr == "reading"
assert jsonNode["hobbies"][1].getStr == "swimming"
# Converting JSON to string
let jsonStr = $jsonNode
# Parsing JSON from string with better error handling
let parsedResult = JsonNode.parse(jsonStr)
assert parsedResult.isSuccess
let parsedNode = !parsedResult
assert parsedNode.kind == JObject
assert parsedNode["name"].getStr == "John"
# Pretty printing
let prettyJson = pretty(jsonNode)
```
## Implementation Details
The JSON serialization in nim-serde is based on the `%` operator pattern:
1. The `%` operator converts values to `JsonNode` objects
2. Various overloads handle different types (primitives, objects, collections)
3. The `toJson` function converts the `JsonNode` to a string
This approach makes it easy to extend with custom types by defining your own `%` operator overloads.
For deserialization, the library uses compile-time reflection to map JSON fields to object fields, respecting the configuration provided by pragmas.

View File

@ -14,8 +14,9 @@ import pkg/questionable/results
import ./parser
import ./errors
import ./stdjson
import ./pragmas
import ./types
import ../utils/pragmas
import ../utils/types
import ../utils/errors
import ./helpers
export parser
@ -31,29 +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)
without val =? parseEnum[T](json.str).catch, error:
@ -296,7 +274,9 @@ proc fromJson*[T: SomeInteger or SomeFloat or openArray[byte] or bool or enum](
success newSeq[T]()
else:
if T is enum:
let err = newSerdeError("Cannot deserialize a seq[enum]: not yet implemented, PRs welcome")
let err = newSerdeError(
"Cannot deserialize a seq[enum]: not yet implemented, PRs welcome"
)
return failure err
let jsn = ?JsonNode.parse(json)
@ -309,7 +289,9 @@ proc fromJson*[T: SomeInteger or SomeFloat or openArray[byte] or bool or enum](
success seq[T].none
else:
if T is enum:
let err = newSerdeError("Cannot deserialize a seq[enum]: not yet implemented, PRs welcome")
let err = newSerdeError(
"Cannot deserialize a seq[enum]: not yet implemented, PRs welcome"
)
return failure err
let jsn = ?JsonNode.parse(json)
Option[seq[T]].fromJson(jsn)
@ -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

View File

@ -1,17 +1,6 @@
import std/sets
import ./stdjson
import ./types
{.push raises: [].}
proc mapErrTo*[E1: ref CatchableError, E2: SerdeError](
e1: E1, _: type E2, msg: string = e1.msg
): ref E2 =
return newException(E2, msg, e1)
proc newSerdeError*(msg: string): ref SerdeError =
newException(SerdeError, msg)
import ../utils/types
import std/sets
proc newUnexpectedKindError*(
expectedType: type, expectedKinds: string, json: JsonNode

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

@ -2,8 +2,8 @@ import std/json as stdjson
import pkg/questionable/results
import ./errors
import ./types
import ../utils/errors
import ../utils/types
{.push raises: [].}

View File

@ -10,16 +10,14 @@ import pkg/stew/byteutils
import pkg/stint
import ./stdjson
import ./pragmas
import ./types
import ../utils/pragmas
import ../utils/types
export chronicles except toJson
export stdjson
export pragmas
export types
{.push raises: [].}
logScope:
topics = "nimserde json serializer"

11
serde/utils/errors.nim Normal file
View File

@ -0,0 +1,11 @@
import ./types
{.push raises: [].}
proc mapErrTo*[E1: ref CatchableError, E2: SerdeError](
e1: E1, _: type E2, msg: string = e1.msg
): ref E2 =
return newException(E2, msg, e1)
proc newSerdeError*(msg: string): ref SerdeError =
newException(SerdeError, msg)

View File

@ -1,6 +1,7 @@
type
SerdeError* = object of CatchableError
JsonParseError* = object of SerdeError
CborParseError* = object of SerdeError
UnexpectedKindError* = object of SerdeError
SerdeMode* = enum
OptOut

74
tests/benchmark.nim Normal file
View File

@ -0,0 +1,74 @@
import pkg/serde
import std/[times]
import pkg/questionable
import pkg/questionable/results
import pkg/stew/byteutils
import pkg/stint
import serde/json/serializer
import serde/cbor/serializer
import serde/cbor/deserializer
type Inner {.serialize.} = object
size: uint64
type CustomPoint {.serialize.} = object
u: uint64 # Unsigned integer
n: int # Signed integer
b: seq[byte] # Byte sequence
t: string # Text string
arr: seq[int] # Integer sequence
tag: float # Floating point
flag: bool # Boolean
inner: Inner # Nested object
innerArr: seq[Inner] # Sequence of objects
proc generateCustomPoint(): CustomPoint =
CustomPoint(
u: 1234567890,
n: -1234567890,
b: "hello world".toBytes,
t: "hello world",
arr: @[1, 2, 3, 4, 5],
tag: 3.14,
flag: true,
inner: Inner(size: 1234567890),
innerArr: @[Inner(size: 1234567890), Inner(size: 9543210)],
)
proc benchmark(): void =
let point = generateCustomPoint()
var jsonStr = ""
var cborStr = ""
let jsonStartTime = cpuTime()
for i in 1 .. 100000:
jsonStr = toJson(point)
let jsonEndTime = cpuTime()
let jsonDuration = jsonEndTime - jsonStartTime
let cborStartTime = cpuTime()
for i in 1 .. 100000:
cborStr = toCbor(point).tryValue
let cborEndTime = cpuTime()
let cborDuration = cborEndTime - cborStartTime
let jsonDeserializeStartTime = cpuTime()
for i in 1 .. 100000:
assert CustomPoint.fromJson(jsonStr).isSuccess
let jsonDeserializeEndTime = cpuTime()
let jsonDeserializeDuration = jsonDeserializeEndTime - jsonDeserializeStartTime
let cborDeserializeStartTime = cpuTime()
for i in 1 .. 100000:
assert CustomPoint.fromCbor(cborStr).isSuccess
let cborDeserializeEndTime = cpuTime()
let cborDeserializeDuration = cborDeserializeEndTime - cborDeserializeStartTime
echo "JSON Serialization Time: ", jsonDuration
echo "CBOR Serialization Time: ", cborDuration
echo "JSON Deserialization Time: ", jsonDeserializeDuration
echo "CBOR Deserialization Time: ", cborDeserializeDuration
when isMainModule:
benchmark()

236
tests/cbor/testObjects.nim Normal file
View File

@ -0,0 +1,236 @@
import std/unittest
import std/options
import std/streams
import std/times
import std/macros
import pkg/serde
import pkg/questionable
import pkg/questionable/results
#[
Test types definitions
These types are used to test various aspects of CBOR serialization/deserialization:
- Basic types (integers, strings, etc.)
- Custom types with custom serialization logic
- Nested objects
- Reference types
- Collections (sequences, tuples)
]#
type
# A simple 2D point with x and y coordinates
CustomPoint = object
x: int
y: int
# Enum type to test enum serialization
CustomColor = enum
Red
Green
Blue
# Object combining different custom types
CustomObject = object
name: string
point: CustomPoint
color: CustomColor
# Simple object with a string and sequence
Inner = object
s: string
nums: seq[int]
# Reference type for testing ref object serialization
NewType = ref object
size: uint64
# Complex object with various field types to test comprehensive serialization
CompositeNested = object
u: uint64 # Unsigned integer
n: int # Signed integer
b: seq[byte] # Byte sequence
t: string # Text string
arr: seq[int] # Integer sequence
tag: float # Floating point
flag: bool # Boolean
inner: Inner # Nested object
innerArr: seq[Inner] # Sequence of objects
coordinates: tuple[x: int, y: int, label: string] # Tuple
refInner: ref Inner # Reference to object
refNewInner: NewType # Custom reference type
refNil: ref Inner # Nil reference
customPoint: CustomPoint # Custom type
time: Time # Time
date: DateTime # DateTime
# Custom deserialization for CustomColor enum
# Converts a CBOR negative integer to a CustomColor enum value
proc fromCbor*(_: type CustomColor, n: CborNode): ?!CustomColor =
var v: CustomColor
if n.kind == cborNegative:
v = CustomColor(n.int)
success(v)
else:
failure(newSerdeError("Expected signed integer, got " & $n.kind))
# Custom deserialization for CustomPoint
# Expects a CBOR array with exactly 2 elements representing x and y coordinates
proc fromCbor*(_: type CustomPoint, n: CborNode): ?!CustomPoint =
if n.kind == cborArray and n.seq.len == 2:
let x = ?int.fromCbor(n.seq[0])
let y = ?int.fromCbor(n.seq[1])
return success(CustomPoint(x: x, y: y))
else:
return failure(newSerdeError("Expected array of length 2 for CustomPoint"))
# Custom serialization for CustomPoint
# Serializes a CustomPoint as a CBOR array with 2 elements: [x, y]
proc writeCbor*(str: Stream, val: CustomPoint): ?!void =
# Write array header with length 2
?str.writeCborArrayLen(2)
# Write x and y coordinates
?str.writeCbor(val.x)
str.writeCbor(val.y)
# 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),
]
# Creates a CBOR map node representing a CustomObject
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":
test "deserializes object with custom types":
# Create a test point
let point = CustomPoint(x: 15, y: 25)
# Create CBOR representation of a CustomObject
let node = createObjectCbor("Test Object", point, Green)
# Deserialize CBOR to CustomObject
let result = CustomObject.fromCbor(node)
# Verify deserialization was successful
check result.isSuccess
var deserializedObj = result.tryValue
# Verify all fields were correctly deserialized
check deserializedObj.name == "Test Object"
check deserializedObj.point.x == 15
check deserializedObj.point.y == 25
check deserializedObj.color == Green
test "serialize and deserialize object with all supported wire types":
# Setup test data with various types
# 1. Create reference objects
var refInner = new Inner
refInner.s = "refInner"
refInner.nums = @[30, 40]
var refNewObj = new NewType
refNewObj.size = 42
# 2. Create a complex object with all supported types
var original = CompositeNested(
u: 42, # unsigned integer
n: -99, # signed integer
b: @[byte 1, byte 2], # byte array
t: "hi", # string
arr: @[1, 2, 3], # integer array
tag: 1.5, # float
flag: true, # boolean
inner: Inner(s: "inner!", nums: @[10, 20]), # nested object
innerArr:
@[ # array of objects
Inner(s: "first", nums: @[1, 2]), Inner(s: "second", nums: @[3, 4, 5])
],
coordinates: (x: 10, y: 20, label: "test"), # tuple
refInner: refInner, # reference to object
refNewInner: refNewObj, # custom reference type
refNil: nil, # nil reference
customPoint: CustomPoint(x: 15, y: 25), # custom type
time: getTime(), # time
date: now().utc, # date
)
# Test serialization using encode helper
without encodedStr =? toCbor(original), error:
fail()
# Test serialization using stream API
let stream = newStringStream()
check not stream.writeCbor(original).isFailure
# Get the serialized CBOR data
let cborData = stream.data
# Verify both serialization methods produce the same result
check cborData == encodedStr
# Parse CBOR data back to CborNode
let node = parseCbor(cborData)
# Deserialize CborNode to CompositeNested object
let res = CompositeNested.fromCbor(node)
check res.isSuccess
let roundtrip = res.tryValue
# Verify all fields were correctly round-tripped
# 1. Check primitive 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 # Float comparison with epsilon
check roundtrip.flag == original.flag
# 2. Check nested object fields
check roundtrip.inner.s == original.inner.s
check roundtrip.inner.nums == original.inner.nums
# 3. Check sequence 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
# 4. Check tuple fields
check roundtrip.coordinates.x == original.coordinates.x
check roundtrip.coordinates.y == original.coordinates.y
check roundtrip.coordinates.label == original.coordinates.label
# 5. Check reference fields
check not roundtrip.refInner.isNil
check roundtrip.refInner.s == original.refInner.s
check roundtrip.refInner.nums == original.refInner.nums
# 6. Check nil reference
check roundtrip.refNil.isNil
# 7. Check custom type
check roundtrip.customPoint.x == original.customPoint.x
check roundtrip.customPoint.y == original.customPoint.y

View File

@ -0,0 +1,45 @@
import std/unittest
import std/streams
import ../../serde/cbor
import ../../serde/utils/pragmas
{.push raises: [].}
suite "CBOR pragma checks":
test "fails to compile when object marked with 'serialize' pragma":
type SerializeTest {.serialize.} = object
value: int
check not compiles(toCbor(SerializeTest(value: 42)))
test "fails to compile when object marked with 'deserialize' pragma":
type DeserializeTest {.deserialize.} = object
value: int
let node = CborNode(kind: cborMap)
check not compiles(DeserializeTest.fromCbor(node))
test "fails to compile when field marked with 'serialize' pragma":
type FieldSerializeTest = object
normalField: int
pragmaField {.serialize.}: int
check not compiles(toCbor(FieldSerializeTest(normalField: 42, pragmaField: 100)))
test "fails to compile when field marked with 'deserialize' pragma":
type FieldDeserializeTest = object
normalField: int
pragmaField {.deserialize.}: int
let node = CborNode(kind: cborMap)
check not compiles(FieldDeserializeTest.fromCbor(node))
test "compiles when type has no pragmas":
type NoPragmaTest = object
value: int
check compiles(toCbor(NoPragmaTest(value: 42)))
let node = CborNode(kind: cborMap)
check compiles(NoPragmaTest.fromCbor(node))

View File

@ -0,0 +1,102 @@
# This file is a modified version of Emery Hemingways CBOR library for Nim,
# originally available at https://github.com/ehmry/cbor-nim and released under The Unlicense.
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
let js = parseCbor(controlCbor).toJson()
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
check($parseCbor(controlCbor) == 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
test name:
without testCbor =? toCbor(parseCbor(controlCbor)), error:
fail()
if controlCbor != testCbor:
let testB64 = base64.encode(testCbor)
check(controlB64 == testB64)
suite "hooks":
test "DateTime":
let dt = now()
without bin =? toCbor(dt), error:
fail()
check(parseCbor(bin).text == $dt)
test "Time":
let t = now().toTime
var bin = toCbor(t).tryValue
check(parseCbor(bin).getInt == t.toUnix)
test "tag":
var c = toCborNode("foo").tryValue
c.tag = some(99'u64)
check c.tag == some(99'u64)
test "sorting":
var map = initCborMap()
var keys =
@[
toCborNode(10).tryValue,
toCborNode(100).tryValue,
toCborNode(-1).tryValue,
toCborNode("z").tryValue,
toCborNode("aa").tryValue,
toCborNode([toCborNode(100).tryValue]).tryValue,
toCborNode([toCborNode(-1).tryValue]).tryValue,
toCborNode(false).tryValue,
]
shuffle(keys)
for k in keys:
map[k] = toCborNode(0).tryValue
check not map.isSorted.tryValue
check sort(map).isSuccess
check map.isSorted.tryValue
test "invalid wire type":
let node = CborNode(kind: cborText, text: "not an int")
let result = int.fromCbor(node)
check result.isFailure
check $result.error.msg ==
"deserialization to int failed: expected {cborUnsigned, cborNegative} but got cborText"

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

@ -2,4 +2,4 @@ switch("path", "..")
when (NimMajor, NimMinor) >= (1, 4):
switch("hint", "XCannotRaiseY:off")
when (NimMajor, NimMinor, NimPatch) >= (1, 6, 11):
switch("warning", "BareExcept:off")
switch("warning", "BareExcept:off")

View File

@ -99,7 +99,8 @@ suite "json - deserialize objects":
myint: int
let expected = MyRef(mystring: "abc", myint: 1)
let byteArray = """{
let byteArray =
"""{
"mystring": "abc",
"myint": 1
}""".toBytes

View File

@ -50,7 +50,8 @@ suite "json serialization - serialize":
let json = %*{"myobj": myobj, "mystuint": mystuint}
let expected = """{
let expected =
"""{
"myobj": {
"mystring": "abc",
"myint": 123,
@ -69,7 +70,8 @@ suite "json serialization - serialize":
let obj = %MyObj(mystring: "abc", myint: 1, mybool: true)
let expected = """{
let expected =
"""{
"mystring": "abc",
"myint": 1
}""".flatten
@ -83,7 +85,8 @@ suite "json serialization - serialize":
let obj = %MyRef(mystring: "abc", myint: 1)
let expected = """{
let expected =
"""{
"mystring": "abc",
"myint": 1
}""".flatten

View File

@ -3,5 +3,7 @@ import ./json/testDeserializeModes
import ./json/testPragmas
import ./json/testSerialize
import ./json/testSerializeModes
import ./cbor/testPrimitives
import ./cbor/testObjects
{.warning[UnusedImport]: off.}