|
used like this ```
pub async fn account_pubkey(
GetAccountPubkey { account_id }: GetAccountPubkey,
State(st): State<AppState>,
) -> Response!(RegistrationKey; not_found: UserNotFound) {
``` ```
pub async fn action(
SubmitAction {}: SubmitAction,
State(st): State<AppState>,
TypedHeader(content_type): TypedHeader<headers::ContentType>,
body: axum::body::Bytes,
) -> Result<
axum::body::Bytes,
ApiError!(unsupported_media_type: AcceptedMediaType, payload_too_large: PayloadTooLarge),
> {
``` as you see limitation of ast macro - single entity per code. to do more - need proc macro. default response is json ```
#[macro_export]
macro_rules! Response {
($ty:ty) => {
Result<::axum::Json<$ty>, $crate::http::error::Error<()>>
};
($ty:ty; $($var:ident : $e:ty),) => {
Result<::axum::Json<$ty>, nord_core::ApiError!($($var : $e),)>
};
}
``` use ApiError for other mime types ```
#[macro_export]
macro_rules! ApiError {
( $($var:ident : $e:ty),* $(,)? ) => {
nord_core::ApiError!({ $($var : $e,)* })
};
() => {
$crate::http::error::Error::<()>
};
({ $($var:ident : $e:ty),* $(,)? }) => {
nord_core::ApiError!(
@internal
bad_request: $crate::http::error::Infallible,
not_found: $crate::http::error::Infallible,
forbidden: $crate::http::error::Infallible,
unsupported_media_type: $crate::http::error::Infallible,
payload_too_large: $crate::http::error::Infallible,
not_implemented: $crate::http::error::Infallible,
| $($var : $e,)*
)
};
(
@internal
bad_request: $bad_request:ty,
not_found: $not_found:ty,
forbidden: $forbidden:ty,
unsupported_media_type: $unsupported_media_type:ty,
payload_too_large: $payload_too_large:ty,
not_implemented: $not_implemented:ty,
| // empty
) => {
$crate::http::error::Error<(
$bad_request,
$not_found,
$forbidden,
$unsupported_media_type,
$payload_too_large,
$not_implemented,
)>
};
(
@internal
bad_request: $_:ty,
not_found: $not_found:ty,
forbidden: $forbidden:ty,
unsupported_media_type: $unsupported_media_type:ty,
payload_too_large: $payload_too_large:ty,
not_implemented: $not_implemented:ty,
| bad_request: $bad_request:ty, $($rest:tt)*
) => {
nord_core::ApiError!(
@internal
bad_request: $bad_request,
not_found: $not_found,
forbidden: $forbidden,
unsupported_media_type: $unsupported_media_type,
payload_too_large: $payload_too_large,
not_implemented: $not_implemented,
| $($rest)*
)
}; .... crazy a lot of repeated code of recursive tt muncher ``` error can be custom: ```
#[derive(Debug, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct AcceptedMediaType {
pub expected: String,
} impl ExpectedMimeType for AcceptedMediaType {
fn expected(&self) -> &str {
&self.expected
}
} impl AcceptedMediaType {
pub fn new(value: headers::ContentType) -> Self {
Self {
expected: value.to_string(),
}
}
}
``` each method has its own error as needed. openapi integration
``` impl<ST: StatusTypes> aide::OperationOutput for Error<ST> {
type Inner = Self; fn inferred_responses(
cx: &mut aide::generate::GenContext,
op: &mut aide::openapi::Operation,
) -> Vec<(Option<u16>, aide::openapi::Response)> {
[
<ST::BadRequest as OperationOutputInternal>::operation_response(cx, op)
.map(|x| (Some(400), x)),
....
<ST::UnsupportedMediaType as OperationOutputInternal>::operation_response(cx, op).map(
|mut x| {
use aide::openapi::{
Header, ParameterSchemaOrContent, ReferenceOr, SchemaObject,
};
let header = Header {
description: Some("Expected request media type".into()),
style: Default::default(),
required: true,
deprecated: None,
format: ParameterSchemaOrContent::Schema(SchemaObject {
json_schema: schemars::schema::Schema::Object(
schemars::schema_for!(String).schema,
),
external_docs: None,
example: None,
}),
example: Some(serde_json::json!(
mime::APPLICATION_OCTET_STREAM.to_string()
)),
examples: Default::default(),
extensions: Default::default(),
};
x.headers
.insert(header::ACCEPT.to_string(), ReferenceOr::Item(header));
(Some(415), x)
},
),
...
<ST::NotImplemented as OperationOutputInternal>::operation_response(cx, op)
.map(|x| (Some(501), x)),
]
.into_iter()
.flatten()
.collect()
}
}
```user vs internal errors - tracing: ```
impl<ST: StatusTypes> IntoResponse for Error<ST>
where
ST: StatusTypes,
{
fn into_response(self) -> axum::response::Response {
let status = self.status_code();
match self {
Self::Internal(error) => {
let error = &error as &dyn std::error::Error;
tracing::error!(error, "internal error during http request");
(status, Json("INTERNAL SERVER ERROR")).into_response()
}
Self::Forbidden(e) => (status, Json(e)).into_response(),
Self::UnsupportedMediaType(e) => {
let value = HeaderValue::from_str(e.expected());
let mut resp = (status, Json(e)).into_response();
if let Ok(value) = value {
resp.headers_mut().insert(header::ACCEPT, value);
}
resp
}
Self::PayloadTooLarge(e) => (status, Json(e)).into_response(),
...
}
}
} ``` and types sugar
```
mod typelevel { /// No client error defined; this type can't be constructed.
#[derive(Debug, Serialize, Deserialize)]
pub enum Infallible {}
impl ExpectedMimeType for Infallible {
fn expected(&self) -> &str {
unreachable!("Infallible")
}
}
pub trait ExpectedMimeType {
fn expected(&self) -> &str;
}
pub trait StatusTypes {
type BadRequest: serde::Serialize + OperationOutputInternal;
type UnsupportedMediaType: serde::Serialize + OperationOutputInternal + ExpectedMimeType;
...
}
impl StatusTypes for () {
type BadRequest = Infallible;
type NotFound = Infallible;
...
} impl StatusTypes for Infallible {
type BadRequest = Infallible;
type NotFound = Infallible;
..
} impl<
BadRequest: serde::Serialize + OperationOutputInternal,
NotFound: serde::Serialize + OperationOutputInternal,
Forbidden: serde::Serialize + OperationOutputInternal,
UnsupportedMediaType: serde::Serialize + OperationOutputInternal + ExpectedMimeType,
PayloadTooLarge: serde::Serialize + OperationOutputInternal,
NotImplemented: serde::Serialize + OperationOutputInternal,
> StatusTypes
for (
BadRequest,
NotFound,
Forbidden,
UnsupportedMediaType,
PayloadTooLarge,
NotImplemented,
)
{
type BadRequest = BadRequest;
type NotFound = NotFound;
type Forbidden = Forbidden;
type UnsupportedMediaType = UnsupportedMediaType;
type PayloadTooLarge = PayloadTooLarge;
type NotImplemented = NotImplemented;
}
...
}use typelevel::; #[derive(Debug)]
pub enum Error<ST: StatusTypes> {
Internal(Box<dyn std::error::Error + Send + Sync>),
BadRequest(ST::BadRequest),
...
} impl std::fmt::Display for Infallible {
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {}
}
} impl<ST: StatusTypes> Error<ST> {
pub fn internal(value: impl std::error::Error + Send + Sync + 'static) -> Self {
Self::Internal(Box::new(value))
} pub fn bad_request(value: ST::BadRequest) -> Self {
Self::BadRequest(value)
}
pub fn not_found(value: ST::NotFound) -> Self {
Self::NotFound(value)
}
pub fn forbidden(value: ST::Forbidden) -> Self {
Self::Forbidden(value)
}
``` |