amethystate_macros/lib.rs
1use proc_macro::TokenStream;
2
3mod amethystate;
4mod migrate;
5//TODO: add as_root attribute
6
7/// Generates a persistent state wrapper for a struct.
8///
9/// This macro creates structures that manage persistence, reactive subscribers,
10/// and migrations. Depending on the selected `mode`, it generates either reactive
11/// `Field<T>` accessors or a flat persistent-only model.
12///
13/// # Struct Attributes (`#[amethystate(...)]`)
14///
15/// * `#[amethystate(prefix = "path", version = 1, mode = "reactive")]` - Defines a **Root** struct.
16/// * `prefix` (String): Sets the top-level namespace path in the store.
17/// Generates `pub fn new(store: &Arc<DefaultStore>) -> Result<Self>`.
18/// * `version` (optional u32): Schema version for migrations (defaults to 0).
19/// * `mode` (optional String): Controls the generated code paradigm. One of:
20/// * `"reactive"` (default): Generates fine-grained reactive `Field<T>` accessors.
21/// * `"persistent"`: Generates a flat struct with plain-type fields and synchronous `.save()` / `.save_lazy()` methods.
22/// * `"both"`: Generates both reactive accessors on `#name` and a separate `#name_Persistent` flat struct.
23/// * `#[amethystate]` - Defines a **Nested** struct.
24/// * Used as a component within other structures.
25/// * Generates `pub fn new(store: &Arc<DefaultStore>, namespace: &str) -> Result<Self>`.
26///
27/// # Field Attributes (`#[amestate(...)]`)
28///
29/// | Option | Type | Description |
30/// | :--- | :--- | :--- |
31/// | `default` | `Expr` | Initial value if not present in store. Required for leaf fields. |
32/// | `nested` | `bool` | Marks field as another `#[amethystate]` struct. |
33/// | `volatile` | `bool` | In-memory only. Never saved to or loaded from disk. |
34/// | `export_mut` | `bool` | Allows this field to be mutated via `lookup` from other structs. |
35/// | `key` | `String` | Overrides the storage key (defaults to field name). |
36/// | `lookup` | `String` | Links to a leaf field in a `parent` struct. Supports dot-notation. |
37/// | `lookup_node` | `String` | Links to a nested struct node in a `parent` struct. |
38/// | `parent` | `Type` | The source `amethystate` struct for `lookup` or `lookup_node`. |
39///
40/// # Examples
41///
42/// ### Reactive Mode (Default)
43/// ```rust,ignore
44/// #[amethystate(prefix = "settings")]
45/// pub struct AppSettings {
46/// #[amestate(default = "localhost".to_string())]
47/// pub host: String,
48///
49/// #[amestate(default = false, volatile)]
50/// pub debug_mode: bool,
51/// }
52///
53/// // Usage:
54/// // let settings = AppSettings::new(&store)?;
55/// // let _sub = settings.host().subscribe(|val| println!("Host: {val}"));
56/// // settings.host().set("10.0.0.1".to_string())?;
57/// ```
58///
59/// ### Persistent-only Mode
60/// ```rust,ignore
61/// #[amethystate(prefix = "network", mode = "persistent")]
62/// pub struct NetworkConfig {
63/// #[amestate(default = "localhost".to_string())]
64/// pub host: String,
65/// #[amestate(default = 8080)]
66/// pub port: u16,
67/// }
68///
69/// // Usage:
70/// // let mut cfg = NetworkConfig::load(&store)?;
71/// // cfg.host = "10.0.0.1".to_string(); // Direct field mutation (plain types)
72/// // cfg.save_lazy()?; // RAM-buffer write (debounced/background)
73/// // cfg.save()?; // Immediate synchronous flush to disk
74/// ```
75///
76/// ### Lookups and Permissions
77/// ```rust,ignore
78/// #[amethystate(prefix = "database")]
79/// pub struct DatabaseState {
80/// #[amestate(default = 10, export_mut)]
81/// pub pool_size: u32,
82/// }
83///
84/// #[amethystate(prefix = "ui")]
85/// pub struct Dashboard {
86/// // Links to DatabaseState.pool_size (read-only by default)
87/// #[amestate(lookup = "pool_size", parent = DatabaseState)]
88/// pub view_limit: u32,
89///
90/// // Links to DatabaseState.pool_size (writable)
91/// #[amestate(lookup = "pool_size", parent = DatabaseState, export_mut)]
92/// pub edit_limit: u32,
93/// }
94/// ```
95#[proc_macro_attribute]
96pub fn amethystate(args: TokenStream, input: TokenStream) -> TokenStream {
97 amethystate::amethystate_impl(args, input)
98}
99
100/// Transforms a function into a migration step between two state versions.
101///
102/// The macro derives source and target types from the function signature:
103/// - **from**: the type of the first argument
104/// - **to**: the inner type of `Result<T>` return type
105///
106/// The function name becomes the migration step description in the registry.
107///
108/// # Attributes
109///
110/// - `#[rename(old_field => new_field)]` — declares a field rename. Can be stacked.
111/// Generates a compile-time check that both fields exist on the respective types.
112///
113/// # Examples
114///
115/// Simple rename, no context:
116///
117/// ```rust,ignore
118/// mod v1 {
119/// #[amethystate(prefix = "app", version = 1)]
120/// pub struct Config {
121/// #[amestate(default = "localhost".to_string())]
122/// pub host: String,
123/// #[amestate(default = 8080)]
124/// pub port: u16,
125/// }
126/// }
127///
128/// #[amethystate(prefix = "app", version = 2)]
129/// pub struct Config {
130/// #[amestate(default = "localhost".to_string())]
131/// pub address: String,
132/// #[amestate(default = 8080)]
133/// pub port: u16,
134/// }
135///
136/// #[migrate]
137/// #[rename(host => address)]
138/// fn migrate_config_v1_to_v2(old: AmeData<v1::Config>) -> amethystate::Result<AmeData<Config>> {
139/// Ok(AmeData::<Config> { address: old.host, port: old.port })
140/// }
141/// ```
142///
143/// Manual key cleanup via `MigrationContext`:
144///
145/// ```rust,ignore
146/// #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, AmeType)]
147/// pub struct ProxyEndpoint {
148/// pub url: String,
149/// pub timeout_ms: u32,
150/// }
151///
152/// mod v1 {
153/// #[amethystate(prefix = "network", version = 1)]
154/// pub struct ProxyConfig {
155/// #[amestate(default = "default".into())]
156/// pub name: String,
157/// pub routes: ReactiveMap<String, String>,
158/// }
159/// }
160///
161/// #[amethystate(prefix = "network", version = 2)]
162/// pub struct ProxyConfig {
163/// #[amestate(default = "default".into())]
164/// pub name: String,
165/// pub endpoints: ReactiveMap<String, ProxyEndpoint>,
166/// }
167///
168/// #[migrate]
169/// fn migrate_proxy_config_v1_to_v2(
170/// old: AmeData<v1::ProxyConfig>,
171/// ctx: &mut amethystate::migration::MigrationContext,
172/// ) -> amethystate::Result<AmeData<ProxyConfig>> {
173/// for key in old.routes.keys() {
174/// ctx.delete(&format!("routes.{}", key))?;
175/// }
176/// let endpoints = old.routes.into_iter()
177/// .map(|(k, v)| (k, ProxyEndpoint { url: v, timeout_ms: 5000 }))
178/// .collect();
179/// Ok(AmeData::<ProxyConfig> { name: old.name, endpoints })
180/// }
181/// ```
182#[proc_macro_attribute]
183pub fn migrate(args: TokenStream, input: TokenStream) -> TokenStream {
184 migrate::migrate_impl(args, input)
185}
186
187//TODO: check corner-cases
188#[proc_macro_derive(AmeType)]
189pub fn ame_type_derive(input: TokenStream) -> TokenStream {
190 let input = syn::parse_macro_input!(input as syn::DeriveInput);
191 let name = &input.ident;
192
193 let field_hashes = if let syn::Data::Struct(s) = &input.data {
194 s.fields
195 .iter()
196 .map(|f| {
197 let field_name = f.ident.as_ref().map(|i| i.to_string()).unwrap_or_default();
198 let ty = &f.ty;
199 quote::quote! {
200 ^ ::amethystate::migration::types::fnv1a(#field_name.as_bytes())
201 ^ <#ty as ::amethystate::migration::types::AmeType>::TYPE_HASH
202 }
203 })
204 .collect::<Vec<_>>()
205 } else {
206 vec![]
207 };
208
209 let expanded = quote::quote! {
210 impl ::amethystate::migration::types::AmeType for #name {
211 const TYPE_HASH: u32 = ::amethystate::migration::types::fnv1a(stringify!(#name).as_bytes())
212 #(#field_hashes)*;
213 const TYPE_NAME: &'static str = stringify!(#name);
214 }
215 };
216 TokenStream::from(expanded)
217}