Implement offsets and skip_serialization_if

This commit is contained in:
Nicolas Stalder
2020-01-12 02:53:02 +01:00
parent 261d1ed124
commit 3a1bb45313
5 changed files with 270 additions and 46 deletions
+4 -2
View File
@@ -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"
+23 -8
View File
@@ -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
View File
@@ -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),* })
}
+89
View File
@@ -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
View File
@@ -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());
}
}