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.
10///   The version string must be a valid semantic version.
11///
12/// # Example
13///
14/// ```ignore
15/// use version_migrate::Versioned;
16///
17/// #[derive(Versioned)]
18/// #[versioned(version = "1.0.0")]
19/// pub struct Task_V1_0_0 {
20///     pub id: String,
21///     pub title: String,
22/// }
23/// ```
24#[proc_macro_derive(Versioned, attributes(versioned))]
25pub fn derive_versioned(input: TokenStream) -> TokenStream {
26    let input = parse_macro_input!(input as DeriveInput);
27
28    // Extract the version attribute
29    let version = extract_version(&input);
30
31    let name = &input.ident;
32    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
33
34    let expanded = quote! {
35        impl #impl_generics version_migrate::Versioned for #name #ty_generics #where_clause {
36            const VERSION: &'static str = #version;
37        }
38    };
39
40    TokenStream::from(expanded)
41}
42
43fn extract_version(input: &DeriveInput) -> String {
44    for attr in &input.attrs {
45        if attr.path().is_ident("versioned") {
46            if let Meta::List(meta_list) = &attr.meta {
47                let nested = meta_list.tokens.to_string();
48                // Parse version = "x.y.z"
49                if let Some(version_str) = parse_version_attr(&nested) {
50                    // Validate semver at compile time
51                    if let Err(e) = semver::Version::parse(&version_str) {
52                        panic!("Invalid semantic version '{}': {}", version_str, e);
53                    }
54                    return version_str;
55                }
56            }
57        }
58    }
59    panic!("Missing #[versioned(version = \"x.y.z\")] attribute");
60}
61
62fn parse_version_attr(tokens: &str) -> Option<String> {
63    // Simple parser for: version = "x.y.z"
64    let tokens = tokens.trim();
65    if let Some(rest) = tokens.strip_prefix("version") {
66        let rest = rest.trim();
67        if let Some(rest) = rest.strip_prefix('=') {
68            let rest = rest.trim();
69            if rest.starts_with('"') && rest.ends_with('"') {
70                return Some(rest[1..rest.len() - 1].to_string());
71            }
72        }
73    }
74    None
75}