Improve documentation and examples tests
parent
6201f2564f
commit
b666ac0c84
160
README.md
160
README.md
|
@ -1,4 +1,4 @@
|
|||
# CBOR
|
||||
# Crystal CBOR
|
||||
|
||||
[![builds.sr.ht status](https://builds.sr.ht/~arestifo/crystal-cbor.svg)](https://builds.sr.ht/~arestifo/crystal-cbor?)
|
||||
|
||||
|
@ -7,11 +7,67 @@ in Crystal.
|
|||
|
||||
**WARNING:** This library is still a work in progress.
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [API](./docs/api.md)
|
||||
- [Supported Tags](#supported-tags)
|
||||
- [Limitations](#limitations)
|
||||
- [Community](#community)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Features
|
||||
|
||||
- Full RFC 7049
|
||||
- Tested against all examples in the RFC 7049
|
||||
- Simple and powerful API inspired by the standard library JSON
|
||||
- Full support for diagnostic notation
|
||||
- Assign a field to a type base on the CBOR tag
|
||||
- Support for a wide range of IANA CBOR Tags (see below)
|
||||
- Support for a [wide range of IANA CBOR Tags](#supported-tags)
|
||||
- Support custom CBOR Tags
|
||||
|
||||
## Example
|
||||
|
||||
```crystal
|
||||
require "cbor"
|
||||
|
||||
class Location
|
||||
include CBOR::Serializable
|
||||
|
||||
@[CBOR::Field(key: "lat")]
|
||||
property latitude : Float64
|
||||
|
||||
@[CBOR::Field(key: "lng")]
|
||||
property longitude : Float64
|
||||
end
|
||||
|
||||
class House
|
||||
include CBOR::Serializable
|
||||
|
||||
property address : String
|
||||
property location : Location?
|
||||
end
|
||||
|
||||
data = {
|
||||
"address" => "Crystal Road 1234",
|
||||
"location" => { "lat" => 12.3, "lng" => 34.5 }
|
||||
}
|
||||
cbor = data.to_cbor # => Bytes[...]
|
||||
CBOR::Diagnostic.to_s(cbor) # => {"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}
|
||||
|
||||
house = House.from_cbor(cbor)
|
||||
house.address # => "Crystal Road 1234"
|
||||
house.location # => #<Location:0x10cd93d80 @latitude=12.3, @longitude=34.5>
|
||||
bytes = house.to_cbor # => Bytes[...]
|
||||
CBOR::Diagnostic.to_s(bytes) # => {_ "address": "Crystal Road 1234", "location": {_ "lat": 12.3, "lng": 34.5}}
|
||||
|
||||
data_array = [data]
|
||||
cbor_array = data_array.to_cbor # => Bytes[...]
|
||||
CBOR::Diagnostic.to_s(cbor) # => [{"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}]}
|
||||
|
||||
houses = Array(House).from_cbor(cbor_array)
|
||||
houses.size # => 1
|
||||
bytes = houses.to_cbor # => Bytes[...]
|
||||
CBOR::Diagnostic.to_s(bytes) # => [{_ "address": "Crystal Road 1234", "location": {_ "lat": 12.3, "lng": 34.5}}]
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -27,11 +83,105 @@ in Crystal.
|
|||
|
||||
## Usage
|
||||
|
||||
Including `CBOR::Serializable` will create `#to_cbor` and `self.from_cbor` methods
|
||||
on the current class, and a constructor which takes a `CBOR::Decoder`.
|
||||
|
||||
By default, these methods serialize into a cbor map containing the value of
|
||||
every instance variable, the keys being the instance variable name.
|
||||
|
||||
Most primitives and collections are supported as instance variable values (string,
|
||||
integer, array, hash, etc.), along with objects which define to_cbor and a
|
||||
constructor taking a `CBOR::Decoder`.
|
||||
|
||||
Union types are also supported, including unions with `nil`. If multiple types
|
||||
in a union parse correctly, it is undefined which one will be chosen.
|
||||
|
||||
To change how individual instance variables are parsed and serialized,
|
||||
the annotation `CBOR::Field` can be placed on the instance variable.
|
||||
Annotating property, getter and setter macros is also allowed.
|
||||
|
||||
```crystal
|
||||
require "cbor"
|
||||
|
||||
class A
|
||||
include CBOR::Serializable
|
||||
|
||||
@[CBOR::Field(key: "my_key")]
|
||||
getter a : Int32?
|
||||
end
|
||||
```
|
||||
|
||||
TODO: Write usage instructions here
|
||||
### `CBOR::Field` properties
|
||||
|
||||
- **ignore**: if `true` skip this field in serialization and deserialization
|
||||
(by default `false`)
|
||||
- **key**: the value of the key in the json object (by default the name of the
|
||||
instance variable)
|
||||
- **converter**: specify an alternate type for parsing and generation.
|
||||
The converter must define `from_cbor(CBOR::Decoder)` and
|
||||
`to_cbor(value, CBOR::Builder)` as class methods. Examples of converters are
|
||||
`Time::Format::RFC_333` and `Time::EpochConverter` for `Time`.
|
||||
- **presence**: if `true`, a `@{{key}}_present` instance variable will be generated
|
||||
when the key was present (even if it has a `null` value), `false` by default
|
||||
- **emit_null**: if `true`, emits a `null` value for nilable property
|
||||
(by default nulls are not emitted)
|
||||
- **nil_as_undefined**: if `true`, when the value is `nil`, it is emitted as
|
||||
`undefined` (by default `nil` are encoded as `null`)
|
||||
|
||||
Deserialization also respects default values of variables:
|
||||
|
||||
```
|
||||
require "cbor"
|
||||
|
||||
struct A
|
||||
include CBOR::Serializable
|
||||
@a : Int32
|
||||
@b : Float64 = 1.0
|
||||
end
|
||||
|
||||
A.from_cbor({"a" => 1}.to_cbor) # => A(@a=1, @b=1.0)
|
||||
```
|
||||
|
||||
### Extensions: `CBOR::Serializable::Unmapped`
|
||||
|
||||
If the `CBOR::Serializable::Unmapped` module is included, unknown properties in
|
||||
the CBOR document will be stored in a `Hash(String, CBOR::Type)`.
|
||||
|
||||
On serialization, any keys inside `cbor_unmapped` will be serialized and appended
|
||||
to the current cbor map.
|
||||
|
||||
```
|
||||
require "cbor"
|
||||
|
||||
struct A
|
||||
include CBOR::Serializable
|
||||
include CBOR::Serializable::Unmapped
|
||||
@a : Int32
|
||||
end
|
||||
|
||||
a = A.from_cbor({"a" => 1, "b" => 2}.to_cbor) # => A(@cbor_unmapped={"b" => 2}, @a=1)
|
||||
bytes = a.to_cbor # => Bytes[...]
|
||||
CBOR::Diagnostic.to_s(bytes) # => {_ "a": 1, "b": 2}
|
||||
```
|
||||
|
||||
### Class annotation `CBOR::Serializable::Options`
|
||||
|
||||
supported properties:
|
||||
|
||||
- **emit_nulls**: if `true`, emits a `null` value for all nilable properties
|
||||
(by default nulls are not emitted)
|
||||
- **nil_as_undefined**: if `true`, emits a `nil` value as undefined
|
||||
(by default nil emits `null`)
|
||||
|
||||
```
|
||||
require "json"
|
||||
|
||||
@[CBOR::Serializable::Options(emit_nulls: true)]
|
||||
class A
|
||||
include JSON::Serializable
|
||||
@a : Int32?
|
||||
end
|
||||
```
|
||||
|
||||
## Supported tags
|
||||
|
||||
|
@ -52,7 +202,7 @@ to be a `UInt64`.
|
|||
While this library supports lengths expressed as a `UInt64`, it must not exceed
|
||||
`Int32::MAX`.
|
||||
|
||||
## Development
|
||||
## Community
|
||||
|
||||
TODO: Write development instructions here
|
||||
|
||||
|
|
|
@ -115,19 +115,48 @@ describe CBOR::Serializable do
|
|||
|
||||
describe "documentation examples" do
|
||||
describe "house example" do
|
||||
houses = [House.new(address: "Crystal Road 1234", location: Location.new(latitude: 12.3, longitude: 34.5))]
|
||||
cbor_houses_bytes = Bytes[129, 191, 103, 97, 100, 100, 114, 101, 115, 115, 113, 67, 114, 121, 115, 116, 97, 108, 32, 82, 111, 97, 100, 32, 49, 50, 51, 52, 104, 108, 111, 99, 97, 116, 105, 111, 110, 191, 99, 108, 97, 116, 251, 64, 40, 153, 153, 153, 153, 153, 154, 99, 108, 110, 103, 251, 64, 65, 64, 0, 0, 0, 0, 0, 255, 255]
|
||||
data = {
|
||||
"address" => "Crystal Road 1234",
|
||||
"location" => {"lat" => 12.3, "lng" => 34.5},
|
||||
}
|
||||
bytes = data.to_cbor
|
||||
|
||||
it "encodes to cbor" do
|
||||
cbor = houses.to_cbor
|
||||
cbor.should eq(cbor_houses_bytes)
|
||||
it "has the correct starting data" do
|
||||
CBOR::Diagnostic.to_s(bytes).should eq(%({"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}))
|
||||
end
|
||||
|
||||
it "decodes form cbor" do
|
||||
decoded = Array(House).from_cbor(cbor_houses_bytes)
|
||||
it "decodes from CBOR" do
|
||||
house = House.from_cbor(bytes)
|
||||
|
||||
decoded.size.should eq(1)
|
||||
house = decoded[0]
|
||||
house.address.should eq("Crystal Road 1234")
|
||||
loc = house.location
|
||||
loc.should_not be_nil
|
||||
loc.not_nil!.latitude.should eq(12.3)
|
||||
loc.not_nil!.longitude.should eq(34.5)
|
||||
end
|
||||
|
||||
it "encodes to CBOR" do
|
||||
cbor = House.from_cbor(bytes).to_cbor
|
||||
CBOR::Diagnostic.to_s(cbor).should eq(%({_ "address": "Crystal Road 1234", "location": {_ "lat": 12.3, "lng": 34.5}}))
|
||||
end
|
||||
end
|
||||
|
||||
describe "houses array" do
|
||||
data = [{
|
||||
"address" => "Crystal Road 1234",
|
||||
"location" => {"lat" => 12.3, "lng" => 34.5},
|
||||
}]
|
||||
bytes = data.to_cbor
|
||||
|
||||
it "has the correct starting data" do
|
||||
CBOR::Diagnostic.to_s(bytes).should eq(%([{"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}]))
|
||||
end
|
||||
|
||||
it "decodes from CBOR" do
|
||||
houses = Array(House).from_cbor(bytes)
|
||||
|
||||
houses.size.should eq(1)
|
||||
house = houses[0]
|
||||
house.address.should eq("Crystal Road 1234")
|
||||
|
||||
loc = house.location
|
||||
|
@ -135,6 +164,12 @@ describe CBOR::Serializable do
|
|||
loc.not_nil!.latitude.should eq(12.3)
|
||||
loc.not_nil!.longitude.should eq(34.5)
|
||||
end
|
||||
|
||||
it "encodes to CBOR" do
|
||||
cbor = Array(House).from_cbor(bytes).to_cbor
|
||||
|
||||
CBOR::Diagnostic.to_s(cbor).should eq(%([{_ "address": "Crystal Road 1234", "location": {_ "lat": 12.3, "lng": 34.5}}]))
|
||||
end
|
||||
end
|
||||
|
||||
describe "default values example" do
|
||||
|
@ -142,5 +177,16 @@ describe CBOR::Serializable do
|
|||
A.from_cbor({"a" => 1}.to_cbor).inspect.should eq("A(@a=1, @b=1.0)")
|
||||
end
|
||||
end
|
||||
|
||||
describe "Unmapped extension" do
|
||||
it "decodes with the values in cbor_unmapped" do
|
||||
res = ExampleUnmapped.from_cbor({"a" => 1, "b" => 2}.to_cbor)
|
||||
|
||||
res.a.should eq(1)
|
||||
res.cbor_unmapped.should eq({"b" => 2})
|
||||
|
||||
CBOR::Diagnostic.to_s(res.to_cbor).should eq(%({_ "a": 1, "b": 2}))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,10 @@ class CBOR::Diagnostic
|
|||
@lexer = Lexer.new(input)
|
||||
end
|
||||
|
||||
def self.to_s(bytes : Bytes) : String
|
||||
self.new(bytes).to_s
|
||||
end
|
||||
|
||||
# Reads the content of the IO and prints out a diagnostic string
|
||||
# represation of the input.
|
||||
def to_s : String
|
||||
|
|
|
@ -8,7 +8,7 @@ class CBOR::Encoder
|
|||
def initialize(@io : IO = IO::Memory.new)
|
||||
end
|
||||
|
||||
def write(value : Nil.class, use_undefined : Bool = false)
|
||||
def write(value : Nil | Nil.class, use_undefined : Bool = false)
|
||||
write(use_undefined ? SimpleValue::Undefined : SimpleValue::Null)
|
||||
end
|
||||
|
||||
|
|
|
@ -21,18 +21,19 @@ module CBOR
|
|||
#
|
||||
# class House
|
||||
# include CBOR::Serializable
|
||||
#
|
||||
# property address : String
|
||||
# property location : Location?
|
||||
# end
|
||||
#
|
||||
# house = House.from_cbor(Bytes[191, 103, 97, 100, 100, 114, 101, 115, 115, 113, 67, 114, 121, 115, 116, 97, 108, 32, 82, 111, 97, 100, 32, 49, 50, 51, 52, 104, 108, 111, 99, 97, 116, 105, 111, 110, 191, 99, 108, 97, 116, 251, 64, 40, 153, 153, 153, 153, 153, 154, 99, 108, 110, 103, 251, 64, 65, 64, 0, 0, 0, 0, 0, 255, 255])
|
||||
# house = House.from_cbor({"address" => "Crystal Road 1234", "location" => {"lat" => 12.3, "lng" => 34.5}}.to_cbor)
|
||||
# house.address # => "Crystal Road 1234"
|
||||
# house.location # => #<Location:0x10cd93d80 @latitude=12.3, @longitude=34.5>
|
||||
# house.to_cbor # => Bytes[191, 103, 97, 100, 100, 114, 101, 115, 115, 113, 67, 114, 121, 115, 116, 97, 108, 32, 82, 111, 97, 100, 32, 49, 50, 51, 52, 104, 108, 111, 99, 97, 116, 105, 111, 110, 191, 99, 108, 97, 116, 251, 64, 40, 153, 153, 153, 153, 153, 154, 99, 108, 110, 103, 251, 64, 65, 64, 0, 0, 0, 0, 0, 255, 255]
|
||||
# house.to_cbor # => Bytes[...]
|
||||
#
|
||||
# houses = Array(House).from_json(Bytes[129, 191, 103, 97, 100, 100, 114, 101, 115, 115, 113, 67, 114, 121, 115, 116, 97, 108, 32, 82, 111, 97, 100, 32, 49, 50, 51, 52, 104, 108, 111, 99, 97, 116, 105, 111, 110, 191, 99, 108, 97, 116, 251, 64, 40, 153, 153, 153, 153, 153, 154, 99, 108, 110, 103, 251, 64, 65, 64, 0, 0, 0, 0, 0, 255, 255])
|
||||
# houses = Array(House).from_cbor([{"address" => "Crystal Road 1234", "location" => {"lat" => 12.3, "lng" => 34.5}}].to_cbor)
|
||||
# houses.size # => 1
|
||||
# houses.to_json # => Bytes[129, 191, 103, 97, 100, 100, 114, 101, 115, 115, 113, 67, 114, 121, 115, 116, 97, 108, 32, 82, 111, 97, 100, 32, 49, 50, 51, 52, 104, 108, 111, 99, 97, 116, 105, 111, 110, 191, 99, 108, 97, 116, 251, 64, 40, 153, 153, 153, 153, 153, 154, 99, 108, 110, 103, 251, 64, 65, 64, 0, 0, 0, 0, 0, 255, 255]
|
||||
# houses.to_cbor # Bytes[...]
|
||||
# ```
|
||||
#
|
||||
# ### Usage
|
||||
|
|
|
@ -74,25 +74,35 @@ module Time::Format::RFC_3339
|
|||
# [RFC 7049 section 2.4.1](https://tools.ietf.org/html/rfc7049#section-2.4.1).
|
||||
def self.to_cbor(value : Time, encoder : CBOR::Encoder)
|
||||
encoder.write(CBOR::Tag::RFC3339Time)
|
||||
self.format(self, fraction_digits: 0).to_cbor(encoder)
|
||||
value.format(value, fraction_digits: 0).to_cbor(encoder)
|
||||
end
|
||||
end
|
||||
|
||||
module Time::EpochConverter
|
||||
# Emits the time as a tagged unix timestamp, asp specified by
|
||||
# [RFC 7049 section 2.4.1](https://tools.ietf.org/html/rfc7049#section-2.4.1).
|
||||
#
|
||||
def self.to_cbor(value : Time, encoder : CBOR::Encoder)
|
||||
encoder.write(CBOR::Tag::EpochTime)
|
||||
value.to_unix.to_cbor(encoder)
|
||||
end
|
||||
end
|
||||
|
||||
struct Time
|
||||
# Emits the time as a tagged unix timestamp, asp specified by
|
||||
# Encodes the time as a properly tagged CBOR string as specified by
|
||||
# [RFC 7049 section 2.4.1](https://tools.ietf.org/html/rfc7049#section-2.4.1).
|
||||
#
|
||||
# If you would like to encode the time as a tagged RFC 3339 string isntead,
|
||||
# you can tag the field with the `Time::Format::RFC_3339` instead:
|
||||
# If you would like to encode it as a unix timestamp, you can instead specify
|
||||
# `Time::EpochConverter`:
|
||||
#
|
||||
# ```
|
||||
# class Foo
|
||||
# @[CBOR::Filed(converter: Time::Format::RFC_3339)]
|
||||
# @[CBOR::Filed(converter: Time::EpochConverter)]
|
||||
# property created_at : Time
|
||||
# end
|
||||
# ```
|
||||
def to_cbor(encoder : CBOR::Encoder)
|
||||
encoder.write(CBOR::Tag::EpochTime)
|
||||
self.to_unix.to_cbor(encoder)
|
||||
encoder.write(CBOR::Tag::RFC3339Time)
|
||||
self.format(self, fraction_digits: 0).to_cbor(encoder)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue