|
|
|
@@ -1,147 +1,72 @@
|
|
|
|
|
//! Shared API trait declarations for Yoi services.
|
|
|
|
|
//!
|
|
|
|
|
//! `#[api]` turns a public trait into a single, transport-neutral API definition. The
|
|
|
|
|
//! original trait remains usable by Rust code; generated metadata and zero-sized operation
|
|
|
|
|
//! markers give adapters and generators a stable view of the same declaration.
|
|
|
|
|
//!
|
|
|
|
|
//! # Declaration syntax
|
|
|
|
|
//!
|
|
|
|
|
//! ```
|
|
|
|
|
//! # mod example {
|
|
|
|
|
//! use api_macros::api;
|
|
|
|
|
//!
|
|
|
|
|
//! pub struct CreateWidget;
|
|
|
|
|
//! pub struct Widget;
|
|
|
|
|
//! pub struct PublicError;
|
|
|
|
|
//!
|
|
|
|
|
//! #[api]
|
|
|
|
|
//! pub trait WidgetApi {
|
|
|
|
|
//! #[post(
|
|
|
|
|
//! "/widgets",
|
|
|
|
|
//! operation_id = "widgets.create",
|
|
|
|
|
//! status = 201,
|
|
|
|
|
//! error_status = 422
|
|
|
|
|
//! )]
|
|
|
|
|
//! async fn create(
|
|
|
|
|
//! &self,
|
|
|
|
|
//! #[body] request: CreateWidget,
|
|
|
|
|
//! #[header("x-request-id")] request_id: String,
|
|
|
|
|
//! ) -> Result<Widget, PublicError>;
|
|
|
|
|
//!
|
|
|
|
|
//! #[get("/widgets/{widget_id}", operation_id = "widgets.get")]
|
|
|
|
|
//! async fn get(&self, widget_id: u64) -> Result<Widget, PublicError>;
|
|
|
|
|
//! }
|
|
|
|
|
//! # }
|
|
|
|
|
//! ```
|
|
|
|
|
//!
|
|
|
|
|
//! Supported route attributes are `get`, `post`, `put`, `patch`, `delete`, `head`, and
|
|
|
|
|
//! `options`. The first argument is a path template. `operation_id` defaults to the Rust
|
|
|
|
|
//! method name but should be set explicitly for contracts which must remain stable while Rust
|
|
|
|
|
//! names evolve. Success status defaults to `200`, except an empty (`()`) response defaults to
|
|
|
|
|
//! `204`. `alternate_status` opts a JSON response into [`HttpSuccess`] when two successful HTTP
|
|
|
|
|
//! outcomes share one schema. Operations whose successful statuses have different body or header
|
|
|
|
|
//! shapes use `responses = [(status = 200, body = Widget, headers = [("etag", String)]),
|
|
|
|
|
//! (status = 304, headers = [("etag", String)])]`; the macro emits a typed result enum in the
|
|
|
|
|
//! `<trait_name>_responses` module. Only explicit body-less `304` declarations extend success
|
|
|
|
|
//! beyond 2xx. A public error inferred from `Result<T, E>` defaults to status `400`;
|
|
|
|
|
//! `additional_error_statuses` publishes the same typed error schema for other declared outcomes.
|
|
|
|
|
//! `bearer_auth = true` and `browser_auth = true` attach standard bearer and browser-session cookie
|
|
|
|
|
//! security schemes. Body operations may opt into typed Axum rejection normalization with
|
|
|
|
|
//! typed Axum rejection normalization with `normalize_body_errors = true` and [`HttpRequestError`].
|
|
|
|
|
//!
|
|
|
|
|
//! Arguments are classified with `#[body]`, `#[binary]`, `#[query]`, `#[header]`, `#[path]`, or
|
|
|
|
|
//! `#[extension]`. Extension values are trusted server-local Axum context: generated clients and
|
|
|
|
|
//! OpenAPI omit them. An unannotated argument whose Rust name occurs in the route template is
|
|
|
|
|
//! inferred as a path argument. Header attributes may carry a wire name, as in
|
|
|
|
|
//! `#[header("x-request-id")]`. Exactly one JSON or binary body is allowed. JSON request, response,
|
|
|
|
|
//! and error bodies must be named Rust types; tuples, references, arrays, and other anonymous
|
|
|
|
|
//! structural types are rejected. Binary request bodies use [`BinaryBody`] and remain byte-exact.
|
|
|
|
|
//! A named success response whose final type identifier is `BinaryBody` is likewise byte-exact,
|
|
|
|
|
//! uses `application/octet-stream`, and may include declared response headers. Generated clients
|
|
|
|
|
//! collect binary responses under the same configured response byte limit as JSON responses.
|
|
|
|
|
//! `openapi = false` explicitly excludes an operation whose wire body cannot yet satisfy the
|
|
|
|
|
//! strict OpenAPI schema boundary; Reqwest and Axum adapters are still generated.
|
|
|
|
|
//!
|
|
|
|
|
//! WebSocket routes use a declaration-only associated type rather than a dummy unary method:
|
|
|
|
|
//!
|
|
|
|
|
//! ```
|
|
|
|
|
//! # mod websocket_example {
|
|
|
|
|
//! # use api_macros::api;
|
|
|
|
|
//! # pub struct ClientFrame;
|
|
|
|
|
//! # pub struct ServerFrame;
|
|
|
|
|
//! #[api]
|
|
|
|
|
//! pub trait EventApi {
|
|
|
|
|
//! #[websocket(
|
|
|
|
|
//! "/events/{stream_id}",
|
|
|
|
|
//! operation_id = "events.stream",
|
|
|
|
|
//! method = GET,
|
|
|
|
|
//! client_to_server = ClientFrame,
|
|
|
|
|
//! server_to_client = ServerFrame,
|
|
|
|
|
//! path_parameters = [stream_id: u64]
|
|
|
|
|
//! )]
|
|
|
|
|
//! type EventStream;
|
|
|
|
|
//! }
|
|
|
|
|
//! # }
|
|
|
|
|
//! ```
|
|
|
|
|
//!
|
|
|
|
|
//! The declaration is removed from the emitted service trait and becomes a typed marker implementing
|
|
|
|
|
//! [`WebSocketOperation`]. It participates in the same operation/route inventory and path validation,
|
|
|
|
|
//! but never generates a Reqwest method, unary Axum adapter, or OpenAPI path. Under the `axum`
|
|
|
|
|
//! feature, [`axum::websocket_route`] mounts an existing manual upgrade handler at its declared GET
|
|
|
|
|
//! path without exposing framework WebSocket types to the contract.
|
|
|
|
|
//!
|
|
|
|
|
//! # Generated names
|
|
|
|
|
//!
|
|
|
|
|
//! For `WidgetApi`, the macro emits `WidgetApiMetadata`, which implements [`ApiContract`],
|
|
|
|
|
//! plus a `widget_api_operations` module. That module contains one marker per Rust method
|
|
|
|
|
//! (`Create`, `Get`, ...), each implementing [`Operation`] and connecting metadata to concrete
|
|
|
|
|
//! body types at compile time. Unusual method names which have no valid PascalCase form use the
|
|
|
|
|
//! deterministic `Operation<utf8-hex>` fallback. The `ApiContract::OPERATIONS` inventory is
|
|
|
|
|
//! sorted by operation ID, so its ordering is independent of source method order.
|
|
|
|
|
//!
|
|
|
|
|
//! Empty, JSON, and bounded binary request and success response bodies are supported. [`WireKind`] reserves explicit
|
|
|
|
|
//! variants for other future transport work; accepting one requires a deliberate macro and adapter
|
|
|
|
|
//! change rather than silently treating it as JSON.
|
|
|
|
|
//!
|
|
|
|
|
//! # OpenAPI 3.1 export
|
|
|
|
|
//!
|
|
|
|
|
//! With the `openapi` feature, `#[api(openapi)]` emits a `<trait_name>_openapi` function from the
|
|
|
|
|
//! same normalized operation model used by the HTTP adapters. The caller supplies title, version,
|
|
|
|
|
//! and an opaque source digest through [`openapi::OpenApiInfo`], then either serves
|
|
|
|
|
//! [`openapi::OpenApiDocument::to_json`] or writes exactly those canonical bytes with
|
|
|
|
|
//! [`openapi::OpenApiDocument::write_json`] from a CLI. Documents contain no server URLs,
|
|
|
|
|
//! environment names, timestamps, or other deployment topology.
|
|
|
|
|
//!
|
|
|
|
|
//! Every exposed wire type must implement [`openapi::OpenApiSchema`] as an explicit assertion that
|
|
|
|
|
//! its `schemars::JsonSchema` contract matches Serde serialization. The trait's naming hook is the
|
|
|
|
|
//! public component-name contract. Different schemas claiming one name, unsafe-width integer
|
|
|
|
|
//! schemas, and ambiguous non-null `anyOf`/untagged representations fail closed. Use bounded
|
|
|
|
|
//! integers (for example `u32`) for JSON numeric fields, and model optionality separately from
|
|
|
|
|
//! nullability through object `required` membership and nullable schemas. A normalized
|
|
|
|
|
//! `#[header("authorization")]` string (or optional string) is projected as an HTTP bearer
|
|
|
|
|
//! security scheme and operation requirement rather than as a raw header parameter.
|
|
|
|
|
//!
|
|
|
|
|
//! # Optional HTTP adapters
|
|
|
|
|
//!
|
|
|
|
|
//! `#[api(reqwest)]` generates `TraitNameClient` and enables a typed Reqwest client when this
|
|
|
|
|
//! crate's `reqwest` feature is enabled. `#[api(axum)]` generates `TraitNameAxum` plus one public
|
|
|
|
|
//! router per operation when the `axum` feature is enabled. `#[api(reqwest, axum)]` emits both
|
|
|
|
|
//! from the same normalized definition. A plain `#[api]` remains framework-independent and pulls
|
|
|
|
|
//! neither framework into DTO/contract-only builds.
|
|
|
|
|
//!
|
|
|
|
|
//! Generated clients use a fallible `try_new` constructor and builder. The builder validates an
|
|
|
|
|
//! absolute credential-free HTTP(S) base URL and configures request timeout and response byte
|
|
|
|
|
//! limit. A prevalidated [`reqwest::BaseUrl`] and policy-configured `reqwest::framework::Client`
|
|
|
|
|
//! may be injected instead. Request authorizers receive the final method, encoded path/query, and
|
|
|
|
|
//! exact serialized body bytes; credential headers and all body content are redacted from normal
|
|
|
|
|
//! diagnostics.
|
|
|
|
|
//!
|
|
|
|
|
//! Generated Axum adapters keep authentication, authorization, application state, body limits,
|
|
|
|
|
//! and timeout layers outside the adapter. Callers may layer the complete router or an individual
|
|
|
|
|
//! generated operation router before merging it into their application.
|
|
|
|
|
#![doc = include_str!("../README.md")]
|
|
|
|
|
|
|
|
|
|
extern crate self as api_macros;
|
|
|
|
|
|
|
|
|
|
/// Normalize a public trait into a transport-independent, typed API contract.
|
|
|
|
|
///
|
|
|
|
|
/// Preserve the original Rust trait and generate operation markers and metadata from the
|
|
|
|
|
/// same declaration. HTTP adapters and OpenAPI use the same normalized result, avoiding
|
|
|
|
|
/// manual synchronization of separate route lists. Sort the inventory by operation ID so
|
|
|
|
|
/// declaration reordering does not change the external contract order.
|
|
|
|
|
///
|
|
|
|
|
/// Validate request/response wire shapes at declaration time; unsupported types and duplicate
|
|
|
|
|
/// routes are compile errors. Exclude server-local extensions from clients and OpenAPI;
|
|
|
|
|
/// leave authentication, authorization, body limits, and timeouts to the enclosing application.
|
|
|
|
|
/// Declarations without adapters do not depend on an HTTP framework.
|
|
|
|
|
///
|
|
|
|
|
/// This declaration checks that a Rust trait and typed operation markers can be defined together.
|
|
|
|
|
///
|
|
|
|
|
/// ```
|
|
|
|
|
/// # mod example {
|
|
|
|
|
/// use api_macros::api;
|
|
|
|
|
///
|
|
|
|
|
/// pub struct CreateWidget;
|
|
|
|
|
/// pub struct Widget;
|
|
|
|
|
/// pub struct PublicError;
|
|
|
|
|
///
|
|
|
|
|
/// #[api]
|
|
|
|
|
/// pub trait WidgetApi {
|
|
|
|
|
/// #[post(
|
|
|
|
|
/// "/widgets",
|
|
|
|
|
/// operation_id = "widgets.create",
|
|
|
|
|
/// status = 201,
|
|
|
|
|
/// error_status = 422
|
|
|
|
|
/// )]
|
|
|
|
|
/// async fn create(
|
|
|
|
|
/// &self,
|
|
|
|
|
/// #[body] request: CreateWidget,
|
|
|
|
|
/// #[header("x-request-id")] request_id: String,
|
|
|
|
|
/// ) -> Result<Widget, PublicError>;
|
|
|
|
|
///
|
|
|
|
|
/// #[get("/widgets/{widget_id}", operation_id = "widgets.get")]
|
|
|
|
|
/// async fn get(&self, widget_id: u64) -> Result<Widget, PublicError>;
|
|
|
|
|
/// }
|
|
|
|
|
/// # }
|
|
|
|
|
/// ```
|
|
|
|
|
///
|
|
|
|
|
/// WebSocket associated types are declarations only: remove them from the output trait
|
|
|
|
|
/// and turn them into [`WebSocketOperation`] markers. They are not unary HTTP operations
|
|
|
|
|
/// and generate neither client methods nor OpenAPI paths.
|
|
|
|
|
///
|
|
|
|
|
/// ```
|
|
|
|
|
/// # mod websocket_example {
|
|
|
|
|
/// # use api_macros::api;
|
|
|
|
|
/// # pub struct ClientFrame;
|
|
|
|
|
/// # pub struct ServerFrame;
|
|
|
|
|
/// #[api]
|
|
|
|
|
/// pub trait EventApi {
|
|
|
|
|
/// #[websocket(
|
|
|
|
|
/// "/events/{stream_id}",
|
|
|
|
|
/// operation_id = "events.stream",
|
|
|
|
|
/// method = GET,
|
|
|
|
|
/// client_to_server = ClientFrame,
|
|
|
|
|
/// server_to_client = ServerFrame,
|
|
|
|
|
/// path_parameters = [stream_id: u64]
|
|
|
|
|
/// )]
|
|
|
|
|
/// type EventStream;
|
|
|
|
|
/// }
|
|
|
|
|
/// # }
|
|
|
|
|
/// ```
|
|
|
|
|
pub use api_macros_impl::api;
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "axum")]
|
|
|
|
|