Skip to main content

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}