version_migrate_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{parse_macro_input, DeriveInput, Meta};
4
5/// Derives the `Versioned` trait for a struct.
6///
7/// # Attributes
8///
9/// - `#[versioned(version = "x.y.z")]`: Specifies the semantic version (required).
10///   The version string must be a valid semantic version.
11/// - `#[versioned(version_key = "...")]`: Customizes the version field key (optional, default: "version").
12/// - `#[versioned(data_key = "...")]`: Customizes the data field key (optional, default: "data").
13///
14/// # Examples
15///
16/// Basic usage:
17/// ```ignore
18/// use version_migrate::Versioned;
19///
20/// #[derive(Versioned)]
21/// #[versioned(version = "1.0.0")]
22/// pub struct Task_V1_0_0 {
23///     pub id: String,
24///     pub title: String,
25/// }
26/// ```
27///
28/// Custom keys:
29/// ```ignore
30/// #[derive(Versioned)]
31/// #[versioned(
32///     version = "1.0.0",
33///     version_key = "schema_version",
34///     data_key = "payload"
35/// )]
36/// pub struct Task { ... }
37/// // Serializes to: {"schema_version":"1.0.0","payload":{...}}
38/// ```
39#[proc_macro_derive(Versioned, attributes(versioned))]
40pub fn derive_versioned(input: TokenStream) -> TokenStream {
41    let input = parse_macro_input!(input as DeriveInput);
42
43    // Extract attributes
44    let attrs = extract_attributes(&input);
45
46    let name = &input.ident;
47    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
48
49    let version = &attrs.version;
50    let version_key = &attrs.version_key;
51    let data_key = &attrs.data_key;
52
53    let expanded = quote! {
54        impl #impl_generics version_migrate::Versioned for #name #ty_generics #where_clause {
55            const VERSION: &'static str = #version;
56            const VERSION_KEY: &'static str = #version_key;
57            const DATA_KEY: &'static str = #data_key;
58        }
59    };
60
61    TokenStream::from(expanded)
62}
63
64struct VersionedAttributes {
65    version: String,
66    version_key: String,
67    data_key: String,
68}
69
70fn extract_attributes(input: &DeriveInput) -> VersionedAttributes {
71    let mut version = None;
72    let mut version_key = String::from("version");
73    let mut data_key = String::from("data");
74
75    for attr in &input.attrs {
76        if attr.path().is_ident("versioned") {
77            if let Meta::List(meta_list) = &attr.meta {
78                let tokens = meta_list.tokens.to_string();
79                parse_versioned_attrs(&tokens, &mut version, &mut version_key, &mut data_key);
80            }
81        }
82    }
83
84    let version = version.unwrap_or_else(|| {
85        panic!("Missing #[versioned(version = \"x.y.z\")] attribute");
86    });
87
88    // Validate semver at compile time
89    if let Err(e) = semver::Version::parse(&version) {
90        panic!("Invalid semantic version '{}': {}", version, e);
91    }
92
93    VersionedAttributes {
94        version,
95        version_key,
96        data_key,
97    }
98}
99
100fn parse_versioned_attrs(
101    tokens: &str,
102    version: &mut Option<String>,
103    version_key: &mut String,
104    data_key: &mut String,
105) {
106    // Parse comma-separated key = "value" pairs
107    for part in tokens.split(',') {
108        let part = part.trim();
109
110        if let Some(val) = parse_attr_value(part, "version") {
111            *version = Some(val);
112        } else if let Some(val) = parse_attr_value(part, "version_key") {
113            *version_key = val;
114        } else if let Some(val) = parse_attr_value(part, "data_key") {
115            *data_key = val;
116        }
117    }
118}
119
120fn parse_attr_value(token: &str, key: &str) -> Option<String> {
121    let token = token.trim();
122    if let Some(rest) = token.strip_prefix(key) {
123        let rest = rest.trim();
124        if let Some(rest) = rest.strip_prefix('=') {
125            let rest = rest.trim();
126            if rest.starts_with('"') && rest.ends_with('"') {
127                return Some(rest[1..rest.len() - 1].to_string());
128            }
129        }
130    }
131    None
132}