Migrate runtime management REST to generated API

This commit is contained in:
2026-09-18 01:20:03 +09:00
parent 01f5c560c7
commit 5038033c3a
13 changed files with 1512 additions and 217 deletions
+9
View File
@@ -171,6 +171,15 @@ pub trait Operation {
const METADATA: OperationMetadata;
}
/// Public JSON error that can select its concrete HTTP status at the server boundary.
///
/// `error_status` remains the contract's default/documented status. Implementations may return a
/// different 4xx/5xx status for one named error shape, allowing a generated client to preserve a
/// service's typed 401/403/404/409 responses without introducing duplicate route handlers.
pub trait HttpError {
fn status_code(&self) -> u16;
}
/// Marker used when an operation has no request, response, or public error body.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum NoBody {}
+8
View File
@@ -352,6 +352,8 @@ impl<A: RequestAuthorizer> ClientCore<A> {
let response = request.send().await.map_err(|error| {
if error.is_timeout() {
ClientFailure::Timeout
} else if error.is_connect() {
ClientFailure::Connect
} else {
ClientFailure::Transport
}
@@ -461,10 +463,12 @@ pub enum DecodeKind {
pub enum ClientFailure {
Authorization,
RequestEncoding,
Connect,
Transport,
Timeout,
ResponseTooLarge { limit: usize },
UnexpectedStatus { expected: u16, actual: u16 },
ErrorResponseDecode { status: u16 },
Decode { kind: DecodeKind },
UnexpectedBody,
}
@@ -480,6 +484,7 @@ impl fmt::Display for ClientFailure {
match self {
Self::Authorization => formatter.write_str("request authorization failed"),
Self::RequestEncoding => formatter.write_str("request encoding failed"),
Self::Connect => formatter.write_str("HTTP connection failed"),
Self::Transport => formatter.write_str("HTTP transport failed"),
Self::Timeout => formatter.write_str("HTTP request timed out"),
Self::ResponseTooLarge { limit } => {
@@ -491,6 +496,9 @@ impl fmt::Display for ClientFailure {
"expected HTTP status {expected}, received {actual}"
)
}
Self::ErrorResponseDecode { status } => {
write!(formatter, "HTTP {status} error response JSON was malformed")
}
Self::Decode {
kind: DecodeKind::Success,
} => formatter.write_str("success response JSON was malformed"),
+15
View File
@@ -28,6 +28,14 @@ pub struct Widget {
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PublicError {
message: String,
#[serde(skip)]
status: u16,
}
impl api_macros::HttpError for PublicError {
fn status_code(&self) -> u16 {
self.status
}
}
#[api(reqwest, axum)]
@@ -65,6 +73,7 @@ impl WidgetApi for WidgetService {
if request.name == "bad" {
return Err(PublicError {
message: "sensitive-public-detail".to_owned(),
status: 409,
});
}
Ok(Widget {
@@ -210,10 +219,16 @@ async fn generated_client_and_router_follow_the_normalized_contract() {
.await
.expect_err("typed public error");
assert!(!format!("{public_error:?}").contains("sensitive-public-detail"));
assert!(matches!(
&public_error,
api_reqwest::ClientError::Public { status, .. }
if *status == api_reqwest::framework::StatusCode::CONFLICT
));
assert_eq!(
public_error.into_public_error(),
Some(PublicError {
message: "sensitive-public-detail".to_owned(),
status: 0,
})
);