unique_uuid_derive/lib.rs
1//! This crate provides procedural macros for generating unique UUIDs associated with tags and types.
2//! It offers two main functionalities:
3//! - `unique_tag`: A procedural macro that generates a unique UUID for a given string tag
4//! - `UniqueTypeTag`: A derive macro that automatically generates a unique UUID for a type
5//!
6//! The generated UUIDs are persisted in a TOML file (`types.toml` by default) to ensure
7//! consistency across multiple compilations and crate boundaries.
8//!
9//! # Features
10//! - Persistent UUID generation and storage
11//! - Consistent UUID mapping for types and tags
12//! - Thread-safe file handling
13//! - TOML-based storage format
14//!
15//! # Examples
16//! ```rust
17//! use unique_uuid_derive::{unique_tag, UniqueTypeTag};
18//!
19//! // Using unique_tag macro
20//! let my_tag = unique_tag!("my_custom_tag");
21//!
22//! // Using UniqueTypeTag derive macro
23//! #[derive(UniqueTypeTag)]
24//! struct MyStruct;
25//! ```
26//!
27//! # File Structure
28//! The crate maintains a TOML file with the following structure:
29//! ```toml
30//! [unique_tags]
31//! "tag_name" = "uuid"
32//!
33//! [unique_type_tags]
34//! "type_name" = "uuid"
35//! ```
36//!
37//! # Implementation Details
38//! - UUIDs are generated using UUID v4 (random)
39//! - File operations are performed with proper error handling
40//! - The system supports both string tags and type tags
41//!
42//! # Safety
43//! This crate performs file I/O operations during compilation, which may fail if:
44//! - The process lacks file system permissions
45//! - The TOML file becomes corrupted
46//! - Concurrent compilation attempts cause file access conflicts
47use std::{
48 collections::HashMap,
49 fs::OpenOptions,
50 io::{Read, Seek, Write},
51};
52
53use proc_macro::TokenStream;
54use serde::{Deserialize, Serialize};
55use syn::spanned::Spanned;
56
57static DEFAULT_TYPES_FILE_NAME: &str = "types.toml";
58
59/// A procedural macro that generates a unique UUID for a given string tag.
60/// The generated UUID is persisted in a TOML file to ensure consistency across
61/// multiple compilations and crate boundaries.
62///
63/// # Warnings
64/// Tags are consistent within the same crate, but **MAY** differ across crates. They
65/// will match only if the `types.toml` file is shared between the crates (typically for
66/// a workspace). As such special care should be taken when working with macros that are
67/// public and used across crates.
68///
69/// # Arguments
70/// * Input must be a string literal that serves as the tag identifier
71///
72/// # Returns
73/// Returns a [`unique_uuid::UniqueTag`] containing a UUID that is consistently
74/// mapped to the input tag.
75///
76/// # Example
77/// ```rust
78/// use unique_uuid_derive::unique_tag;
79///
80/// let my_uuid = unique_tag!("my_custom_tag");
81/// ```
82///
83/// # Panics
84/// This macro will panic if:
85/// * The TOML file cannot be opened or created
86/// * There are permission issues with the file system
87/// * The TOML file is corrupted or invalid
88///
89/// # File Storage
90/// The UUID-tag mapping is stored in the `types.toml` file under the `[unique_tags]` section.
91#[proc_macro]
92pub fn unique_tag(input: TokenStream) -> TokenStream {
93 let string = syn::parse_macro_input!(input as syn::LitStr);
94 let uuid = get_uuid_from_tag(&string.value(), UType::UniqueTags).to_string();
95 let uuid = syn::LitStr::new(&uuid, string.span());
96
97 TokenStream::from(quote::quote! {
98 unique_uuid::UniqueTag(unique_uuid::uuid::uuid!(#uuid))
99 })
100}
101
102/// A derive macro that automatically generates a unique UUID for a type.
103/// The generated UUID is associated with the type name and persisted in a TOML file
104/// to ensure consistency across multiple compilations and crate boundaries.
105///
106/// This macro implements the [`unique_uuid::UniqueTypeTag`] trait for the decorated type,
107/// providing a constant `TYPE_TAG` that contains a unique UUID.
108///
109/// # Example
110/// ```rust
111/// use unique_uuid_derive::UniqueTypeTag;
112///
113/// #[derive(UniqueTypeTag)]
114/// struct MyStruct;
115///
116/// // The UUID can be accessed via the trait implementation
117/// let type_uuid = MyStruct::TYPE_TAG;
118/// ```
119///
120/// # Implementation Details
121/// * Generates a UUID v4 for the type if one doesn't exist
122/// * Stores the UUID in `types.toml` under the `[unique_type_tags]` section
123/// * Uses the type's name as the key for UUID mapping
124///
125/// # Panics
126/// This macro will panic if:
127/// * The TOML file cannot be opened or created
128/// * There are permission issues with the file system
129/// * The TOML file is corrupted or invalid
130///
131#[proc_macro_derive(UniqueTypeTag)]
132pub fn unique_type_tag(input: TokenStream) -> TokenStream {
133 let input = syn::parse_macro_input!(input as syn::DeriveInput);
134 let tag = format!("{}::{}", "", input.ident);
135
136 let uuid = get_uuid_from_tag(&tag, UType::UniqueTypeTags).to_string();
137 let uuid = syn::LitStr::new(&uuid, input.span());
138
139 let input_ident = input.ident;
140
141 TokenStream::from(quote::quote! {
142 impl unique_uuid::UniqueTypeTag for #input_ident {
143 const TYPE_TAG: unique_uuid::UniqueTag = unique_uuid::UniqueTag(unique_uuid::uuid::uuid!(#uuid));
144 }
145 })
146}
147
148enum UType {
149 UniqueTags,
150 UniqueTypeTags,
151}
152
153#[derive(Default, Serialize, Deserialize)]
154#[serde(default)]
155struct FileStructure {
156 #[serde(default)]
157 unique_tags: HashMap<String, uuid::Uuid>,
158
159 #[serde(default)]
160 unique_type_tags: HashMap<String, uuid::Uuid>,
161}
162
163fn get_uuid_from_tag(tag: &str, r#type: UType) -> uuid::Uuid {
164 let file_path = DEFAULT_TYPES_FILE_NAME;
165 let mut file = match OpenOptions::new()
166 .read(true)
167 .write(true)
168 .create(true)
169 .open(file_path)
170 {
171 Ok(file) => file,
172 Err(err) => {
173 panic!("Error opening file: {}", err);
174 }
175 };
176
177 // Read the TOML file
178 let mut contents = String::new();
179 file.read_to_string(&mut contents).unwrap();
180
181 // Deserialize the TOML file
182 let mut file_structure: FileStructure = toml::from_str(&contents).unwrap();
183
184 let target = match r#type {
185 UType::UniqueTags => &mut file_structure.unique_tags,
186 UType::UniqueTypeTags => &mut file_structure.unique_type_tags,
187 };
188 if let Some(uuid) = target.get(tag) {
189 uuid.clone()
190 } else {
191 let uuid = uuid::Uuid::new_v4();
192 target.insert(tag.to_string(), uuid);
193 let toml = toml::to_string(&file_structure).unwrap();
194 file.seek(std::io::SeekFrom::Start(0)).unwrap();
195 file.write_all(toml.as_bytes()).unwrap();
196 uuid
197 }
198}