fixed_size/
lib.rs

1//! struct attribute to set fixed sizes for certain fields which are normally dynamic
2//! 
3//! This is useful when generating structs from protobufs using prost and
4//! also using those structs with a serde format that requires fixed length strings
5//! 
6//! # Example
7//! ```protoc
8//! syntax = "proto3";
9//! message Foo
10//! {
11//!  string my_string = 1;
12//! }
13//! ````
14//! Prost will create use [`String`] for the my_string field. If you have a binary format requiring
15//! exactly 4 characters in a string this will be difficult to handle in a generic manner. If you add
16//! the `#[fixed(my_string=4)]` attribute then you'll end up with a `ArrayString::<4>` instead.
17//! 
18//! By default, ArrayString will be used but this can be overridden with `#[fixed(typ=MyString, thestring=4)]`
19//! The typical use is
20//! ```rust
21//! use arrayvec::ArrayString;
22//! 
23//! struct MyString<const CAP: usize>(ArrayString<CAP>);
24//! 
25//! impl<const CAP: usize> AsRef<ArrayString<CAP>> for MyString<CAP> {
26//!    fn as_ref(&self) -> &ArrayString<CAP> {
27//!        &self.0
28//!    }
29//!}
30//! impl<const CAP: usize> serde::Serialize for MyString<CAP> {
31//!    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
32//!        where S: serde::Serializer
33//!    {
34//!        // specialized serialize to override ArrayString's conversion to &str
35//!        todo!()
36//!    }
37//! }
38//! // More impls, probably AsMut, etc.
39//! ```
40//! 
41//! ```rust
42//! use arrayvec::ArrayString;
43//! use fixed_size::fixed;
44//! 
45//! #[fixed(my_string=4)]
46//! #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
47//! struct Foo {
48//!   my_string: String,
49//! }
50//! 
51//! let foo = Foo { my_string: ArrayString::<4>::from("abcd").unwrap() };
52//! // bincode actually supports var length strings but it's just used as an example and test
53//! let encoded = bincode::serialize(&foo).unwrap();
54//! let decoded: Foo = bincode::deserialize(&encoded[..]).unwrap();
55//! assert_eq!(foo, decoded);
56//! ```
57//! 
58//! Adding fewer than 4 characters to my_string will 0 pad the value. Adding more than
59//! 4 characters will result in an error.
60
61extern crate proc_macro;
62
63use proc_macro::TokenStream;
64use proc_macro2::Span;
65use quote::quote;
66use std::collections::HashMap;
67use syn::{parse::{Parse, ParseStream, Result}, Token, punctuated::Punctuated,
68                  fold::Fold, Expr, ExprAssign, Ident, LitInt, Lit, parse_macro_input,
69                  ItemStruct, Type, Field, parse_quote};
70
71type MapType = HashMap<Ident, LitInt>;
72struct Args {
73    size_map: MapType,
74    typ: Ident,
75}
76
77const ERRMSG: &str = "Must specify an Ident=Int or typ=Structname";
78
79impl Parse for Args {
80    fn parse(input: ParseStream) -> Result<Self> {
81        let vars = Punctuated::<ExprAssign, Token![,]>::parse_terminated(input)?;
82        let mut size_map = MapType::new();
83        let mut typ = Ident::new("ArrayString", Span::mixed_site());
84        for var in vars.into_iter() {
85            match (&*var.left, &*var.right) {
86                (Expr::Path(p), Expr::Lit(v)) => {
87                    let key = p.path.get_ident().unwrap();
88                    if let Lit::Int(num) = &v.lit {
89                        size_map.insert(key.clone(), num.clone());
90                    } else {
91                        return Err(input.error(ERRMSG));
92                    }
93                },
94                (Expr::Path(p), Expr::Path(v)) => {
95                    let key = p.path.get_ident().unwrap();
96                    if key.to_string() != "typ" {
97                        return Err(input.error(ERRMSG));
98                    }
99                    if let Some(val) = v.path.get_ident() {
100                        typ = val.clone();
101                    } else {
102                        return Err(input.error(ERRMSG));
103                    }
104                }
105                (_, _) => {
106                    return Err(input.error(ERRMSG));
107                }
108            }
109        }
110
111        Ok(Args { size_map, typ })
112    }
113}
114
115impl Fold for Args {
116    fn fold_field(&mut self, input: Field) -> syn::Field {
117        if let Some(key) = &input.ident {
118            let typ = &self.typ;
119            if let Some(num) = self.size_map.get(key) {
120                if let Type::Path(p) = &input.ty {
121                    if p.path.is_ident("String") {
122                        return Field {
123                            attrs: input.attrs,
124                            vis: input.vis,
125                            mutability: input.mutability,
126                            ident: input.ident,
127                            colon_token: input.colon_token,
128                            ty: parse_quote!{#typ::<#num>},
129                        };
130                    }
131                }
132            }
133        }
134        input
135    }
136}
137
138/// Replace one or more variable length fields with a fixed length equivalent
139/// 
140/// Pass in a list of `field_name=length` arguments. Optionally
141/// pass `typ=MyType` to use a different type for the replacement. See
142/// the crate documentation for moreinformation.
143#[proc_macro_attribute]
144pub fn fixed(args: TokenStream, input: TokenStream) -> TokenStream {
145    let mut args = parse_macro_input!(args as Args);
146    let input = parse_macro_input!(input as ItemStruct);
147    let output = args.fold_item_struct(input);
148    proc_macro::TokenStream::from(quote!(#output))
149}