3 ms·
mind sharing a link?
by vsgherzi 11mo ago
mind sharing a link?
- dlahoda 11mo agoused 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) } ```
- dlahoda 11mo agoend result is here https://zo-devnet.n1.xyz/docs https://zo-devnet.n1.xyz/docs - well documented strongly typed errors for API. most eveolved https://zo-devnet.n1.xyz/docs#tag/default/post/action 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 https://github.com/target-san/scoped-panic-hook to catch panics as exceptions. was thinking to use https://github.com/iex-rs/lithium https://github.com/iex-rs/lithium panics = bugs or undefinenide behavour.