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}