https://zo-devnet.n1.xyz/docs#tag/default/post/action - mixes binary encoding, headers, and numeric errors embeeding (http goes to binary tx format).
all strongly typed without duplicated boilerplate.
inside - ad hoc error assembly - display_doc,derive_more, backtrace - no anyhow, not thiserror, no snafu.
fail fast and panic a lot, covered by proptests(toward fuzzytests).
used https://github.com/target-san/scoped-panic-hook to catch panics as exceptions.
was thinking to use https://github.com/iex-rs/lithium
panics = bugs or undefinenide behavour.
``` 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)
}
```