#![recursion_limit = "128"]
use fs2::FileExt;
use proc_macro::TokenStream;
use quote::quote;
use std::collections::BTreeMap;
use std::fs::File;
use syn::parse_macro_input;
static DEFAULT_TYPES_FILE_NAME: &str = "types.toml";
static DEFAULT_ID_TYPE: &str = "u64";
static DEFAULT_ID_START: &str = "0";
type PairsMap = BTreeMap<String, u64>;
#[proc_macro_derive(
UniqueTypeId,
attributes(UniqueTypeIdFile, UniqueTypeIdType, UniqueTypeIdStart)
)]
pub fn unique_type_id(input: TokenStream) -> TokenStream {
implement_type_id(input, unique_implementor)
}
fn read_file_into_string(file_name: &str) -> String {
use std::io::Read;
let mut f = match File::open(file_name) {
Ok(f) => f,
Err(_) => return String::default(),
};
f.lock_exclusive().expect("Unable to lock the file");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("Unable to read the file");
fs2::FileExt::unlock(&f).expect("Unable to unlock the file");
contents
}
fn file_string_to_tree(file_contents: String) -> PairsMap {
let mut map = PairsMap::new();
file_contents
.split(&['\n', '\r'][..])
.filter_map(pair_from_line)
.for_each(|p| {
map.insert(p.0, p.1);
});
map
}
fn pair_from_line(line: &str) -> Option<(String, u64)> {
let mut pair = line.split('=');
let key = pair.next()?.to_owned();
let value = pair.next()?.parse::<u64>().ok()?;
Some((key, value))
}
fn append_pair_to_file(file_name: &str, record: &str, value: u64) {
use std::fs::OpenOptions;
use std::io::Write;
let mut f = OpenOptions::new()
.read(true)
.append(true)
.create(true)
.open(file_name)
.expect("Unable to create file");
f.lock_exclusive().expect("Unable to lock the file");
let contents = format!("{record}={value}\n");
f.write_all(contents.as_bytes())
.expect("Unable to write to the file");
fs2::FileExt::unlock(&f).expect("Unable to unlock the file");
}
fn gen_id(file_name: &str, record: &str, start: u64) -> u64 {
let pairs_map = file_string_to_tree(read_file_into_string(file_name));
match pairs_map.get(record) {
Some(record_id) => record_id.to_owned(),
None => {
let mut new_id = start;
loop {
if !pairs_map.values().any(|id| &new_id == id) {
break;
}
new_id += 1;
}
append_pair_to_file(file_name, record, new_id);
new_id
}
}
}
fn implement_type_id(
input: TokenStream,
implementor: fn(&syn::DeriveInput) -> TokenStream,
) -> TokenStream {
let ast = parse_macro_input!(input as syn::DeriveInput);
implementor(&ast)
}
fn parse_attribute(attrs: &[syn::Attribute], name: &str, default: &str) -> String {
use quote::ToTokens;
use syn::spanned::Spanned;
attrs
.iter()
.find(|a| a.path().is_ident(name))
.map(|a| {
a.meta
.to_token_stream()
.into_iter()
.nth(2)
.ok_or_else(|| {
syn::Error::new(
a.span(),
format!(r#"The attribute should be in the format: `{name} = "{default}"`"#),
)
})
.unwrap()
.to_string()
.trim_matches('\"')
.to_owned()
})
.unwrap_or_else(|| default.to_string())
}
fn unique_implementor(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let types_file_name = parse_attribute(&ast.attrs, "UniqueTypeIdFile", DEFAULT_TYPES_FILE_NAME);
let id_type_name = parse_attribute(&ast.attrs, "UniqueTypeIdType", DEFAULT_ID_TYPE);
let gen_start: u64 = parse_attribute(&ast.attrs, "UniqueTypeIdStart", DEFAULT_ID_START)
.parse()
.unwrap();
let id_type = syn::parse_str::<syn::Type>(&id_type_name).unwrap();
let id = gen_id(&types_file_name, &ast.ident.to_string(), gen_start);
TokenStream::from(quote! {
impl #impl_generics unique_type_id::UniqueTypeId<#id_type> for #name #ty_generics #where_clause {
const TYPE_ID: unique_type_id::TypeId<#id_type> = unique_type_id::TypeId(#id as #id_type);
fn id() -> unique_type_id::TypeId<#id_type> {
<Self as unique_type_id::UniqueTypeId<#id_type>>::TYPE_ID
}
}
impl #impl_generics #name #ty_generics #where_clause {
const fn unique_type_id() -> unique_type_id::TypeId<#id_type> {
<Self as unique_type_id::UniqueTypeId<#id_type>>::TYPE_ID
}
}
})
}