Skip to main content

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}