by dlahoda | 2025-11-26 11:27:14+00:00
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) }

```