Migrate runtime management REST to generated API
This commit is contained in:
@@ -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 {}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user