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