A better Rust ATProto crate

IntoResponse compatible error passthrough for axum

+110 -40
+109 -38
crates/jacquard-axum/src/lib.rs
··· 53 53 Json, Router, 54 54 body::Bytes, 55 55 extract::{FromRequest, Request}, 56 - http::{HeaderValue, StatusCode, header}, 56 + http::StatusCode, 57 57 response::{IntoResponse, Response}, 58 58 }; 59 59 use jacquard::{ 60 60 IntoStatic, 61 - xrpc::{XrpcEndpoint, XrpcMethod, XrpcRequest}, 61 + xrpc::{XrpcEndpoint, XrpcError, XrpcMethod, XrpcRequest}, 62 62 }; 63 63 use serde_json::json; 64 64 ··· 92 92 Ok(value) => Ok(ExtractXrpc(*value.into_static())), 93 93 Err(err) => Err(( 94 94 StatusCode::BAD_REQUEST, 95 - [( 96 - header::CONTENT_TYPE, 97 - HeaderValue::from_static("application/json"), 98 - )], 99 95 Json(json!({ 100 96 "error": "InvalidRequest", 101 97 "message": format!("failed to decode request: {}", err) ··· 107 103 XrpcMethod::Query => { 108 104 if let Some(path_query) = req.uri().path_and_query() { 109 105 let query = path_query.query().unwrap_or(""); 110 - let value: R::Request<'_> = serde_html_form::from_str::<R::Request<'_>>( 111 - query, 112 - ) 113 - .map_err(|e| { 114 - ( 115 - StatusCode::BAD_REQUEST, 116 - [( 117 - header::CONTENT_TYPE, 118 - HeaderValue::from_static("application/json"), 119 - )], 120 - Json(json!({ 121 - "error": "InvalidRequest", 122 - "message": format!("failed to decode request: {}", e) 123 - })), 124 - ) 125 - .into_response() 126 - })?; 106 + let value: R::Request<'_> = 107 + serde_html_form::from_str::<R::Request<'_>>(query).map_err(|e| { 108 + ( 109 + StatusCode::BAD_REQUEST, 110 + Json(json!({ 111 + "error": "InvalidRequest", 112 + "message": format!("failed to decode request: {}", e) 113 + })), 114 + ) 115 + .into_response() 116 + })?; 127 117 Ok(ExtractXrpc(value.into_static())) 128 118 } else { 129 119 Err(( 130 120 StatusCode::BAD_REQUEST, 131 - [( 132 - header::CONTENT_TYPE, 133 - HeaderValue::from_static("application/json"), 134 - )], 135 121 Json(json!({ 136 122 "error": "InvalidRequest", 137 - "message": "wrong nsid for wherever this ended up" 123 + "message": "wrong path" 138 124 })), 139 125 ) 140 126 .into_response()) ··· 143 129 } 144 130 } 145 131 } 146 - } 147 - 148 - #[derive(Debug, thiserror::Error, miette::Diagnostic)] 149 - pub enum XrpcRequestError { 150 - #[error("Unsupported encoding: {0}")] 151 - UnsupportedEncoding(String), 152 - #[error("JSON decode error: {0}")] 153 - JsonDecodeError(serde_json::Error), 154 - #[error("UTF-8 decode error: {0}")] 155 - Utf8DecodeError(std::string::FromUtf8Error), 156 132 } 157 133 158 134 /// Conversion trait to turn an XrpcEndpoint and a handler into an axum Router ··· 185 161 ) 186 162 } 187 163 } 164 + 165 + /// Axum-compatible Xrpc error wrapper 166 + /// 167 + /// Implements IntoResponse, and does some mildly opinionated mapping. 168 + /// 169 + /// Currently assumes that the internal xrpc errors are well-formed and 170 + /// compatible with [the spec](https://atproto.com/specs/xrpc#error-responses). 171 + #[derive(Debug, thiserror::Error, miette::Diagnostic)] 172 + #[error("Xrpc error: {error}")] 173 + pub struct XrpcErrorResponse<E> 174 + where 175 + E: std::error::Error + IntoStatic, 176 + { 177 + pub status: StatusCode, 178 + #[diagnostic_source] 179 + pub error: XrpcError<E>, 180 + } 181 + 182 + impl<E> XrpcErrorResponse<E> 183 + where 184 + E: std::error::Error + IntoStatic + serde::Serialize, 185 + { 186 + /// Creates a new XrpcErrorResponse from the given status code and error. 187 + pub fn new(status: StatusCode, error: XrpcError<E>) -> Self { 188 + Self { status, error } 189 + } 190 + 191 + /// Changes the status code of the error response. 192 + pub fn with_status(self, status: StatusCode) -> Self { 193 + Self { 194 + status, 195 + error: self.error, 196 + } 197 + } 198 + } 199 + 200 + impl<E> IntoResponse for XrpcErrorResponse<E> 201 + where 202 + E: std::error::Error + IntoStatic + serde::Serialize, 203 + { 204 + fn into_response(self) -> Response { 205 + let (status, json) = match self.error { 206 + XrpcError::Xrpc(error) => ( 207 + self.status, 208 + serde_json::to_value(&error).unwrap_or(json!({ 209 + "error": "InternalError", 210 + "message": format!("{error}") 211 + })), 212 + ), 213 + XrpcError::Auth(auth_error) => ( 214 + self.status, 215 + json!({ 216 + "error": "Authentication", 217 + "message": format!("{auth_error}") 218 + }), 219 + ), 220 + XrpcError::Generic(generic) => ( 221 + self.status, 222 + serde_json::to_value(&generic).unwrap_or(json!({ 223 + "error": "InternalError", 224 + "message": format!("{generic}", ) 225 + })), 226 + ), 227 + XrpcError::Decode(error) => ( 228 + self.status, 229 + json!({ 230 + "error": "InvalidRequest", 231 + "message": format!("failed to decode request: {error}", ) 232 + }), 233 + ), 234 + }; 235 + (status, Json(json)).into_response() 236 + } 237 + } 238 + 239 + impl<E> From<XrpcError<E>> for XrpcErrorResponse<E> 240 + where 241 + E: std::error::Error + IntoStatic, 242 + { 243 + fn from(value: XrpcError<E>) -> Self { 244 + Self { 245 + status: StatusCode::INTERNAL_SERVER_ERROR, 246 + error: value, 247 + } 248 + } 249 + } 250 + 251 + impl<E> From<XrpcErrorResponse<E>> for XrpcError<E> 252 + where 253 + E: std::error::Error + IntoStatic, 254 + { 255 + fn from(value: XrpcErrorResponse<E>) -> Self { 256 + value.error 257 + } 258 + }
+1 -2
examples/read_tangled_repo.rs
··· 1 1 use clap::Parser; 2 - use jacquard::api::sh_tangled::repo::Repo; 3 2 use jacquard::client::{AgentSessionExt, BasicClient}; 4 3 use jacquard::types::string::AtUri; 5 4 use jacquard_api::sh_tangled::repo::RepoRecord; ··· 24 23 // Create an unauthenticated agent for public record access 25 24 let agent = BasicClient::unauthenticated(); 26 25 27 - // Use Agent's get_record helper with the at:// URI 26 + // Use Agent's fetch_record helper with the at:// URI & marker struct 28 27 let output = agent.fetch_record(RepoRecord, uri).await?; 29 28 30 29 println!("Tangled Repository\n");