docs: consolidate design documentation and standardize crate English

This commit is contained in:
2026-10-12 14:58:00 +09:00
parent a633a53274
commit 4efb0ace1f
404 changed files with 4556 additions and 11654 deletions
+9
View File
@@ -0,0 +1,9 @@
# api-macros
Defines transport-independent API metadata from public traits. The same declarations drive optional HTTP adapters and OpenAPI generation, while callers retain authentication, authorization, and application state.
## Main entry points
- [`api`] — API declaration and validation
- [`ApiContract`], [`Operation`], and [`WebSocketOperation`] — typed operation contracts
- `axum`, `reqwest`, and `openapi` — adapters and document generation enabled by their respective features
+4 -5
View File
@@ -1,9 +1,8 @@
//! Axum support used by routers generated with `#[api(axum)]`.
//! Transport boundary for generated routers.
//!
//! Generated operation routers contain only transport extraction, typed service invocation, and
//! response mapping. Authentication/authorization context, body-limit policy, timeout policy, and
//! other application state remain explicit outer Axum layers. Each operation is also exposed as a
//! separate router so route-specific layers can be applied before routers are merged.
//! Routers only extract inputs, call typed services, and convert responses.
//! Outer layers retain authentication, authorization, body limits, timeouts, and application state.
//! Per-operation routers allow route-specific constraints to be applied before merging.
use std::{fmt, str::FromStr};
+66 -141
View File
@@ -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")]
+6 -4
View File
@@ -1,8 +1,10 @@
//! Deterministic OpenAPI 3.1 projection for `#[api(openapi)]` contracts.
//! Generate deterministic OpenAPI documents from API contracts.
//!
//! The projection deliberately has no deployment topology (`servers`) and no
//! generated timestamps. Callers supply an opaque source digest, which is
//! retained as provenance in `info.x-yoi-source-digest`.
//! Omit deployment URLs and generation timestamps; retain the caller's source digest as
//! provenance. This separates contract changes from environment changes and produces the
//! same bytes from the same inputs. Owners explicitly assert wire-schema/Serde agreement
//! through [`OpenApiSchema`]; name collisions, unsafe integers, and ambiguous schemas are
//! not silently published.
use std::{
borrow::Cow,
+4 -4
View File
@@ -1,8 +1,8 @@
//! Reqwest transport support used by clients generated with `#[api(reqwest)]`.
//! Transport boundary for generated clients.
//!
//! The generated builder validates the base URL before it can be stored. Callers that enforce
//! stricter egress policy (custom DNS resolution, proxy policy, TLS roots, or connection pinning)
//! can construct their own [`reqwest::Client`] and inject it with the validated [`BaseUrl`].
//! Validate the base URL before storing it; apply timeouts and byte limits to requests
//! and response reads. Stricter outbound constraints such as DNS, proxy, and TLS policy
//! remain at the boundary where callers inject their own [`reqwest::Client`] with [`BaseUrl`].
use std::{fmt, time::Duration};