Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions ENHANCEMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# String-Keyed CBOR Maps

This branch adds two features for encoding structs and enums as CBOR maps
with string keys instead of integer indices.

Based on [issue #52](https://github.com/twittner/minicbor/issues/52) and
the `#[s("...")]` string index support from @twittner's
[PR #53](https://github.com/twittner/minicbor/pull/53).

## `#[s("...")]` per-field string index

Works like `#[n(0)]`, but uses a string key:

```rust
use minicbor::{Encode, Decode};

#[derive(Encode, Decode)]
#[cbor(map)]
struct SensorData {
#[s("temperature")]
temperature: i16,
#[s("humidity")]
humidity: u8,
#[n(2)]
flags: u8, // numeric and string indices can coexist
}
```

Wire format (CBOR diagnostic notation):

```
{"temperature": 23, "humidity": 65, 2: 7}
```

## `#[cbor(text_keys)]` for automatic string keys

Applies string keys to all fields automatically, using their Rust
identifiers. No per-field annotations needed:

```rust
#[derive(Encode, Decode)]
#[cbor(text_keys)]
struct SensorData {
temperature: i16,
humidity: u8,
active: bool,
}
```

Wire format:

```
{"temperature": 23, "humidity": 65, "active": true}
```

`text_keys` implies map encoding, so `#[cbor(map)]` is not required.

### Renaming keys

Use `#[cbor(key = "...")]` to override individual key names:

```rust
#[derive(Encode, Decode)]
#[cbor(text_keys)]
struct SensorData {
#[cbor(key = "t")]
temperature: i16,
#[cbor(key = "h")]
humidity: u8,
active: bool,
}
```

Wire format:

```
{"t": 23, "h": 65, "active": true}
```

### Optional fields

`Option<T>` fields that are `None` are omitted from the map:

```rust
#[derive(Encode, Decode)]
#[cbor(text_keys)]
struct Config {
name: u8,
#[cbor(key = "interval_ms")]
interval: Option<u32>,
}

// Config { name: 1, interval: None } encodes as {"name": 1}
// Config { name: 1, interval: Some(500) } encodes as {"name": 1, "interval_ms": 500}
```

### Skipping fields

`#[cbor(skip)]` excludes fields from encoding. They get `Default::default()`
on decode:

```rust
#[derive(Encode, Decode)]
#[cbor(text_keys)]
struct State {
value: u16,
#[cbor(skip)]
cached: u32, // not encoded, defaults to 0 on decode
}
```

### Nested structs

```rust
#[derive(Encode, Decode)]
#[cbor(text_keys)]
struct Inner { a: u8 }

#[derive(Encode, Decode)]
#[cbor(text_keys)]
struct Outer {
inner: Inner,
b: u16,
}

// Outer { inner: Inner { a: 1 }, b: 2 }
// encodes as {"inner": {"a": 1}, "b": 2}
```

### Enums

Unit variants encode as a plain string, non-unit variants as a
single-entry map:

```rust
#[derive(Encode, Decode)]
#[cbor(text_keys)]
enum Command {
Reset,
SetInterval { #[s("ms")] ms: u32 },
}

// Command::Reset encodes as "Reset"
// Command::SetInterval { ms: 500 } encodes as {"SetInterval": {"ms": 500}}
```

### Generics

Trait bounds are derived automatically:

```rust
#[derive(Encode, Decode)]
#[cbor(text_keys)]
struct Wrapper<T> {
value: T,
label: u8,
}
```

### Collections

Standard collection types work as field values:

```rust
use std::collections::BTreeMap;

#[derive(Encode, Decode)]
#[cbor(text_keys)]
struct DataSet {
readings: Vec<u16>,
metadata: BTreeMap<u32, u16>,
name: String,
}
```

## Decode behavior

- Unknown keys are skipped (forward compatible)
- Missing optional fields default to `None` (backward compatible)
- Missing required fields produce a decode error
- Key order does not matter
- Definite and indefinite-length maps are both supported

## Compile-time checks

The following produce compile errors:

- `text_keys` on a tuple struct (fields have no names)
- `text_keys` combined with `transparent` or `index_only`
- `key = "..."` without `text_keys` on the struct or enum
- Two fields with the same key name

## `no_std` and `no_alloc`

Both features work on `no_std` and `no_alloc` targets.
`Encoder::str()` writes directly, `Decoder::str()` borrows from the
input buffer. No heap allocation anywhere.
58 changes: 55 additions & 3 deletions minicbor-derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ pub enum Kind {
Skip,
SkipIf,
Flat,
Default
Default,
TextKeys,
Key
}

#[derive(Debug, Clone)]
Expand All @@ -68,7 +70,9 @@ enum Value {
Skip(proc_macro2::Span),
SkipIf(Option<syn::ExprPath>, proc_macro2::Span),
Flat(proc_macro2::Span),
Default(proc_macro2::Span)
Default(proc_macro2::Span),
TextKeys(proc_macro2::Span),
Key(String, proc_macro2::Span)
}

#[derive(Debug, Copy, Clone)]
Expand Down Expand Up @@ -138,6 +142,14 @@ impl Attributes {
{
return Err(syn::Error::new(*s, "flat enum does not support map encoding"))
}
if let Some(Value::TextKeys(s)) = this.get(Kind::TextKeys) {
if this.contains_key(Kind::Transparent) {
return Err(syn::Error::new(*s, "`text_keys` and `transparent` are mutually exclusive"))
}
if this.contains_key(Kind::IndexOnly) {
return Err(syn::Error::new(*s, "`text_keys` and `index_only` are mutually exclusive"))
}
}
// `skip_if` triggers the creation of a custom codec where `encode` and `decode`
// correspond to the default routines, `is_nil` is defined via `skip_if`'s
// predicate and `nil` points to an internal helper that matches the signature
Expand Down Expand Up @@ -385,6 +397,11 @@ impl Attributes {
attrs.try_insert(Kind::Skip, Value::Skip(meta.path.span()))?
} else if meta.path.is_ident("flat") {
attrs.try_insert(Kind::Flat, Value::Flat(meta.path.span()))?
} else if meta.path.is_ident("text_keys") {
attrs.try_insert(Kind::TextKeys, Value::TextKeys(meta.path.span()))?
} else if meta.path.is_ident("key") {
let s: LitStr = meta.value()?.parse()?;
attrs.try_insert(Kind::Key, Value::Key(s.value(), meta.path.span()))?
} else if meta.path.is_ident("default") {
attrs.try_insert(Kind::Default, Value::Default(meta.path.span()))?
} else {
Expand Down Expand Up @@ -456,6 +473,23 @@ impl Attributes {
self.contains_key(Kind::Default)
}

pub fn text_keys(&self) -> bool {
self.contains_key(Kind::TextKeys)
}

/// Effective encoding: text_keys implies Map.
pub fn effective_encoding(&self) -> Encoding {
if self.text_keys() {
Encoding::Map
} else {
self.encoding().unwrap_or_default()
}
}

pub fn key(&self) -> Option<&str> {
self.get(Kind::Key).and_then(|v| v.key())
}

fn contains_key(&self, k: Kind) -> bool {
self.attrs.contains_key(&k)
}
Expand All @@ -479,6 +513,7 @@ impl Attributes {
| Kind::Transparent
| Kind::ContextBound
| Kind::Tag
| Kind::TextKeys
=> {}
| Kind::Borrow
| Kind::TypeParam
Expand All @@ -493,6 +528,7 @@ impl Attributes {
| Kind::SkipIf
| Kind::Flat
| Kind::Default
| Kind::Key
=> {
let msg = format!("attribute is not supported on {}-level", self.level);
return Err(syn::Error::new(val.span(), msg))
Expand All @@ -511,12 +547,14 @@ impl Attributes {
| Kind::Skip
| Kind::SkipIf
| Kind::Default
| Kind::Key
=> {}
| Kind::Encoding
| Kind::IndexOnly
| Kind::Transparent
| Kind::ContextBound
| Kind::Flat
| Kind::TextKeys
=> {
let msg = format!("attribute is not supported on {}-level", self.level);
return Err(syn::Error::new(val.span(), msg))
Expand All @@ -528,6 +566,7 @@ impl Attributes {
| Kind::ContextBound
| Kind::Tag
| Kind::Flat
| Kind::TextKeys
=> {}
| Kind::Borrow
| Kind::TypeParam
Expand All @@ -541,6 +580,7 @@ impl Attributes {
| Kind::Skip
| Kind::SkipIf
| Kind::Default
| Kind::Key
=> {
let msg = format!("attribute is not supported on {}-level", self.level);
return Err(syn::Error::new(val.span(), msg))
Expand All @@ -550,6 +590,7 @@ impl Attributes {
| Kind::Encoding
| Kind::Index
| Kind::Tag
| Kind::Key
=> {}
| Kind::Borrow
| Kind::TypeParam
Expand All @@ -565,6 +606,7 @@ impl Attributes {
| Kind::SkipIf
| Kind::Flat
| Kind::Default
| Kind::TextKeys
=> {
let msg = format!("attribute is not supported on {}-level", self.level);
return Err(syn::Error::new(val.span(), msg))
Expand Down Expand Up @@ -743,7 +785,9 @@ impl Value {
Value::Skip(s) => *s,
Value::SkipIf(_, s) => *s,
Value::Flat(s) => *s,
Value::Default(s) => *s
Value::Default(s) => *s,
Value::TextKeys(s) => *s,
Value::Key(_, s) => *s
}
}

Expand Down Expand Up @@ -810,6 +854,14 @@ impl Value {
None
}
}

fn key(&self) -> Option<&str> {
if let Value::Key(s, _) = self {
Some(s)
} else {
None
}
}
}

fn parse_i64_arg(a: &syn::Attribute) -> syn::Result<i64> {
Expand Down
Loading