mirror of
https://github.com/trussed-dev/serde-indexed.git
synced 2026-06-20 04:16:32 -07:00
Implement offsets and skip_serialization_if
This commit is contained in:
+4
-2
@@ -1,9 +1,11 @@
|
||||
[package]
|
||||
name = "serde-indexed"
|
||||
version = "0.0.0-unreleased"
|
||||
version = "0.0.1"
|
||||
authors = ["Nicolas Stalder <n@stalder.io>"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
description = "Derive Serialize and Deserialize that replaces struct keys with numerical indices."
|
||||
description = "Derivation of Serialize and Deserialize that replaces struct keys with numerical indices."
|
||||
categories = ["embedded", "encoding", "no-std"]
|
||||
keywords = ["serde", "cbor", "rust", "no-std"]
|
||||
repository = "https://github.com/nickray/serde-indexed"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
## Overview
|
||||
## serde-indexed
|
||||
|
||||
Derive Serialize and Deserialize that replaces struct keys with numerical indices.
|
||||
Derivation of [`Serialize`][serialize] and [`Deserialize`][deserialize] that replaces struct keys with numerical indices.
|
||||
|
||||
**WIP**. Primary use case is to handle CTAP CBOR messages.
|
||||
Primary use case is to handle [CTAP CBOR][ctap-cbor] messages, in particular support for:
|
||||
- [`skip_serializing_if`][skip-serializing-if] for optional keys
|
||||
- configurable index `offset`
|
||||
|
||||
Missing features:
|
||||
- `skip_serializing_if` for optional keys.
|
||||
- configurable index offset
|
||||
#### Example
|
||||
|
||||
This is my attempt to learn proc-macros, I'm roughly following [`serde-repr`][serde-repr].
|
||||
```rust
|
||||
#[derive(Clone, Debug, PartialEq, SerializeIndexed, DeserializeIndexed)]
|
||||
#[serde_indexed(offset = 1)]
|
||||
pub struct SomeKeys {
|
||||
pub number: i32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub option: Option<u8>,
|
||||
pub bytes: [u8; 7],
|
||||
}
|
||||
```
|
||||
|
||||
Tip: To "see" the generated code, run `cargo expand --test basics`.
|
||||
This was a nice opportunity to learn proc-macros, I roughly followed [`serde-repr`][serde-repr].
|
||||
|
||||
To see some generated code, run `cargo expand --test basics`.
|
||||
|
||||
[serialize]: https://docs.serde.rs/serde/ser/trait.Serialize.html
|
||||
[deserialize]: https://docs.serde.rs/serde/de/trait.Deserialize.html
|
||||
[ctap-cbor]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ctap2-canonical-cbor-encoding-form
|
||||
[skip-serializing-if]: https://serde.rs/field-attrs.html#skip_serializing_if
|
||||
[serde-repr]: https://github.com/dtolnay/serde-repr
|
||||
|
||||
#### License
|
||||
|
||||
+84
-21
@@ -1,4 +1,30 @@
|
||||
//! Derive `Serialize` and `Deserialize` that replaces struct keys with numerical indices.
|
||||
/*! Derivation of [`Serialize`][serialize] and [`Deserialize`][deserialize] that replaces struct keys with numerical indices.
|
||||
|
||||
### Usage example
|
||||
The macros currently understand `serde`'s [`skip_if_serialized`][skip-serializing-if] field attribute
|
||||
and a custom `offset` container attribute.
|
||||
|
||||
```
|
||||
use serde_indexed::{DeserializeIndexed, SerializeIndexed};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, SerializeIndexed, DeserializeIndexed)]
|
||||
#[serde_indexed(offset = 1)]
|
||||
pub struct SomeKeys {
|
||||
pub number: i32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub option: Option<u8>,
|
||||
pub bytes: [u8; 7],
|
||||
}
|
||||
```
|
||||
|
||||
### Generated code example
|
||||
`cargo expand --test basics` exercises the macros using [`serde_cbor`][serde-cbor].
|
||||
|
||||
[serialize]: https://docs.serde.rs/serde/ser/trait.Serialize.html
|
||||
[deserialize]: https://docs.serde.rs/serde/de/trait.Deserialize.html
|
||||
[skip-serializing-if]: https://serde.rs/field-attrs.html#skip_serializing_if
|
||||
[serde-cbor]: https://docs.rs/serde_cbor
|
||||
*/
|
||||
|
||||
extern crate proc_macro;
|
||||
|
||||
@@ -10,28 +36,55 @@ use syn::parse_macro_input;
|
||||
|
||||
use crate::parse::Input;
|
||||
|
||||
// TODO: make this configurable with container attribute
|
||||
const OFFSET: usize = 1;
|
||||
|
||||
fn serialize_visitor(fields: &[parse::Field], offset: usize) -> Vec<proc_macro2::TokenStream> {
|
||||
fn serialize_fields(fields: &[parse::Field], offset: usize) -> Vec<proc_macro2::TokenStream> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
let index = field.index + offset;
|
||||
let member = &field.member;
|
||||
quote! {
|
||||
map.serialize_entry(&#index, &self.#member)?;
|
||||
// println!("field {:?} index {:?}", &field.label, field.index);
|
||||
match &field.skip_serializing_if {
|
||||
Some(path) => {
|
||||
quote! {
|
||||
if !#path(&self.#member) {
|
||||
map.serialize_entry(&#index, &self.#member)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
quote! {
|
||||
map.serialize_entry(&#index, &self.#member)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(SerializeIndexed)]
|
||||
fn count_serialized_fields(fields: &[parse::Field]) -> Vec<proc_macro2::TokenStream> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
// let index = field.index + offset;
|
||||
let member = &field.member;
|
||||
match &field.skip_serializing_if {
|
||||
Some(path) => {
|
||||
quote! { + if #path(&self.#member) { 0 } else { 1 } }
|
||||
}
|
||||
None => {
|
||||
quote! { + 1 }
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(SerializeIndexed, attributes(serde, serde_indexed))]
|
||||
pub fn derive_serialize(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as Input);
|
||||
let ident = input.ident;
|
||||
let num_fields = input.fields.len();
|
||||
let serialize_fields = serialize_visitor(&input.fields, OFFSET);
|
||||
let num_fields = count_serialized_fields(&input.fields);
|
||||
let serialize_fields = serialize_fields(&input.fields, input.attrs.offset);
|
||||
|
||||
TokenStream::from(quote! {
|
||||
impl serde::Serialize for #ident {
|
||||
@@ -40,7 +93,10 @@ pub fn derive_serialize(input: TokenStream) -> TokenStream {
|
||||
S: serde::Serializer
|
||||
{
|
||||
use serde::ser::SerializeMap;
|
||||
let mut map = serializer.serialize_map(Some(#num_fields))?;
|
||||
let num_fields = 0
|
||||
#(#num_fields)*
|
||||
;
|
||||
let mut map = serializer.serialize_map(Some(num_fields))?;
|
||||
|
||||
#(#serialize_fields)*
|
||||
|
||||
@@ -62,14 +118,21 @@ fn none_fields(fields: &[parse::Field]) -> Vec<proc_macro2::TokenStream> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unwrap_fields(fields: &[parse::Field]) -> Vec<proc_macro2::TokenStream> {
|
||||
fn unwrap_expected_fields(fields: &[parse::Field]) -> Vec<proc_macro2::TokenStream> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
let label = stringify!(field.label.clone());
|
||||
let label = field.label.clone();
|
||||
let ident = syn::Ident::new(&field.label, proc_macro2::Span::call_site());
|
||||
quote! {
|
||||
let #ident = #ident.ok_or_else(|| serde::de::Error::missing_field(#label))?;
|
||||
if field.skip_serializing_if.is_none() {
|
||||
quote! {
|
||||
let #ident = #ident.ok_or_else(|| serde::de::Error::missing_field(#label))?;
|
||||
}
|
||||
} else {
|
||||
// TODO: still confused here, but the tests pass ;)
|
||||
quote! {
|
||||
// let #ident = #ident.or(None);
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@@ -79,7 +142,7 @@ fn match_fields(fields: &[parse::Field], offset: usize) -> Vec<proc_macro2::Toke
|
||||
fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
let label = stringify!(field.label.clone());
|
||||
let label = field.label.clone();
|
||||
let ident = syn::Ident::new(&field.label, proc_macro2::Span::call_site());
|
||||
let index = field.index + offset;
|
||||
quote! {
|
||||
@@ -106,13 +169,13 @@ fn all_fields(fields: &[parse::Field]) -> Vec<proc_macro2::TokenStream> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(DeserializeIndexed)]
|
||||
#[proc_macro_derive(DeserializeIndexed, attributes(serde, serde_indexed))]
|
||||
pub fn derive_deserialize(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as Input);
|
||||
let ident = input.ident;
|
||||
let none_fields = none_fields(&input.fields);
|
||||
let unwrap_fields = unwrap_fields(&input.fields);
|
||||
let match_fields = match_fields(&input.fields, OFFSET);
|
||||
let unwrap_expected_fields = unwrap_expected_fields(&input.fields);
|
||||
let match_fields = match_fields(&input.fields, input.attrs.offset);
|
||||
let all_fields = all_fields(&input.fields);
|
||||
|
||||
TokenStream::from(quote! {
|
||||
@@ -127,7 +190,7 @@ pub fn derive_deserialize(input: TokenStream) -> TokenStream {
|
||||
type Value = #ident;
|
||||
|
||||
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
formatter.write_str("struct #ident")
|
||||
formatter.write_str(stringify!(#ident))
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<#ident, V::Error>
|
||||
@@ -145,7 +208,7 @@ pub fn derive_deserialize(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
}
|
||||
|
||||
#(#unwrap_fields)*
|
||||
#(#unwrap_expected_fields)*
|
||||
|
||||
Ok(#ident { #(#all_fields),* })
|
||||
}
|
||||
|
||||
@@ -4,18 +4,73 @@ use syn::{Data, DeriveInput, Fields, Ident, Token};
|
||||
|
||||
pub struct Input {
|
||||
pub ident: Ident,
|
||||
pub attrs: StructAttrs,
|
||||
pub fields: Vec<Field>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StructAttrs {
|
||||
pub offset: usize,
|
||||
// pub skip_nones: bool,
|
||||
}
|
||||
|
||||
pub struct Field {
|
||||
pub label: String,
|
||||
pub member: syn::Member,
|
||||
pub index: usize,
|
||||
pub skip_serializing_if: Option<syn::ExprPath>,
|
||||
// pub attrs: attr::Field,
|
||||
pub ty: syn::Type,
|
||||
pub original: syn::Field,
|
||||
}
|
||||
|
||||
fn parse_meta(attrs: &mut StructAttrs, meta: &syn::Meta) -> Result<()> {
|
||||
if let syn::Meta::List(value) = meta {
|
||||
for meta in &value.nested {
|
||||
match meta {
|
||||
syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) => {
|
||||
if name_value.path.is_ident("offset") {
|
||||
if let syn::Lit::Int(offset) = &name_value.lit {
|
||||
attrs.offset = offset.base10_parse()?;
|
||||
// println!("shall use offset {}", attrs.offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
// This `skip_nones` approach is tricky, as then we
|
||||
// need to detect Option types, which means a lot of path
|
||||
// manipulation, possibly in vain.
|
||||
//
|
||||
// syn::NestedMeta::Meta(syn::Meta::Path(path)) => {
|
||||
// if path.is_ident("skip_nones") {
|
||||
// // println!("shall skip nones");
|
||||
// attrs.skip_nones = true;
|
||||
// }
|
||||
// },
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_attrs(attrs: &Vec<syn::Attribute>) -> Result<StructAttrs> {
|
||||
let mut struct_attrs: StructAttrs = Default::default();
|
||||
|
||||
for attr in attrs {
|
||||
if attr.path.is_ident("serde_indexed") {
|
||||
// println!("parsing serde_indexed");
|
||||
parse_meta(&mut struct_attrs, &attr.parse_meta()?)?;
|
||||
}
|
||||
if attr.path.is_ident("serde") {
|
||||
// println!("parsing serde");
|
||||
parse_meta(&mut struct_attrs, &attr.parse_meta()?)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(struct_attrs)
|
||||
}
|
||||
|
||||
impl Parse for Input {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let call_site = Span::call_site();
|
||||
@@ -28,6 +83,8 @@ impl Parse for Input {
|
||||
}
|
||||
};
|
||||
|
||||
let attrs: StructAttrs = parse_attrs(&derive_input.attrs)?;
|
||||
|
||||
let syn_fields: syn::FieldsNamed = match data.fields {
|
||||
Fields::Named(named_fields) => named_fields,
|
||||
_ => {
|
||||
@@ -41,6 +98,7 @@ impl Parse for Input {
|
||||
|
||||
Ok(Input {
|
||||
ident: derive_input.ident,
|
||||
attrs,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
@@ -70,6 +128,37 @@ fn fields_from_ast<'a>(
|
||||
}
|
||||
},
|
||||
index: i,
|
||||
// TODO: make this... more concise? handle errors? the thing with the spans?
|
||||
skip_serializing_if: {
|
||||
let mut skip_serializing_if = None;
|
||||
for attr in &field.attrs {
|
||||
if attr.path.is_ident("serde") {
|
||||
if let Ok(syn::Meta::List(value)) = attr.parse_meta() {
|
||||
for meta in &value.nested {
|
||||
match meta {
|
||||
syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) => {
|
||||
if name_value.path.is_ident("skip_serializing_if") {
|
||||
// println!("so close!");
|
||||
if let syn::Lit::Str(litstr) = &name_value.lit {
|
||||
let tokens =
|
||||
syn::parse_str(&litstr.value()).unwrap();
|
||||
// println!("found something: {:?}", &litstr.value());
|
||||
skip_serializing_if =
|
||||
Some(syn::parse2(tokens).unwrap());
|
||||
}
|
||||
} else {
|
||||
// safety net, remove?
|
||||
panic!("unknown field attribute");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
skip_serializing_if
|
||||
},
|
||||
ty: field.ty.clone(),
|
||||
original: field.clone(),
|
||||
})
|
||||
|
||||
+70
-15
@@ -6,11 +6,14 @@ mod some_keys {
|
||||
use heapless::consts;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, SerializeIndexed, DeserializeIndexed)]
|
||||
// #[serde_indexed(offset = 1)]
|
||||
#[serde_indexed(offset = 1)]
|
||||
// #[serde_indexed(skip_nones)]
|
||||
pub struct SomeKeys {
|
||||
pub number: i32,
|
||||
pub bytes: [u8; 7],
|
||||
pub string: heapless::String<consts::U10>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub option: Option<u8>,
|
||||
pub vector: heapless::Vec<u8, consts::U16>,
|
||||
}
|
||||
|
||||
@@ -25,28 +28,62 @@ mod some_keys {
|
||||
number: -7,
|
||||
bytes: [37u8; 7],
|
||||
string,
|
||||
option: None,
|
||||
vector,
|
||||
}
|
||||
}
|
||||
|
||||
// in Python: cbor.dumps({1: -7, 2: [37]*7, 3: "so serde", 4: [42]*1})
|
||||
fn another_example() -> SomeKeys {
|
||||
let mut string = heapless::String::new();
|
||||
string.push_str("so serde").unwrap();
|
||||
|
||||
let mut vector = heapless::Vec::<u8, consts::U16>::new();
|
||||
vector.push(42).unwrap();
|
||||
|
||||
SomeKeys {
|
||||
number: -7,
|
||||
bytes: [37u8; 7],
|
||||
string,
|
||||
option: Some(0xff),
|
||||
vector,
|
||||
}
|
||||
}
|
||||
|
||||
// in Python: cbor.dumps({1: -7, 2: [37]*7, 3: "so serde", 5: [42]*1})
|
||||
const SERIALIZED_AN_EXAMPLE: &'static [u8] =
|
||||
b"\xa4\x01&\x02\x87\x18%\x18%\x18%\x18%\x18%\x18%\x18%\x03hso serde\x04\x81\x18*";
|
||||
b"\xa4\x01&\x02\x87\x18%\x18%\x18%\x18%\x18%\x18%\x18%\x03hso serde\x05\x81\x18*";
|
||||
|
||||
const SERIALIZED_ANOTHER_EXAMPLE: &'static [u8] =
|
||||
b"\xa5\x01&\x02\x87\x18%\x18%\x18%\x18%\x18%\x18%\x18%\x03hso serde\x04\x18\xff\x05\x81\x18*";
|
||||
|
||||
fn cbor_serialize<T: serde::Serialize>(
|
||||
object: &T,
|
||||
buffer: &mut [u8],
|
||||
) -> Result<usize, serde_cbor::Error> {
|
||||
let writer = serde_cbor::ser::SliceWrite::new(buffer);
|
||||
let mut ser = serde_cbor::Serializer::new(writer);
|
||||
|
||||
object.serialize(&mut ser)?;
|
||||
|
||||
let writer = ser.into_inner();
|
||||
let size = writer.bytes_written();
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn cbor_deserialize<'de, T: serde::Deserialize<'de>>(
|
||||
buffer: &'de mut [u8],
|
||||
) -> Result<T, serde_cbor::Error> {
|
||||
let mut deserializer = serde_cbor::de::Deserializer::from_mut_slice(buffer);
|
||||
serde::de::Deserialize::deserialize(&mut deserializer)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let example = an_example();
|
||||
|
||||
let mut buffer = [0u8; 1024];
|
||||
let writer = serde_cbor::ser::SliceWrite::new(&mut buffer);
|
||||
let mut ser = serde_cbor::Serializer::new(writer);
|
||||
|
||||
use serde::ser::Serialize;
|
||||
|
||||
example.serialize(&mut ser).unwrap();
|
||||
|
||||
let writer = ser.into_inner();
|
||||
let size = writer.bytes_written();
|
||||
let size = cbor_serialize(&example, &mut buffer).unwrap();
|
||||
|
||||
assert_eq!(&buffer[..size], SERIALIZED_AN_EXAMPLE);
|
||||
}
|
||||
@@ -56,10 +93,28 @@ mod some_keys {
|
||||
let mut buffer = [0u8; 1024];
|
||||
buffer[..SERIALIZED_AN_EXAMPLE.len()].copy_from_slice(SERIALIZED_AN_EXAMPLE);
|
||||
|
||||
let mut deserializer = serde_cbor::de::Deserializer::from_mut_slice(&mut buffer);
|
||||
let maybe_example: SomeKeys =
|
||||
serde::de::Deserialize::deserialize(&mut deserializer).unwrap();
|
||||
let maybe_example: SomeKeys = cbor_deserialize(&mut buffer).unwrap();
|
||||
|
||||
assert_eq!(maybe_example, an_example());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_serialize() {
|
||||
let example = another_example();
|
||||
|
||||
let mut buffer = [0u8; 1024];
|
||||
let size = cbor_serialize(&example, &mut buffer).unwrap();
|
||||
|
||||
assert_eq!(&buffer[..size], SERIALIZED_ANOTHER_EXAMPLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_deserialize() {
|
||||
let mut buffer = [0u8; 1024];
|
||||
buffer[..SERIALIZED_ANOTHER_EXAMPLE.len()].copy_from_slice(SERIALIZED_ANOTHER_EXAMPLE);
|
||||
|
||||
let maybe_example: SomeKeys = cbor_deserialize(&mut buffer).unwrap();
|
||||
|
||||
assert_eq!(maybe_example, another_example());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user