Skip to main content

oxide_generator_rs/
lib.rs

1#![doc = include_str!("../README.md")]
2
3// Proc-macro entrypoint and stable macro surface.
4//
5// Why: keep macro names and signatures stable for downstream crates, while
6// allowing internal parsing/codegen modules to evolve safely.
7use proc_macro::TokenStream;
8use syn::Item;
9use syn::parse_macro_input;
10#[cfg(test)]
11use std::sync::{Mutex, OnceLock};
12
13mod derive;
14mod meta;
15mod reducer;
16mod routes;
17#[cfg(feature = "isolated-channels")]
18mod isolated_channels;
19#[cfg(test)]
20pub(crate) static TEST_ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
21
22#[proc_macro_attribute]
23/// Marks a struct or enum as an Oxide state type.
24///
25/// The annotated item must be a `struct` or `enum`.
26///
27/// This macro primarily does two things:
28///
29/// 1. Ensures a baseline set of derives exist (`Debug`, `Clone`, `PartialEq`, `Eq`).
30/// 2. Injects metadata into doc strings (`oxide:meta:<json>`) so downstream tools
31///    can discover the state shape without type-checking.
32///
33/// With the `state-persistence` feature enabled, the macro also injects serde
34/// derives using `oxide_core::serde` so consumer crates do not need to depend on
35/// `serde` directly.
36///
37/// # Errors
38/// Emits a compile error if applied to any other item.
39pub fn state(attr: TokenStream, item: TokenStream) -> TokenStream {
40    let args = parse_macro_input!(attr as derive::StateArgs);
41    let input = parse_macro_input!(item as Item);
42    match input {
43        Item::Struct(item_struct) => derive::expand_state_struct(args, item_struct).into(),
44        Item::Enum(item_enum) => derive::expand_state_enum(args, item_enum).into(),
45        other => syn::Error::new_spanned(other, "#[state] can only be applied to a struct or enum")
46            .to_compile_error()
47            .into(),
48    }
49}
50
51#[proc_macro_attribute]
52/// Marks an enum as an Oxide actions type.
53///
54/// The annotated item must be an `enum`.
55///
56/// This performs the same derive and metadata injection as [`state`], but is
57/// restricted to enums (actions are modeled as closed sets).
58///
59/// # Errors
60/// Emits a compile error if applied to any other item.
61pub fn actions(attr: TokenStream, item: TokenStream) -> TokenStream {
62    let _ = parse_macro_input!(attr as syn::parse::Nothing);
63    let input = parse_macro_input!(item as Item);
64    match input {
65        Item::Enum(item_enum) => derive::expand_actions_enum(item_enum).into(),
66        other => syn::Error::new_spanned(other, "#[actions] can only be applied to an enum")
67            .to_compile_error()
68            .into(),
69    }
70}
71
72#[proc_macro_attribute]
73/// Generates an Oxide engine and bindings for a reducer implementation.
74///
75/// The annotated item must be an `impl oxide_core::Reducer for <Type>` block.
76///
77/// See the crate README for the full argument mini-language, including the
78/// required `engine`, `snapshot`, and `initial` keys.
79///
80/// # Errors
81/// Emits a compile error if arguments are missing or the reducer impl is invalid.
82pub fn reducer(attr: TokenStream, item: TokenStream) -> TokenStream {
83    let args = parse_macro_input!(attr as reducer::ReducerArgs);
84    let input = parse_macro_input!(item as Item);
85    match input {
86        Item::Impl(item_impl) => reducer::expand_reducer_impl(args, item_impl).into(),
87        other => syn::Error::new_spanned(
88            other,
89            "#[reducer(...)] can only be applied to an `impl oxide_core::Reducer for <Type>` block",
90        )
91        .to_compile_error()
92        .into(),
93    }
94}
95
96#[proc_macro_attribute]
97/// Generates navigation artifacts for an application routes module.
98///
99/// Apply this macro to an inline `routes` module. The macro discovers `Route`
100/// implementations, emits route metadata, and generates navigation bridge glue.
101///
102/// # Errors
103/// Emits a compile error if route files cannot be scanned or metadata cannot be emitted.
104pub fn routes(attr: TokenStream, item: TokenStream) -> TokenStream {
105    let _ = parse_macro_input!(attr as syn::parse::Nothing);
106    let input = parse_macro_input!(item as Item);
107    match input {
108        Item::Mod(item_mod) => match routes::expand_routes_module(item_mod) {
109            Ok(ts) => ts.into(),
110            Err(e) => e.to_compile_error().into(),
111        },
112        other => syn::Error::new_spanned(other, "#[routes] can only be applied to a module")
113            .to_compile_error()
114            .into(),
115    }
116}
117
118#[proc_macro_attribute]
119pub fn oxide_route(attr: TokenStream, item: TokenStream) -> TokenStream {
120    let args = parse_macro_input!(attr as routes::OxideRouteArgs);
121    let input = parse_macro_input!(item as Item);
122    match input {
123        Item::Struct(item_struct) => match routes::expand_oxide_route_struct(args, item_struct) {
124            Ok(ts) => ts.into(),
125            Err(e) => e.to_compile_error().into(),
126        },
127        other => syn::Error::new_spanned(other, "#[oxide_route] can only be applied to a struct")
128            .to_compile_error()
129            .into(),
130    }
131}
132
133#[cfg(feature = "isolated-channels")]
134#[proc_macro_attribute]
135/// Generates glue for an Oxide isolated event channel or duplex channel.
136///
137/// This macro is applied to an `impl OxideEventChannel for <Type>` block (events)
138/// or an `impl OxideEventDuplexChannel for <Type>` block (duplex).
139///
140/// The generated code follows the locked `OxideIsolatedChannels` specification:
141/// predictable send helpers, explicit initialization boundaries, and FRB-friendly
142/// stream endpoints without string-based routing.
143pub fn oxide_event_channel(attr: TokenStream, item: TokenStream) -> TokenStream {
144    let args = parse_macro_input!(attr as isolated_channels::OxideEventChannelArgs);
145    let input = parse_macro_input!(item as Item);
146    match input {
147        Item::Impl(item_impl) => match isolated_channels::expand_oxide_event_channel(args, item_impl) {
148            Ok(ts) => ts.into(),
149            Err(e) => e.to_compile_error().into(),
150        },
151        other => syn::Error::new_spanned(
152            other,
153            "#[oxide_event_channel] can only be applied to an impl block",
154        )
155        .to_compile_error()
156        .into(),
157    }
158}
159
160#[cfg(feature = "isolated-channels")]
161#[proc_macro_attribute]
162/// Generates glue for an Oxide callback interface (Rust → Dart → Rust).
163///
164/// The macro enforces deterministic request/response binding by variant name:
165/// every `Request::<Variant>` must have a matching `Response::<Variant>`.
166pub fn oxide_callback(attr: TokenStream, item: TokenStream) -> TokenStream {
167    let args = parse_macro_input!(attr as isolated_channels::OxideCallbackArgs);
168    let input = parse_macro_input!(item as Item);
169    match input {
170        Item::Impl(item_impl) => match isolated_channels::expand_oxide_callback(args, item_impl) {
171            Ok(ts) => ts.into(),
172            Err(e) => e.to_compile_error().into(),
173        },
174        other => syn::Error::new_spanned(
175            other,
176            "#[oxide_callback] can only be applied to an impl block",
177        )
178        .to_compile_error()
179        .into(),
180    }
181}