Skip to main content

alux_ext_macros/
lib.rs

1//! Procedural macros for declaring and defunctionalizing DD programs.
2//!
3//! This crate is the shared syntax layer for DD extension, HTTP, and JSON-RPC programs.
4//! Its public surface stays small: parsing, validation, and code generation
5//! live in private modules, while this facade contains only procedural-macro
6//! entry points. Product crates re-export the macros they own.
7//!
8//! Every example below is illustrative rather than tested. Expansion names `::alux_ext`,
9//! `::alux_http`, and `::alux_jsonrpc`, and a procedural-macro crate cannot depend on the crates its
10//! own expansion targets. The executed examples live in those crates' documentation.
11
12mod ext;
13mod extension;
14mod http_program;
15mod jsonrpc_program;
16mod lower;
17mod shape_program;
18mod syntax;
19
20use ext::ext_internal;
21use http_program::http_program_defunc_internal;
22use jsonrpc_program::jsonrpc_program_defunc_internal;
23use proc_macro::TokenStream;
24use shape_program::shape_program_defunc_internal;
25
26/// Declares extension methods and optionally gives each method a first-order operation type.
27///
28/// Arguments unrelated to defunctionalization are forwarded to `extend::ext`.
29/// Plain `defunc` produces a hidden `*Operation<Context>` type implementing
30/// `OperationAlg` and `ApplyAlg`. When every method already builds first-order
31/// route syntax from `routes()`, plain `defunc` names those inferred programs
32/// directly. A backend such as `defunc(via = http)` or `defunc(via = jsonrpc)`
33/// additionally lifts convenient method references while producing named programs
34/// and their interpreter evidence.
35///
36/// ```ignore
37/// use alux_ext::ext;
38///
39/// #[ext(name = CounterExt, defunc)]
40/// impl<This> This
41/// where
42///     This: CounterAlg,
43/// {
44///     async fn incremented(&self, by: u32) -> u32 {
45///         self.increment(by).await
46///     }
47/// }
48///
49/// // The expansion also defines `IncrementedOperation<This>`.
50/// ```
51///
52/// ```ignore
53/// use alux_ext::ext;
54/// use alux_http::http;
55///
56/// #[ext(name = StatusRoutesExt, defunc(via = http))]
57/// impl<This> This
58/// where
59///     This: HttpApiAlg + JsonOutAlg,
60/// {
61///     /// Declares the status surface.
62///     fn status_routes<Alg>(&self)
63///     where
64///         Alg: StatusAlg,
65///     {
66///         self.routes().get("/status", self.op(Alg::status_current).json())
67///     }
68/// }
69///
70/// // The expansion also defines `StatusRoutesProgram<Alg>`.
71/// ```
72#[proc_macro_attribute]
73pub fn ext(attr: TokenStream, item: TokenStream) -> TokenStream {
74    ext_internal(attr.into(), item.into()).unwrap_or_else(syn::Error::into_compile_error).into()
75}
76
77/// Lowers extension methods into named, composable HTTP programs.
78///
79/// This is the HTTP backend that `alux_ext::ext(..., defunc(via = http))` selects. `via = http`
80/// resolves the name in the authoring scope, so a declaration imports it as `use alux_http::http`.
81/// Applying the attribute directly means the same thing.
82///
83/// Each method becomes a first-order HTTP program. A route handler written with `op(...)` is replaced
84/// by the operation type generated by `alux_ext::ext(defunc)`, the declared input roles and output
85/// kind become bounds on the interpreter, and a call to another method of the same extension becomes
86/// a nested program.
87///
88/// ```ignore
89/// use alux_ext::ext;
90/// use alux_http::{HttpApiAlg, JsonOutAlg, http};
91///
92/// #[ext(name = StatusApiExt, defunc(via = http))]
93/// impl<This> This
94/// where
95///     This: HttpApiAlg + JsonOutAlg,
96/// {
97///     /// Declares the status surface.
98///     fn status_api<Alg>(&self)
99///     where
100///         Alg: StatusAlg,
101///     {
102///         self.routes().get("/status", self.op(Alg::status_current).json())
103///     }
104/// }
105///
106/// // The expansion also defines `StatusApiProgram<Alg>`.
107/// ```
108#[proc_macro_attribute]
109pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream {
110    http_program_defunc_internal(attr.into(), item.into()).unwrap_or_else(syn::Error::into_compile_error).into()
111}
112
113/// Lowers extension methods into named, composable JSON-RPC programs.
114///
115/// This is the JSON-RPC backend that `alux_ext::ext(..., defunc(via = jsonrpc))` selects. `via = jsonrpc`
116/// resolves the name in the authoring scope, so a declaration imports it as `use alux_jsonrpc::jsonrpc`.
117/// Applying the attribute directly means the same thing.
118///
119/// Each method becomes a first-order JSON-RPC program. A method handler written with `op(...)` is
120/// replaced by the operation type generated by `alux_ext::ext(defunc)`, the operation's argument
121/// product and output become bounds on the interpreter, `.named()` selects object decoding using the
122/// authored argument names, and a call to another method of the same extension becomes a nested
123/// program.
124///
125/// A `fallible` argument converts every declared method's error into a JSON-RPC protocol error, so
126/// each declaration that does not say otherwise is read as `.fallible()`. This argument belongs to
127/// the JSON-RPC backend; every other argument is forwarded to `extend::ext`.
128///
129/// ```ignore
130/// use alux_ext::ext;
131/// use alux_jsonrpc::{JsonRpcApiAlg, jsonrpc};
132///
133/// #[ext(name = StatusRpcExt, defunc(via = jsonrpc))]
134/// impl<This> This
135/// where
136///     This: JsonRpcApiAlg,
137/// {
138///     /// Declares the status surface.
139///     fn status_rpc<Alg>(&self)
140///     where
141///         Alg: StatusAlg,
142///     {
143///         self.methods().method("status_current", self.op(Alg::status_current))
144///     }
145/// }
146///
147/// // The expansion also defines `StatusRpcProgram<Alg>`.
148/// ```
149#[proc_macro_attribute]
150pub fn jsonrpc(attr: TokenStream, item: TokenStream) -> TokenStream {
151    jsonrpc_program_defunc_internal(attr.into(), item.into()).unwrap_or_else(syn::Error::into_compile_error).into()
152}
153
154/// Lowers extension methods into named, composable shape programs.
155///
156/// This is the shape backend that `alux_ext::ext(..., defunc(via = shape))` selects. `via = shape`
157/// resolves the name in the authoring scope, so a declaration imports it as `use alux_shape::shape`.
158/// Applying the attribute directly means the same thing.
159///
160/// Each method becomes a first-order shape program named after the method, with a `_shape` suffix
161/// dropped. An identifier in name position states the words that name a member, splitting on `_`; a
162/// string literal there states a name no identifier spells. The record the declaration opens is named
163/// and closed by the expansion, so no declaration writes a name for it, a closing call, or a return
164/// type. A call to another method of the same extension becomes a nested program.
165///
166/// A shape body applies no handler — it reads the algebra through the same `self` the author wrote —
167/// so the bounds the impl states are the whole requirement, and no per-member evidence is generated.
168///
169/// The expansion adds no imports, because a declaration's `use` list is the author's. So everything a
170/// body reads must already be in scope: the algebra, and any extension the body derives its vocabulary
171/// from — `ShapeTaggedExt` for a tagged encoding, whatever ext states a leaf vocabulary of its own.
172/// `shape` itself must be in scope too, or `defunc(via = shape)` resolves to nothing: neither
173/// attribute expands, the authored block reaches the compiler as written, and the errors read
174/// `visibility qualifiers are not permitted here` on the `pub impl` followed by `cannot find value`
175/// for every member name. Nothing in that points at the missing import.
176///
177/// ```ignore
178/// use alux_ext::ext;
179/// use alux_shape::{FieldAlg, ShapeAlg, shape};
180///
181/// #[ext(name = UserShapeExt, defunc(via = shape))]
182/// pub impl<This> This
183/// where
184///     This: ShapeAlg + FieldAlg,
185/// {
186///     /// A user, as a surface answers one.
187///     fn user_shape(&self) {
188///         self.record().field(display_name, self.text()).field(email, self.opt(self.text()))
189///     }
190/// }
191///
192/// // The expansion also defines `UserShapeProgram`.
193/// ```
194#[proc_macro_attribute]
195pub fn shape(attr: TokenStream, item: TokenStream) -> TokenStream {
196    shape_program_defunc_internal(attr.into(), item.into()).unwrap_or_else(syn::Error::into_compile_error).into()
197}