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