Expand description
Procedural macros to derive tinycbor’s Encode, Decode, and CborLen
traits.
Deriving is supported for structs and enums. The encoding is cannonical whenever possible,
and decoding is stricter than with ciborium or minicbor.
§Codec Specification
Deriving Encode, Decode and CborLen is supported for enums and structs. This section
goes over the representation of these containers, as well as how the attributes modify the
behaviour of the macros.
§Array representation for structs
A struct is encoded as a CBOR array by default. The fields are encoded in their order
of declaration. The Decode implementation allows for both definite and indefinite length
arrays, but the exact number of fields must be present in both cases. A missing or extra field
leads to a decoding error.
use tinycbor_derive::{Encode, Decode};
use tinycbor::{to_vec, Decoder, Token};
#[derive(Encode, Decode)]
struct Account {
email: String,
username: Option<String>,
password_hash: [u8; 32],
}
let encoded = to_vec(
&Account {
email: "me@yahoo.com".to_string(),
username: None,
password_hash: [0u8; 32],
}
);
let tokens = Decoder(&encoded).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(tokens, vec![
Token::Array(3),
Token::String("me@yahoo.com"),
Token::Null,
Token::Bytes(&[0u8; 32]),
]);§Map representation for structs
A struct can alternatively be encoded as a map by adding #[cbor(map)] on the container. The
fields are encoded as values within a map, with unsigned integers as keys. The Decode
implementation allows for both definite and indefinite length maps. All fields must be present
in the encoded map (unless #[cbor(optional)] is used), otherwise a decoding
error is returned. An unknown or repeated key causes a decoding error.
The key for each field is specified using the #[n(<u64>)] or #[cbor(n(<u64>))] attribute on
the field.
use tinycbor_derive::{Encode, Decode};
use tinycbor::{to_vec, Decoder, Token};
#[derive(Encode, Decode)]
#[cbor(map)]
struct Account {
#[n(0)]
email: String,
#[n(1)]
username: Option<String>,
#[n(2)]
password_hash: [u8; 32],
}
let encoded = to_vec(
&Account {
email: "me@yahoo.com".to_string(),
username: None,
password_hash: [0u8; 32],
}
);
let tokens = Decoder(&encoded).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(tokens, vec![
Token::Map(3),
Token::Int(0.into()),
Token::String("me@yahoo.com"),
Token::Int(1.into()),
Token::Null,
Token::Int(2.into()),
Token::Bytes(&[0u8; 32]),
]);§#[cbor(optional)]
If a field implements Default and PartialEq, it can be marked as optional. When encoding a
field marked as optional, the map entry is ommitted if the field value is equal to
Default::default(). When decoding and the map entry is missing, the field is initialized with
Default::default().
use tinycbor_derive::{Encode, Decode};
use tinycbor::{to_vec, Decoder, Token};
#[derive(Encode, Decode, PartialEq)]
#[cbor(map)]
struct Account {
#[n(0)]
email: String,
#[cbor(n(1), optional)]
username: Option<String>,
#[n(2)]
password_hash: [u8; 32],
}
let encoded = to_vec(
&Account {
email: "me@yahoo.com".to_string(),
username: None,
password_hash: [0u8; 32],
}
);
let tokens = Decoder(&encoded).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(tokens, vec![
Token::Map(2),
Token::Int(0u8.into()),
Token::String("me@yahoo.com"),
Token::Int(2u8.into()),
Token::Bytes(&[0u8; 32]),
]);§Representation for enums
An enum is encoded as a CBOR array. The first element of the array is the variant tag of the
enum (a u64), followed by the encoded fields of the variant (if any).
The variant tag for each variant is specified using the #[n(<u64>)] or #[cbor(n(<u64>))]
attribute on the variant.
use tinycbor_derive::{Encode, Decode};
use tinycbor::{to_vec, Decoder, Token};
#[derive(Encode, Decode)]
enum Shape {
#[n(0)]
Dot,
#[n(1)]
Circle { radius: f64 },
#[n(2)]
Rectangle { width: f64, height: f64 },
}
let encoded = to_vec(&Shape::Rectangle { width: 3.0, height: 4.0 });
let tokens = Decoder(&encoded).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(tokens, vec![
Token::Array(3),
Token::Int(2u8.into()),
Token::Float(3.0),
Token::Float(4.0),
]);§Attributes
Unless otherwise noted, the attributes should only be specified on the container.
§#[cbor(error = "<Ident>")]
This attribute gives the name of the error type to be generated when deriving Decode. It
has no effect when deriving Encode or CborLen. By default, the error type is named
Error.
use tinycbor_derive::{Decode};
#[derive(Decode)]
#[cbor(error = "PersonError")]
struct Person {
name: String,
age: u16,
}
#[derive(Decode)]
struct Company {
name: String,
ceo: Person,
employees: Vec<Person>,
}
type PersonErrorType = PersonError;
type CompanyErrorType = Error;The error type generated by the macro uses associated types, which makes the generated
documentation cluttered. To make it look normal, use the -Znormalize-docs flag on rustdoc.
§#[cbor(recursive)]
This attribute is required when deriving Decode for a recursive type. Without it, the
error type is infinitely sized, causing a compilation error.
use tinycbor_derive::Decode;
#[derive(Decode)]
enum List {
#[n(0)]
Nil,
#[n(1)]
Cons(u64, Box<List>),
}This solves the issue by boxing the error type, and thus requires an allocator to be available.
use tinycbor_derive::Decode;
#[derive(Decode)]
#[cbor(recursive)]
enum List {
#[n(0)]
Nil,
#[n(1)]
Cons(u64, Box<List>),
}§#[cbor({encode_,decode_,len_,}bound = "<WherePredicate>")]
Used to provide bounds on the derived implementations of Encode, Decode and CborLen.
The bound statement applies to all three traits, and the others apply to their respective
trait.
Note that by default, the macros do not add any bounds to generic type parameters. This
requires the user to add necessary bounds manually. The exception to this rule are lifetimes.
For Decode, the lifetime attached to the trait is bound to outlive all other lifetimes in the
type. This translates to:
impl <'de, 'a, 'b, /* ... */> Decode<'de> for MyType<'a, 'b, /* ... */>
where
'de: 'a + 'b + /* ... */,
// User specified bounds...
{
// Implementation...
}For decode_bound the predicate is allowed to use the special lifetime '_, which is
replaced with the trait input lifetime when deriving Decode.
use std::borrow::Cow;
use tinycbor_derive::{Encode, Decode, CborLen};
use tinycbor::{Encode, Decode, CborLen};
#[derive(Encode, Decode, CborLen)]
#[cbor(bound = "L: std::fmt::Debug")]
#[cbor(decode_bound = "L: Decode<'_>")]
#[cbor(encode_bound = "L: Encode")]
#[cbor(len_bound = "L: CborLen")]
struct Car<'a, L> {
model: Cow<'a, str>,
kilometers: u64,
license: L,
}
§#[cbor(naked)]
Removes the outer CBOR array from the representation. That is, array structs and enums
are encoded as their contents only. map structs cannot be marked as naked, as they can
have varying lengths.
The simplest usecase is to make fieldless enums encode and decode as integers instead of
single-element arrays.
use tinycbor_derive::{Encode, Decode, CborLen};
#[derive(Encode, Decode, CborLen)]
#[cbor(naked)]
enum Status {
#[n(0)]
Ok,
#[n(1)]
Error,
#[n(2)]
Sunday,
}When the resulting representation is more than one CBOR element, the type should not be used as part of a larger structure without care. Consider the following example:
use tinycbor_derive::Encode;
use tinycbor::{Decoder, to_vec, Token};
#[derive(Encode)]
#[cbor(naked)]
struct Danger {
value: u64,
oh_no: i64,
}
#[derive(Encode)]
struct Careless {
value: Danger,
}
let cbor = to_vec(&Careless { value: Danger { value: 42, oh_no: -7 } });
let tokens = Decoder(&cbor).collect::<Result<Vec<_>, _>>().unwrap();
// This is probably not what you want!
assert_eq!(
tokens,
vec![
Token::Array(1),
Token::Int(42.into()),
Token::Int((-7).into()),
]
);§#[cbor(tag(<u64>))]
This attribute is allowed on containers and fields. It specifies a CBOR tag that wraps the encoded value.
use tinycbor_derive::{Encode, Decode, CborLen};
#[derive(Encode, Decode, CborLen)]
#[cbor(tag(0))]
struct SavingsAccount {
unlocked: u64,
#[cbor(tag(55))]
locked: u64,
rate: f32
}§#[cbor({encode_,decode_,len_,}with = "<Type>")]
This attribute is only allowed on fields. It specifies a type that provides custom Encode,
Decode and/or CborLen implementations.
When deriving Decode and using #[cbor({decode_,}with = "<Type>")], <Type> must implement
Into<T>, or have a method named into with signature <Type> -> T, where T is the field
type. All occurences of '_ lifetimes within <Type> are replaced with the trait input
lifetime.
When deriving Encode or CborLen and using #[cbor({encode_,len_,}with = "<Type>")],
&T must implement Into<&<Type>>.
use tinycbor_derive::{Encode, Decode, CborLen};
#[derive(Encode, Decode, CborLen)]
struct HotelRoom {
#[cbor(decode_with = "u16")]
number: u32,
#[cbor(with = "tinycbor::num::U8")]
floor: u8,
#[cbor(decode_with = "&'_ str")]
name: String,
#[cbor(with = "tinycbor::Encoded<Vec<String>>")]
amenities: Vec<String>,
}
Interaction with #[cbor(optional)]:
With #[cbor(optional, {encode_,len_,}with = "<Type>")], the original field type’s
Default implementation is used for field initialization and encoding checks, rather than the type provided
in with.
A recurring pattern is to use Option<T> fields with #[cbor(optional)]. Note that in this
case, both “the field is absent” and “the field is present with value null” are accepted
when decoding. If only the former behaviour is desired (i.e., the field being null should
error), one can use the following:
use tinycbor_derive::{Encode, Decode};
#[derive(Encode, Decode)]
#[cbor(map)]
struct Account {
#[n(0)]
email: String,
#[cbor(n(1), decode_with = "String", optional)] // Using `decode_with = "String` prevents `null`.
username: Option<String>,
#[n(2)]
password_hash: [u8; 32],
}