toolkit_contract_macros/lib.rs
1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
2
3use proc_macro::TokenStream;
4use syn::parse_macro_input;
5
6/// Code generation for the `#[contract]` trait expansion.
7mod codegen;
8mod consumes;
9mod contract_error;
10mod grpc_contract;
11mod grpc_contract_parse;
12/// Code generation for `#[derive(ContractError)]`.
13mod model;
14/// Code generation for the `#[grpc_contract]` projection.
15mod parse;
16mod projection;
17mod proto_bridge;
18mod provides;
19mod query_params;
20mod rest_contract;
21mod rest_contract_parse;
22mod stream_attr;
23mod support;
24
25#[proc_macro_attribute]
26pub fn contract(attr: TokenStream, item: TokenStream) -> TokenStream {
27 let contract_attr = parse_macro_input!(attr as parse::ContractAttr);
28 let item_trait = parse_macro_input!(item as syn::ItemTrait);
29
30 match parse::parse_trait(contract_attr, &item_trait) {
31 Ok(model) => codegen::generate(&model).into(),
32 Err(err) => err.to_compile_error().into(),
33 }
34}
35
36#[proc_macro_attribute]
37pub fn rest_contract(attr: TokenStream, item: TokenStream) -> TokenStream {
38 let attr = parse_macro_input!(attr as rest_contract_parse::RestContractAttr);
39 let item = parse_macro_input!(item as syn::ItemTrait);
40
41 match rest_contract_parse::parse(attr, item) {
42 Ok(model) => rest_contract::generate(&model).into(),
43 Err(err) => err.to_compile_error().into(),
44 }
45}
46
47#[proc_macro_attribute]
48pub fn grpc_contract(attr: TokenStream, item: TokenStream) -> TokenStream {
49 let attr = parse_macro_input!(attr as grpc_contract_parse::GrpcContractAttr);
50 let item = parse_macro_input!(item as syn::ItemTrait);
51
52 match grpc_contract_parse::parse(attr, item) {
53 Ok(model) => grpc_contract::generate(&model).into(),
54 Err(err) => err.to_compile_error().into(),
55 }
56}
57
58/// `#[toolkit::provides(contract = ..., local = ..., transports = [...])]` —
59/// auto-wire a generated contract client into the host `ClientHub`.
60///
61/// Applied on a module struct in the provider crate; generates an inherent
62/// `wire_<contract_snake>` async method that validates the contract IR,
63/// reads typed wiring config, and registers the appropriate Local/REST/gRPC
64/// client. See `toolkit_contract_macros::provides` for the full attribute
65/// surface.
66#[proc_macro_attribute]
67pub fn provides(attr: TokenStream, item: TokenStream) -> TokenStream {
68 let attr = parse_macro_input!(attr as provides::ProvidesAttr);
69 let item = parse_macro_input!(item as syn::ItemStruct);
70 match provides::generate(&attr, &item) {
71 Ok(tokens) => tokens.into(),
72 Err(err) => err.to_compile_error().into(),
73 }
74}
75
76/// `#[toolkit::consumes(contract = ..., from = "gear")]` — declare a contract
77/// dependency wired via eventual-readiness directory discovery.
78///
79/// Applied on the gear struct (alongside `#[toolkit::gear]`). Emits a
80/// `ConsumerRegistration` that the runtime's
81/// proxy-wiring phase replays: a compile-time local impl wins, otherwise a
82/// directory-resolving REST client is registered. Does NOT inject a topo-sort
83/// dependency — see `toolkit_contract_macros::consumes` docs.
84#[proc_macro_attribute]
85pub fn consumes(attr: TokenStream, item: TokenStream) -> TokenStream {
86 let attr = parse_macro_input!(attr as consumes::ConsumesAttr);
87 let item = parse_macro_input!(item as syn::ItemStruct);
88 match consumes::generate(&attr, &item) {
89 Ok(tokens) => tokens.into(),
90 Err(err) => err.to_compile_error().into(),
91 }
92}
93
94#[proc_macro_derive(ProtoBridge, attributes(proto_bridge))]
95pub fn derive_proto_bridge(input: TokenStream) -> TokenStream {
96 let input = parse_macro_input!(input as syn::DeriveInput);
97 match proto_bridge::generate(&input) {
98 Ok(tokens) => tokens.into(),
99 Err(err) => err.to_compile_error().into(),
100 }
101}
102
103/// `#[derive(QueryParams)]` — mark a struct as a REST query parameter.
104///
105/// Generates `impl QueryParams`, whose `openapi_params()` describes each field
106/// for the `OpenAPI` document. The generated route registers those parameters, so
107/// the spec is derived from the same declaration that determines the wire
108/// format instead of being inferred separately.
109///
110/// Field rules, both enforced at compile time:
111/// - every field's leaf type must implement
112/// [`QueryScalar`](toolkit_contract::query::QueryScalar) — scalars,
113/// `Option<scalar>`, and `Vec<scalar>`. Nested structs are rejected: a query
114/// string is a flat key/value list and cannot represent them unambiguously.
115/// - a `Vec<..>` field must carry `#[serde(default)]`, since an empty vector
116/// emits no key and would otherwise fail to deserialize.
117///
118/// `#[serde(rename = "...")]` and `#[serde(skip)]` are honoured so the spec
119/// matches what serde actually puts on the wire.
120#[proc_macro_derive(QueryParams)]
121pub fn derive_query_params(input: TokenStream) -> TokenStream {
122 let input = parse_macro_input!(input as syn::DeriveInput);
123 match query_params::generate(&input) {
124 Ok(tokens) => tokens.into(),
125 Err(err) => err.to_compile_error().into(),
126 }
127}
128
129/// `#[derive(ContractError)]` — wire a typed Rust error enum into the
130/// PRD #1536 RFC 9457 envelope.
131///
132/// Per-variant attributes:
133/// - `#[error_code("INSUFFICIENT_FUNDS")]` (required)
134/// - `#[error_domain("billing.v1")]` (required, or set once on the enum)
135/// - `#[canonical(FailedPrecondition)]` (required — one of the 16
136/// `ProblemCategory` variants)
137///
138/// Generates `From<MyError> for Problem` (server-side) and
139/// `TryFrom<Problem> for MyError` (client-side); unknown
140/// `error_code`/`error_domain` pairs round-trip back as the original
141/// `Problem` so callers can still handle them as generic envelopes.
142///
143/// Mark exactly one variant `#[contract_error(fallback)]` (unit, or a single
144/// named field receiving the original `Problem`) to additionally generate a
145/// **total** `From<TransportError> for MyError`, gated on the SDK having either
146/// the `rest-client` or the `grpc-client` feature. Both generated clients call
147/// that conversion on every failure, and they use it to reconstruct typed
148/// variants from an RFC 9457 envelope — the response body over HTTP, the
149/// `x-toolkit-problem-bin` trailer over gRPC — and to route
150/// un-reconstructable transport/protocol failures into the fallback variant.
151///
152/// **Declare a `ContractError` enum as a contract method's error type whenever
153/// the caller branches on a typed variant's payload.** `CanonicalError` cannot
154/// carry one: it has no field for `error_code`, `error_domain` or
155/// `context["data"]`, so converting through it strips the domain identity in
156/// both directions, and for the categories whose context type has required
157/// fields (`FailedPrecondition`, `ResourceExhausted`, `InvalidArgument`,
158/// `Aborted`) it also loses or corrupts the *category*.
159///
160/// The fallback field may be `Problem` **or** `Box<Problem>`. Prefer the boxed
161/// form: a `Problem` is ~208 bytes and the fallback variant sets the size of
162/// the whole enum, hence of every `Result<_, MyError>` the contract returns,
163/// which trips `clippy::result_large_err` on each generated method.
164#[proc_macro_derive(
165 ContractError,
166 attributes(error_code, error_domain, canonical, contract_error)
167)]
168pub fn derive_contract_error(input: TokenStream) -> TokenStream {
169 let input = parse_macro_input!(input as syn::DeriveInput);
170 match contract_error::generate(input) {
171 Ok(tokens) => tokens.into(),
172 Err(err) => err.to_compile_error().into(),
173 }
174}