unique_type_id_derive/
lib.rs

1#![recursion_limit = "128"]
2
3use fs2::FileExt;
4use proc_macro::TokenStream;
5use quote::quote;
6use std::collections::BTreeMap;
7use std::fs::File;
8use syn::parse_macro_input;
9
10static DEFAULT_TYPES_FILE_NAME: &str = "types.toml";
11static DEFAULT_ID_TYPE: &str = "u64";
12static DEFAULT_ID_START: &str = "0";
13
14type PairsMap = BTreeMap<String, u64>;
15
16/// `UniqueTypeId`
17///
18/// ## What does it do?
19///
20/// It simply implements a trait for the type where is only one method - `id() -> TypeId` which
21/// returns a unique positive number. For id generation, the procedural macro reads the file called
22/// "types.toml" and searches for the type name there. You may also specify another file name if
23/// you want by using `UniqueTypeIdFile` attribute. Speaking more detailed:
24///
25/// 1. The procedural macro reads the attributes on a type.
26/// 2. If there are no attributes, it uses `types.toml` file name as types file name, otherwise
27/// uses specified one.
28/// 3. For each type the macro is used it tries to find the type name in the types file. If it can
29/// find it, it returns it's id, otherwise it returns the available id. Reading tests helps in
30/// understanding this.
31///
32/// ## Usage
33///
34/// 1. Add `unique-type-id` as dependency in your `Cargo.toml`:
35///
36/// ```toml
37/// [dependencies]
38/// unique-type-id = "1"
39/// ```
40///
41/// 2. Create a struct or enum and use the trait:
42///
43/// ```rust,ignore
44/// #[test]
45/// fn unique_simple() {
46///     use unique_type_id::UniqueTypeId;
47///     #[derive(UniqueTypeId)]
48///     struct Test1;
49///     #[derive(UniqueTypeId)]
50///     struct Test2;
51///
52///     assert_eq!(Test1::id().0, 1u64);
53///     assert_eq!(Test2::id().0, 2u64);
54/// }
55/// ```
56///
57/// This will generate a types file if it has not been created yet and put there ids, starting with
58/// `0`, for each type which was not found there. This is how it looks when you have predefined set
59/// of ids for your types:
60///
61/// ```rust,ignore
62/// #[test]
63/// fn unique_different_file() {
64///     use unique_type_id::UniqueTypeId;
65///     #[derive(UniqueTypeId)]
66///     #[UniqueTypeIdFile = "types2.toml"]
67///     struct Test1;
68///     #[derive(UniqueTypeId)]
69///     #[UniqueTypeIdFile = "types2.toml"]
70///     struct Test2;
71///
72///     assert_eq!(Test1::id().0, 115u64);
73///     assert_eq!(Test2::id().0, 232u64);
74/// }
75/// ```
76///
77/// Here we set up ids for our types manually by creating the `types2.toml` file.
78///
79/// ## Options
80///
81/// - `UniqueTypeIdFile` - allows to specify the file name to write/read the IDs from.
82/// - `UniqueTypeIdType` - allows to change the ID number type from `u64` (the default) to the
83/// user-preferred one.
84/// - `UniqueTypeIdStart` - allows to set the starting ID number for the type. Can be used if the
85/// type layout file is very well-known and guaranteed to avoid collisions.
86///
87/// ### UniqueTypeIdFile
88///
89/// ```rust,ignore
90/// #[derive(UniqueTypeId)]
91/// #[UniqueTypeIdFile = "types2.toml"]
92/// struct Test1;
93/// ```
94///
95/// ### UniqueTypeIdType
96///
97/// ```rust,ignore
98/// #[derive(UniqueTypeId)]
99/// #[UniqueTypeIdType = "i16"]
100/// struct Test;
101/// ```
102///
103/// ### UniqueTypeIdStart
104///
105/// ```rust,ignore
106/// #[derive(UniqueTypeId)]
107/// #[UniqueTypeIdStart = "23"]
108/// struct Test;
109/// ```
110#[proc_macro_derive(
111    UniqueTypeId,
112    attributes(UniqueTypeIdFile, UniqueTypeIdType, UniqueTypeIdStart)
113)]
114pub fn unique_type_id(input: TokenStream) -> TokenStream {
115    implement_type_id(input, unique_implementor)
116}
117
118fn read_file_into_string(file_name: &str) -> String {
119    use std::io::Read;
120
121    let mut f = match File::open(file_name) {
122        Ok(f) => f,
123        Err(_) => return String::default(),
124    };
125    f.lock_exclusive().expect("Unable to lock the file");
126    let mut contents = String::new();
127    f.read_to_string(&mut contents)
128        .expect("Unable to read the file");
129    fs2::FileExt::unlock(&f).expect("Unable to unlock the file");
130    contents
131}
132
133fn file_string_to_tree(file_contents: String) -> PairsMap {
134    let mut map = PairsMap::new();
135    file_contents
136        .split(&['\n', '\r'][..])
137        .filter_map(pair_from_line)
138        .for_each(|p| {
139            map.insert(p.0, p.1);
140        });
141    map
142}
143
144fn pair_from_line(line: &str) -> Option<(String, u64)> {
145    let mut pair = line.split('=');
146    let key = pair.next()?.to_owned();
147    let value = pair.next()?.parse::<u64>().ok()?;
148    Some((key, value))
149}
150
151fn append_pair_to_file(file_name: &str, record: &str, value: u64) {
152    use std::fs::OpenOptions;
153    use std::io::Write;
154
155    let mut f = OpenOptions::new()
156        .read(true)
157        .append(true)
158        .create(true)
159        .open(file_name)
160        .expect("Unable to create file");
161    f.lock_exclusive().expect("Unable to lock the file");
162    let contents = format!("{record}={value}\n");
163    f.write_all(contents.as_bytes())
164        .expect("Unable to write to the file");
165    fs2::FileExt::unlock(&f).expect("Unable to unlock the file");
166}
167
168fn gen_id(file_name: &str, record: &str, start: u64) -> u64 {
169    let pairs_map = file_string_to_tree(read_file_into_string(file_name));
170    match pairs_map.get(record) {
171        Some(record_id) => record_id.to_owned(),
172        None => {
173            let mut new_id = start;
174
175            loop {
176                if !pairs_map.values().any(|id| &new_id == id) {
177                    break;
178                }
179                new_id += 1;
180            }
181
182            append_pair_to_file(file_name, record, new_id);
183            new_id
184        }
185    }
186}
187
188fn implement_type_id(
189    input: TokenStream,
190    implementor: fn(&syn::DeriveInput) -> TokenStream,
191) -> TokenStream {
192    let ast = parse_macro_input!(input as syn::DeriveInput);
193    implementor(&ast)
194}
195
196fn parse_attribute(attrs: &[syn::Attribute], name: &str, default: &str) -> String {
197    use quote::ToTokens;
198    use syn::spanned::Spanned;
199
200    attrs
201        .iter()
202        .find(|a| a.path().is_ident(name))
203        .map(|a| {
204            a.meta
205                .to_token_stream()
206                .into_iter()
207                // Taking the second part of tokens, after the `=` sign.
208                .nth(2)
209                .ok_or_else(|| {
210                    syn::Error::new(
211                        a.span(),
212                        format!(r#"The attribute should be in the format: `{name} = "{default}"`"#),
213                    )
214                })
215                .unwrap()
216                .to_string()
217                .trim_matches('\"')
218                .to_owned()
219        })
220        .unwrap_or_else(|| default.to_string())
221}
222
223fn unique_implementor(ast: &syn::DeriveInput) -> TokenStream {
224    let name = &ast.ident;
225    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
226    let types_file_name = parse_attribute(&ast.attrs, "UniqueTypeIdFile", DEFAULT_TYPES_FILE_NAME);
227    let id_type_name = parse_attribute(&ast.attrs, "UniqueTypeIdType", DEFAULT_ID_TYPE);
228    let gen_start: u64 = parse_attribute(&ast.attrs, "UniqueTypeIdStart", DEFAULT_ID_START)
229        .parse()
230        .unwrap();
231    let id_type = syn::parse_str::<syn::Type>(&id_type_name).unwrap();
232    let id = gen_id(&types_file_name, &ast.ident.to_string(), gen_start);
233
234    // TODO: Use TryFrom instead of `#id as #id_type` to avoid silently destructive casts
235    TokenStream::from(quote! {
236        impl #impl_generics unique_type_id::UniqueTypeId<#id_type> for #name #ty_generics #where_clause {
237            const TYPE_ID: unique_type_id::TypeId<#id_type> = unique_type_id::TypeId(#id as #id_type);
238
239            fn id() -> unique_type_id::TypeId<#id_type> {
240                <Self as unique_type_id::UniqueTypeId<#id_type>>::TYPE_ID
241            }
242        }
243
244        impl #impl_generics #name #ty_generics #where_clause {
245            const fn unique_type_id() -> unique_type_id::TypeId<#id_type> {
246                <Self as unique_type_id::UniqueTypeId<#id_type>>::TYPE_ID
247            }
248        }
249    })
250}