mirror of
https://github.com/logos-storage/nim-serde.git
synced 2026-01-08 16:43:08 +00:00
380 lines
10 KiB
Markdown
380 lines
10 KiB
Markdown
# 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
|
|
```
|
|
|