resolvedid.rs
1use std::{error::Error, ops::Deref};
2
3use axum::{Json, Router, extract::FromRequestParts, http::StatusCode, response::IntoResponse};
4
5use jacquard::{
6 IntoStatic,
7 identity::resolver::{self, IdentityError, IdentityResolver},
8 types::value::Data,
9};
10use jacquard_api::com_atproto::identity::resolve_did::{
11 ResolveDid, ResolveDidError, ResolveDidOutput,
12};
13use jacquard_common::types::xrpc::{XrpcMethod, XrpcRequest};
14use miette::{IntoDiagnostic, Result};
15use serde::{Deserialize, Serialize};
16use tracing_subscriber::EnvFilter;
17
18trait IntoRouter {
19 fn into_router<T, S, U>(handler: U) -> Router<S>
20 where
21 T: 'static,
22 S: Clone + Send + Sync + 'static,
23 U: axum::handler::Handler<T, S>;
24}
25
26impl<X: XrpcRequest> IntoRouter for X {
27 /// Creates an axum router that will invoke `handler` in response to xrpc
28 /// request `X`.
29 fn into_router<T, S, U>(handler: U) -> Router<S>
30 where
31 T: 'static,
32 S: Clone + Send + Sync + 'static,
33 U: axum::handler::Handler<T, S>,
34 {
35 Router::new().route(
36 // this should really be a compile time operation, but I cannot
37 // figure out how to make that happen
38 format!("/xrpc/{}", X::NSID).as_str(),
39 (match X::METHOD {
40 XrpcMethod::Query => axum::routing::get,
41 XrpcMethod::Procedure(_) => axum::routing::put,
42 })(handler),
43 )
44 }
45}
46
47struct XrpcArgs<T>(T);
48impl<T> Deref for XrpcArgs<T> {
49 type Target = T;
50
51 fn deref(&self) -> &Self::Target {
52 &self.0
53 }
54}
55
56#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, thiserror::Error)]
57#[serde(tag = "error", content = "message")]
58pub enum AtProtoGenericError {
59 #[error("Invalid Request")]
60 InvalidRequest(Option<String>),
61 #[error("Expired Token")]
62 ExpiredToken(Option<String>),
63 #[error("Invalid Token")]
64 InvalidToken(Option<String>),
65}
66
67#[derive(thiserror::Error, Debug)]
68#[error(transparent)]
69struct XrpcError<T: std::error::Error>(pub T);
70
71impl<T: Serialize + Error> IntoResponse for XrpcError<T> {
72 fn into_response(self) -> axum::response::Response {
73 (StatusCode::BAD_REQUEST, Json(self.0)).into_response()
74 }
75}
76
77impl From<AtProtoGenericError> for XrpcError<AtProtoGenericError> {
78 fn from(value: AtProtoGenericError) -> Self {
79 Self(value)
80 }
81}
82impl<'a> From<ResolveDidError<'a>> for XrpcError<ResolveDidError<'static>> {
83 fn from(value: ResolveDidError) -> Self {
84 Self(value.into_static())
85 }
86}
87
88impl From<IdentityError> for XrpcError<ResolveDidError<'static>> {
89 fn from(value: IdentityError) -> Self {
90 Self(ResolveDidError::DidNotFound(Some(format!("{}", value))))
91 }
92}
93
94struct XrpcResponse<T>(T);
95
96impl<T: Serialize> IntoResponse for XrpcResponse<T> {
97 fn into_response(self) -> axum::response::Response {
98 Json(self.0).into_response()
99 }
100}
101
102impl<'a> From<ResolveDidOutput<'a>> for XrpcResponse<ResolveDidOutput<'static>> {
103 fn from(value: ResolveDidOutput<'a>) -> Self {
104 Self(value.into_static())
105 }
106}
107
108impl From<serde_html_form::de::Error> for XrpcError<AtProtoGenericError> {
109 fn from(_: serde_html_form::de::Error) -> Self {
110 AtProtoGenericError::InvalidRequest(None).into()
111 }
112}
113
114impl<S: Send + Sync> FromRequestParts<S> for XrpcArgs<ResolveDid<'static>> {
115 type Rejection = XrpcError<AtProtoGenericError>;
116
117 async fn from_request_parts(
118 parts: &mut axum::http::request::Parts,
119 _state: &S,
120 ) -> std::result::Result<Self, Self::Rejection> {
121 let q = parts.uri.query().unwrap_or_default();
122 let de = serde_html_form::Deserializer::new(url::form_urlencoded::parse(q.as_bytes()));
123 let value = ResolveDid::deserialize(de)?;
124 Ok(Self(value.into_static()))
125 }
126}
127
128#[tokio::main]
129async fn main() -> Result<()> {
130 tracing_subscriber::fmt()
131 .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339())
132 .with_env_filter(EnvFilter::from_env("QDPDS_LOG"))
133 .init();
134 let app =
135 Router::new()
136 .route("/", axum::routing::get(|| async { "hello world!" }))
137 .merge(ResolveDid::into_router(
138 async move |XrpcArgs(args): XrpcArgs<ResolveDid<'static>>| -> Result<
139 XrpcResponse<ResolveDidOutput>,
140 XrpcError<ResolveDidError>,
141 > {
142 let res = resolver::slingshot_resolver_default();
143 let doc = res.resolve_did_doc(&args.did).await?;
144 let valid_doc = doc.parse()?;
145 let doc_value = serde_json::to_value(valid_doc).unwrap();
146 Ok(ResolveDidOutput {
147 did_doc: Data::from_json(&doc_value).unwrap().into_static(),
148 extra_data: Default::default(),
149 }
150 .into())
151 },
152 ))
153 .layer(tower_http::trace::TraceLayer::new_for_http());
154 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
155 .await
156 .into_diagnostic()?;
157 axum::serve(listener, app).await.unwrap();
158 Ok(())
159}