Complete repository contract error coverage

This commit is contained in:
2026-09-18 18:34:29 +09:00
parent 82c191d637
commit 103fee0ba4
9 changed files with 1032 additions and 212 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ pub fn status(code: u16) -> StatusCode {
pub mod framework {
pub use axum::{
Json, Router,
extract::{Extension, Path, Query, State},
extract::{Extension, Path, Query, State, rejection::JsonRejection},
http::{HeaderMap, StatusCode},
response::Response,
routing::{delete, get, head, options, patch, post, put},
+10 -1
View File
@@ -39,7 +39,11 @@
//! 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. A public error inferred from `Result<T, E>` defaults to status `400`.
//! outcomes share one schema. 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]`, `#[query]`, `#[header]`, `#[path]`, or `#[extension]`.
//! Extension values are trusted server-local Axum context: generated clients and OpenAPI omit them.
@@ -211,6 +215,11 @@ pub trait HttpSuccess {
fn status_code(&self) -> u16;
}
/// Public error type capable of normalizing Axum request-body rejections.
pub trait HttpRequestError: HttpError {
fn from_request_rejection(status: u16, message: String) -> Self;
}
/// Marker used when an operation has no request, response, or public error body.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum NoBody {}
+47 -15
View File
@@ -131,6 +131,7 @@ pub struct OpenApiBuilder {
schemas: BTreeMap<String, Value>,
operation_ids: BTreeSet<String>,
uses_bearer_auth: bool,
uses_browser_auth: bool,
}
impl OpenApiBuilder {
@@ -157,6 +158,7 @@ impl OpenApiBuilder {
schemas: BTreeMap::new(),
operation_ids: BTreeSet::new(),
uses_bearer_auth: false,
uses_browser_auth: false,
})
}
@@ -199,6 +201,7 @@ impl OpenApiBuilder {
request_body: None,
responses: BTreeMap::new(),
bearer_security_required: None,
browser_security_required: false,
})
}
@@ -214,15 +217,30 @@ impl OpenApiBuilder {
let schemas = self.schemas.into_iter().collect::<Map<_, _>>();
let mut components = Map::new();
components.insert("schemas".to_owned(), Value::Object(schemas));
let mut security_schemes = Map::new();
if self.uses_bearer_auth {
security_schemes.insert(
"bearerAuth".to_owned(),
json!({
"type": "http",
"scheme": "bearer"
}),
);
}
if self.uses_browser_auth {
security_schemes.insert(
"browserSession".to_owned(),
json!({
"type": "apiKey",
"in": "cookie",
"name": "yoi_workspace_session"
}),
);
}
if !security_schemes.is_empty() {
components.insert(
"securitySchemes".to_owned(),
json!({
"bearerAuth": {
"type": "http",
"scheme": "bearer"
}
}),
Value::Object(security_schemes),
);
}
@@ -295,6 +313,7 @@ pub struct OpenApiOperation<'a> {
request_body: Option<Value>,
responses: BTreeMap<String, Value>,
bearer_security_required: Option<bool>,
browser_security_required: bool,
}
impl OpenApiOperation<'_> {
@@ -338,6 +357,16 @@ impl OpenApiOperation<'_> {
Ok(())
}
pub fn bearer_authentication(&mut self) {
self.parent.uses_bearer_auth = true;
self.bearer_security_required = Some(true);
}
pub fn browser_authentication(&mut self) {
self.parent.uses_browser_auth = true;
self.browser_security_required = true;
}
pub fn request_body<T: OpenApiSchema>(
&mut self,
content_type: &'static str,
@@ -418,15 +447,18 @@ impl OpenApiOperation<'_> {
if let Some(request_body) = self.request_body {
operation.insert("requestBody".to_owned(), request_body);
}
if let Some(required) = self.bearer_security_required {
operation.insert(
"security".to_owned(),
if required {
json!([{ "bearerAuth": [] }])
} else {
json!([{}, { "bearerAuth": [] }])
},
);
let mut security = Vec::new();
if self.bearer_security_required == Some(false) {
security.push(json!({}));
}
if self.bearer_security_required.is_some() {
security.push(json!({ "bearerAuth": [] }));
}
if self.browser_security_required {
security.push(json!({ "browserSession": [] }));
}
if !security.is_empty() {
operation.insert("security".to_owned(), Value::Array(security));
}
self.parent
.paths