mirror of
https://github.com/logos-storage/nim-serde.git
synced 2026-01-02 13:43:06 +00:00
Merge 2d8fa4d940a19a59551a59d79ca4dc38ce6ef104 into 5ced7c88b97d99c582285ce796957fb71fd42434
This commit is contained in:
commit
82252b8eb7
3
.gitignore
vendored
3
.gitignore
vendored
@ -5,4 +5,5 @@ nimble.develop
|
||||
nimble.paths
|
||||
.idea
|
||||
vendor/
|
||||
.vscode/
|
||||
.vscode/
|
||||
nimbledeps
|
||||
481
README.md
481
README.md
@ -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).
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
--styleCheck:usages
|
||||
--styleCheck:error
|
||||
--styleCheck:
|
||||
usages
|
||||
--styleCheck:
|
||||
error
|
||||
|
||||
# begin Nimble config (version 1)
|
||||
when fileExists("nimble.paths"):
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import ./serde/json
|
||||
import ./serde/cbor
|
||||
|
||||
export json
|
||||
export cbor
|
||||
|
||||
@ -9,7 +9,7 @@ skipDirs = @["tests"]
|
||||
# Dependencies
|
||||
requires "nim >= 1.6.14"
|
||||
requires "chronicles >= 0.10.3 & < 0.11.0"
|
||||
requires "questionable >= 0.10.13 & < 0.11.0"
|
||||
requires "questionable >= 0.10.15"
|
||||
requires "stint"
|
||||
requires "stew"
|
||||
|
||||
|
||||
7
serde/cbor.nim
Normal file
7
serde/cbor.nim
Normal file
@ -0,0 +1,7 @@
|
||||
import ./cbor/serializer
|
||||
import ./cbor/deserializer
|
||||
import ./cbor/jsonhook
|
||||
import ./cbor/types as ctypes
|
||||
import ./utils/types
|
||||
import ./utils/errors
|
||||
export serializer, deserializer, ctypes, types, errors, jsonhook
|
||||
379
serde/cbor/README.md
Normal file
379
serde/cbor/README.md
Normal 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
645
serde/cbor/deserializer.nim
Normal file
@ -0,0 +1,645 @@
|
||||
# This file is a modified version of Emery Hemingway’s 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
38
serde/cbor/errors.nim
Normal file
@ -0,0 +1,38 @@
|
||||
# This file is a modified version of Emery Hemingway’s 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
57
serde/cbor/helpers.nim
Normal file
@ -0,0 +1,57 @@
|
||||
# This file is a modified version of Emery Hemingway’s 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
45
serde/cbor/jsonhook.nim
Normal file
@ -0,0 +1,45 @@
|
||||
# This file is a modified version of Emery Hemingway’s 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
433
serde/cbor/serializer.nim
Normal file
@ -0,0 +1,433 @@
|
||||
# This file is a modified version of Emery Hemingway’s 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
170
serde/cbor/types.nim
Normal file
@ -0,0 +1,170 @@
|
||||
# This file is a modified version of Emery Hemingway’s 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
|
||||
@ -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
533
serde/json/README.md
Normal 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.
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 == ""
|
||||
|
||||
@ -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: [].}
|
||||
|
||||
|
||||
@ -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
11
serde/utils/errors.nim
Normal 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)
|
||||
@ -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
74
tests/benchmark.nim
Normal 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
236
tests/cbor/testObjects.nim
Normal 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
|
||||
45
tests/cbor/testPragmaChecks.nim
Normal file
45
tests/cbor/testPragmaChecks.nim
Normal 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))
|
||||
102
tests/cbor/testPrimitives.nim
Normal file
102
tests/cbor/testPrimitives.nim
Normal file
@ -0,0 +1,102 @@
|
||||
# This file is a modified version of Emery Hemingway’s 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
685
tests/cbor/test_vector.json
Normal file
@ -0,0 +1,685 @@
|
||||
[
|
||||
{
|
||||
"cbor": "AA==",
|
||||
"hex": "00",
|
||||
"roundtrip": true,
|
||||
"decoded": 0,
|
||||
"name": "uint_0"
|
||||
},
|
||||
{
|
||||
"cbor": "AQ==",
|
||||
"hex": "01",
|
||||
"roundtrip": true,
|
||||
"decoded": 1,
|
||||
"name": "uint_1"
|
||||
},
|
||||
{
|
||||
"cbor": "Cg==",
|
||||
"hex": "0a",
|
||||
"roundtrip": true,
|
||||
"decoded": 10,
|
||||
"name": "uint_10"
|
||||
},
|
||||
{
|
||||
"cbor": "Fw==",
|
||||
"hex": "17",
|
||||
"roundtrip": true,
|
||||
"decoded": 23,
|
||||
"name": "uint_23"
|
||||
},
|
||||
{
|
||||
"cbor": "GBg=",
|
||||
"hex": "1818",
|
||||
"roundtrip": true,
|
||||
"decoded": 24,
|
||||
"name": "uint_24"
|
||||
},
|
||||
{
|
||||
"cbor": "GBk=",
|
||||
"hex": "1819",
|
||||
"roundtrip": true,
|
||||
"decoded": 25,
|
||||
"name": "uint_25"
|
||||
},
|
||||
{
|
||||
"cbor": "GGQ=",
|
||||
"hex": "1864",
|
||||
"roundtrip": true,
|
||||
"decoded": 100,
|
||||
"name": "uint_100"
|
||||
},
|
||||
{
|
||||
"cbor": "GQPo",
|
||||
"hex": "1903e8",
|
||||
"roundtrip": true,
|
||||
"decoded": 1000,
|
||||
"name": "uint_1000"
|
||||
},
|
||||
{
|
||||
"cbor": "GgAPQkA=",
|
||||
"hex": "1a000f4240",
|
||||
"roundtrip": true,
|
||||
"decoded": 1000000,
|
||||
"name": "uint_1000000"
|
||||
},
|
||||
{
|
||||
"cbor": "GwAAAOjUpRAA",
|
||||
"hex": "1b000000e8d4a51000",
|
||||
"roundtrip": true,
|
||||
"decoded": 1000000000000,
|
||||
"name": "uint_1000000000000"
|
||||
},
|
||||
{
|
||||
"cbor": "IA==",
|
||||
"hex": "20",
|
||||
"roundtrip": true,
|
||||
"decoded": -1,
|
||||
"name": "nint_1"
|
||||
},
|
||||
{
|
||||
"cbor": "KQ==",
|
||||
"hex": "29",
|
||||
"roundtrip": true,
|
||||
"decoded": -10,
|
||||
"name": "nint_10"
|
||||
},
|
||||
{
|
||||
"cbor": "OGM=",
|
||||
"hex": "3863",
|
||||
"roundtrip": true,
|
||||
"decoded": -100,
|
||||
"name": "nint_100"
|
||||
},
|
||||
{
|
||||
"cbor": "OQPn",
|
||||
"hex": "3903e7",
|
||||
"roundtrip": true,
|
||||
"decoded": -1000,
|
||||
"name": "nint_1000"
|
||||
},
|
||||
{
|
||||
"cbor": "+QAA",
|
||||
"hex": "f90000",
|
||||
"roundtrip": true,
|
||||
"decoded": 0.0,
|
||||
"name": "float16_0"
|
||||
},
|
||||
{
|
||||
"cbor": "+YAA",
|
||||
"hex": "f98000",
|
||||
"roundtrip": true,
|
||||
"decoded": -0.0,
|
||||
"name": "float16_neg0"
|
||||
},
|
||||
{
|
||||
"cbor": "+TwA",
|
||||
"hex": "f93c00",
|
||||
"roundtrip": true,
|
||||
"decoded": 1.0,
|
||||
"name": "float16_1"
|
||||
},
|
||||
{
|
||||
"cbor": "+z/xmZmZmZma",
|
||||
"hex": "fb3ff199999999999a",
|
||||
"roundtrip": true,
|
||||
"decoded": 1.1,
|
||||
"name": "float64_1_1"
|
||||
},
|
||||
{
|
||||
"cbor": "+T4A",
|
||||
"hex": "f93e00",
|
||||
"roundtrip": true,
|
||||
"decoded": 1.5,
|
||||
"name": "float16_1_5"
|
||||
},
|
||||
{
|
||||
"cbor": "+Xv/",
|
||||
"hex": "f97bff",
|
||||
"roundtrip": true,
|
||||
"decoded": 65504.0,
|
||||
"name": "float16_65504"
|
||||
},
|
||||
{
|
||||
"cbor": "+kfDUAA=",
|
||||
"hex": "fa47c35000",
|
||||
"roundtrip": true,
|
||||
"decoded": 100000.0,
|
||||
"name": "float32_100000"
|
||||
},
|
||||
{
|
||||
"cbor": "+n9///8=",
|
||||
"hex": "fa7f7fffff",
|
||||
"roundtrip": true,
|
||||
"decoded": 3.4028234663852886e+38,
|
||||
"name": "float32_max"
|
||||
},
|
||||
{
|
||||
"cbor": "+3435DyIAHWc",
|
||||
"hex": "fb7e37e43c8800759c",
|
||||
"roundtrip": true,
|
||||
"decoded": 1e+300,
|
||||
"name": "float64_1e300"
|
||||
},
|
||||
{
|
||||
"cbor": "+QAB",
|
||||
"hex": "f90001",
|
||||
"roundtrip": true,
|
||||
"decoded": 5.960464477539063e-08,
|
||||
"name": "float16_min"
|
||||
},
|
||||
{
|
||||
"cbor": "+QQA",
|
||||
"hex": "f90400",
|
||||
"roundtrip": true,
|
||||
"decoded": 6.103515625e-05,
|
||||
"name": "float16_min_exp"
|
||||
},
|
||||
{
|
||||
"cbor": "+cQA",
|
||||
"hex": "f9c400",
|
||||
"roundtrip": true,
|
||||
"decoded": -4.0,
|
||||
"name": "float32_neg4"
|
||||
},
|
||||
{
|
||||
"cbor": "+8AQZmZmZmZm",
|
||||
"hex": "fbc010666666666666",
|
||||
"roundtrip": true,
|
||||
"decoded": -4.1,
|
||||
"name": "float64_neg4_1"
|
||||
},
|
||||
{
|
||||
"cbor": "+XwA",
|
||||
"hex": "f97c00",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "Infinity",
|
||||
"name": "float16_inf"
|
||||
},
|
||||
{
|
||||
"cbor": "+X4A",
|
||||
"hex": "f97e00",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "NaN",
|
||||
"name": "float16_nan"
|
||||
},
|
||||
{
|
||||
"cbor": "+fwA",
|
||||
"hex": "f9fc00",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "-Infinity",
|
||||
"name": "float16_neginf"
|
||||
},
|
||||
{
|
||||
"cbor": "+n+AAAA=",
|
||||
"hex": "fa7f800000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "Infinity",
|
||||
"name": "float32_inf"
|
||||
},
|
||||
{
|
||||
"cbor": "+n/AAAA=",
|
||||
"hex": "fa7fc00000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "NaN",
|
||||
"name": "float32_nan"
|
||||
},
|
||||
{
|
||||
"cbor": "+v+AAAA=",
|
||||
"hex": "faff800000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "-Infinity",
|
||||
"name": "float32_neginf"
|
||||
},
|
||||
{
|
||||
"cbor": "+3/wAAAAAAAA",
|
||||
"hex": "fb7ff0000000000000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "Infinity",
|
||||
"name": "float64_inf"
|
||||
},
|
||||
{
|
||||
"cbor": "+3/4AAAAAAAA",
|
||||
"hex": "fb7ff8000000000000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "NaN",
|
||||
"name": "float64_nan"
|
||||
},
|
||||
{
|
||||
"cbor": "+//wAAAAAAAA",
|
||||
"hex": "fbfff0000000000000",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "-Infinity",
|
||||
"name": "float64_neginf"
|
||||
},
|
||||
{
|
||||
"cbor": "9A==",
|
||||
"hex": "f4",
|
||||
"roundtrip": true,
|
||||
"decoded": false,
|
||||
"name": "false"
|
||||
},
|
||||
{
|
||||
"cbor": "9Q==",
|
||||
"hex": "f5",
|
||||
"roundtrip": true,
|
||||
"decoded": true,
|
||||
"name": "true"
|
||||
},
|
||||
{
|
||||
"cbor": "9g==",
|
||||
"hex": "f6",
|
||||
"roundtrip": true,
|
||||
"decoded": null,
|
||||
"name": "null"
|
||||
},
|
||||
{
|
||||
"cbor": "9w==",
|
||||
"hex": "f7",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "undefined",
|
||||
"name": "undefined"
|
||||
},
|
||||
{
|
||||
"cbor": "8A==",
|
||||
"hex": "f0",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "simple(16)",
|
||||
"name": "simple_16"
|
||||
},
|
||||
{
|
||||
"cbor": "+Bg=",
|
||||
"hex": "f818",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "simple(24)",
|
||||
"name": "simple_24"
|
||||
},
|
||||
{
|
||||
"cbor": "+P8=",
|
||||
"hex": "f8ff",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "simple(255)",
|
||||
"name": "simple_255"
|
||||
},
|
||||
{
|
||||
"cbor": "wHQyMDEzLTAzLTIxVDIwOjA0OjAwWg==",
|
||||
"hex": "c074323031332d30332d32315432303a30343a30305a",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "0(\"2013-03-21T20:04:00Z\")",
|
||||
"name": "tag0_datetime"
|
||||
},
|
||||
{
|
||||
"cbor": "wRpRS2ew",
|
||||
"hex": "c11a514b67b0",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "1(1363896240)",
|
||||
"name": "tag1_epoch"
|
||||
},
|
||||
{
|
||||
"cbor": "wftB1FLZ7CAAAA==",
|
||||
"hex": "c1fb41d452d9ec200000",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "1(1363896240.5)",
|
||||
"name": "tag1_epoch_float"
|
||||
},
|
||||
{
|
||||
"cbor": "10QBAgME",
|
||||
"hex": "d74401020304",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "23(h'01020304')",
|
||||
"name": "tag23_h64"
|
||||
},
|
||||
{
|
||||
"cbor": "2BhFZElFVEY=",
|
||||
"hex": "d818456449455446",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "24(h'6449455446')",
|
||||
"name": "tag24_b64url"
|
||||
},
|
||||
{
|
||||
"cbor": "2CB2aHR0cDovL3d3dy5leGFtcGxlLmNvbQ==",
|
||||
"hex": "d82076687474703a2f2f7777772e6578616d706c652e636f6d",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "32(\"http://www.example.com\")",
|
||||
"name": "tag32_uri"
|
||||
},
|
||||
{
|
||||
"cbor": "QA==",
|
||||
"hex": "40",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "h''",
|
||||
"name": "bstr_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "RAECAwQ=",
|
||||
"hex": "4401020304",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "h'01020304'",
|
||||
"name": "bstr_bytes"
|
||||
},
|
||||
{
|
||||
"cbor": "YA==",
|
||||
"hex": "60",
|
||||
"roundtrip": true,
|
||||
"decoded": "",
|
||||
"name": "tstr_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "YWE=",
|
||||
"hex": "6161",
|
||||
"roundtrip": true,
|
||||
"decoded": "a",
|
||||
"name": "tstr_a"
|
||||
},
|
||||
{
|
||||
"cbor": "ZElFVEY=",
|
||||
"hex": "6449455446",
|
||||
"roundtrip": true,
|
||||
"decoded": "IETF",
|
||||
"name": "tstr_ietf"
|
||||
},
|
||||
{
|
||||
"cbor": "YiJc",
|
||||
"hex": "62225c",
|
||||
"roundtrip": true,
|
||||
"decoded": "\"\\",
|
||||
"name": "tstr_escaped"
|
||||
},
|
||||
{
|
||||
"cbor": "YsO8",
|
||||
"hex": "62c3bc",
|
||||
"roundtrip": true,
|
||||
"decoded": "\u00fc",
|
||||
"name": "tstr_u00fc"
|
||||
},
|
||||
{
|
||||
"cbor": "Y+awtA==",
|
||||
"hex": "63e6b0b4",
|
||||
"roundtrip": true,
|
||||
"decoded": "\u6c34",
|
||||
"name": "tstr_u6c34"
|
||||
},
|
||||
{
|
||||
"cbor": "ZPCQhZE=",
|
||||
"hex": "64f0908591",
|
||||
"roundtrip": true,
|
||||
"decoded": "\ud800\udd51",
|
||||
"name": "tstr_u10151"
|
||||
},
|
||||
{
|
||||
"cbor": "gA==",
|
||||
"hex": "80",
|
||||
"roundtrip": true,
|
||||
"decoded": [],
|
||||
"name": "array_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "gwECAw==",
|
||||
"hex": "83010203",
|
||||
"roundtrip": true,
|
||||
"decoded": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"name": "array_123"
|
||||
},
|
||||
{
|
||||
"cbor": "gwGCAgOCBAU=",
|
||||
"hex": "8301820203820405",
|
||||
"roundtrip": true,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "array_nested"
|
||||
},
|
||||
{
|
||||
"cbor": "mBkBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgYGBk=",
|
||||
"hex": "98190102030405060708090a0b0c0d0e0f101112131415161718181819",
|
||||
"roundtrip": true,
|
||||
"decoded": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25
|
||||
],
|
||||
"name": "array_25items"
|
||||
},
|
||||
{
|
||||
"cbor": "oA==",
|
||||
"hex": "a0",
|
||||
"roundtrip": true,
|
||||
"decoded": {},
|
||||
"name": "map_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "ogECAwQ=",
|
||||
"hex": "a201020304",
|
||||
"roundtrip": true,
|
||||
"diagnostic": "{1: 2, 3: 4}",
|
||||
"name": "map_pairs"
|
||||
},
|
||||
{
|
||||
"cbor": "omFhAWFiggID",
|
||||
"hex": "a26161016162820203",
|
||||
"roundtrip": true,
|
||||
"decoded": {
|
||||
"a": 1,
|
||||
"b": [
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"name": "map_nested"
|
||||
},
|
||||
{
|
||||
"cbor": "gmFhoWFiYWM=",
|
||||
"hex": "826161a161626163",
|
||||
"roundtrip": true,
|
||||
"decoded": [
|
||||
"a",
|
||||
{
|
||||
"b": "c"
|
||||
}
|
||||
],
|
||||
"name": "map_mixed"
|
||||
},
|
||||
{
|
||||
"cbor": "pWFhYUFhYmFCYWNhQ2FkYURhZWFF",
|
||||
"hex": "a56161614161626142616361436164614461656145",
|
||||
"roundtrip": true,
|
||||
"decoded": {
|
||||
"a": "A",
|
||||
"b": "B",
|
||||
"c": "C",
|
||||
"d": "D",
|
||||
"e": "E"
|
||||
},
|
||||
"name": "map_strings"
|
||||
},
|
||||
{
|
||||
"cbor": "X0IBAkMDBAX/",
|
||||
"hex": "5f42010243030405ff",
|
||||
"roundtrip": false,
|
||||
"diagnostic": "h'0102030405'",
|
||||
"name": "indef_tstr"
|
||||
},
|
||||
{
|
||||
"cbor": "f2VzdHJlYWRtaW5n/w==",
|
||||
"hex": "7f657374726561646d696e67ff",
|
||||
"roundtrip": false,
|
||||
"decoded": "streaming",
|
||||
"name": "indef_array_empty"
|
||||
},
|
||||
{
|
||||
"cbor": "n/8=",
|
||||
"hex": "9fff",
|
||||
"roundtrip": false,
|
||||
"decoded": [],
|
||||
"name": "indef_array_1"
|
||||
},
|
||||
{
|
||||
"cbor": "nwGCAgOfBAX//w==",
|
||||
"hex": "9f018202039f0405ffff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "indef_array_2"
|
||||
},
|
||||
{
|
||||
"cbor": "nwGCAgOCBAX/",
|
||||
"hex": "9f01820203820405ff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "indef_array_3"
|
||||
},
|
||||
{
|
||||
"cbor": "gwGCAgOfBAX/",
|
||||
"hex": "83018202039f0405ff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "indef_array_4"
|
||||
},
|
||||
{
|
||||
"cbor": "gwGfAgP/ggQF",
|
||||
"hex": "83019f0203ff820405",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
[
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
4,
|
||||
5
|
||||
]
|
||||
],
|
||||
"name": "indef_array_long"
|
||||
},
|
||||
{
|
||||
"cbor": "nwECAwQFBgcICQoLDA0ODxAREhMUFRYXGBgYGf8=",
|
||||
"hex": "9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25
|
||||
],
|
||||
"name": "indef_map_1"
|
||||
},
|
||||
{
|
||||
"cbor": "v2FhAWFinwID//8=",
|
||||
"hex": "bf61610161629f0203ffff",
|
||||
"roundtrip": false,
|
||||
"decoded": {
|
||||
"a": 1,
|
||||
"b": [
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"name": "indef_map_2"
|
||||
},
|
||||
{
|
||||
"cbor": "gmFhv2FiYWP/",
|
||||
"hex": "826161bf61626163ff",
|
||||
"roundtrip": false,
|
||||
"decoded": [
|
||||
"a",
|
||||
{
|
||||
"b": "c"
|
||||
}
|
||||
],
|
||||
"name": "indef_map_3"
|
||||
},
|
||||
{
|
||||
"cbor": "v2NGdW71Y0FtdCH/",
|
||||
"hex": "bf6346756ef563416d7421ff",
|
||||
"roundtrip": false,
|
||||
"decoded": {
|
||||
"Fun": true,
|
||||
"Amt": -2
|
||||
},
|
||||
"name": "indef_map_4"
|
||||
}
|
||||
]
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user