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