diff --git a/Cargo.toml b/Cargo.toml index 8f84563..2f4f5c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,11 @@ [package] name = "serde-indexed" -version = "0.0.0-unreleased" +version = "0.0.1" authors = ["Nicolas Stalder "] 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" diff --git a/README.md b/README.md index 2030263..bbe1fea 100644 --- a/README.md +++ b/README.md @@ -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, + 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 diff --git a/src/lib.rs b/src/lib.rs index 1054f67..43af9df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, + 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 { +fn serialize_fields(fields: &[parse::Field], offset: usize) -> Vec { 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 { + 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 { .collect() } -fn unwrap_fields(fields: &[parse::Field]) -> Vec { +fn unwrap_expected_fields(fields: &[parse::Field]) -> Vec { 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 Vec { .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(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),* }) } diff --git a/src/parse.rs b/src/parse.rs index 6b7c414..dcaca5b 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -4,18 +4,73 @@ use syn::{Data, DeriveInput, Fields, Ident, Token}; pub struct Input { pub ident: Ident, + pub attrs: StructAttrs, pub fields: Vec, } +#[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, // 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) -> Result { + 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 { 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(), }) diff --git a/tests/basics.rs b/tests/basics.rs index 4411a3d..0e51224 100644 --- a/tests/basics.rs +++ b/tests/basics.rs @@ -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, + #[serde(skip_serializing_if = "Option::is_none")] + pub option: Option, pub vector: heapless::Vec, } @@ -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::::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( + object: &T, + buffer: &mut [u8], + ) -> Result { + 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 { + 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()); + } }