1use bytes::Bytes;
14use http::{
15 HeaderName, HeaderValue, Request, StatusCode,
16 header::{AUTHORIZATION, CONTENT_TYPE},
17};
18use serde::{Deserialize, Serialize};
19use smol_str::SmolStr;
20use std::fmt::{self, Debug};
21use std::{error::Error, marker::PhantomData};
22use url::Url;
23
24use crate::http_client::HttpClient;
25use crate::types::value::Data;
26use crate::{AuthorizationToken, error::AuthError};
27use crate::{CowStr, error::XrpcResult};
28use crate::{IntoStatic, error::DecodeError};
29use crate::{error::TransportError, types::value::RawData};
30
31#[derive(Debug, thiserror::Error, miette::Diagnostic)]
33pub enum EncodeError {
34 #[error("Failed to serialize query: {0}")]
36 Query(
37 #[from]
38 #[source]
39 serde_html_form::ser::Error,
40 ),
41 #[error("Failed to serialize JSON: {0}")]
43 Json(
44 #[from]
45 #[source]
46 serde_json::Error,
47 ),
48 #[error("Encoding error: {0}")]
50 Other(String),
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
55pub enum XrpcMethod {
56 Query,
58 Procedure(&'static str),
60}
61
62impl XrpcMethod {
63 pub const fn as_str(&self) -> &'static str {
65 match self {
66 Self::Query => "GET",
67 Self::Procedure(_) => "POST",
68 }
69 }
70
71 pub const fn body_encoding(&self) -> Option<&'static str> {
73 match self {
74 Self::Query => None,
75 Self::Procedure(enc) => Some(enc),
76 }
77 }
78}
79
80pub trait XrpcRequest<'de>: Serialize + Deserialize<'de> {
87 const NSID: &'static str;
89
90 const METHOD: XrpcMethod;
92
93 type Response: XrpcResp;
95
96 fn encode_body(&self) -> Result<Vec<u8>, EncodeError> {
100 Ok(serde_json::to_vec(self)?)
101 }
102
103 fn decode_body(body: &'de [u8]) -> Result<Box<Self>, DecodeError> {
107 let body: Self = serde_json::from_slice(body).map_err(|e| DecodeError::Json(e))?;
108
109 Ok(Box::new(body))
110 }
111}
112
113pub trait XrpcResp {
117 const NSID: &'static str;
119
120 const ENCODING: &'static str;
122
123 type Output<'de>: Deserialize<'de> + IntoStatic;
125
126 type Err<'de>: Error + Deserialize<'de> + IntoStatic;
128}
129
130pub trait XrpcEndpoint {
138 const PATH: &'static str;
140 const METHOD: XrpcMethod;
142 type Request<'de>: XrpcRequest<'de> + IntoStatic;
144 type Response: XrpcResp;
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
150pub struct GenericError<'a>(#[serde(borrow)] Data<'a>);
151
152impl<'de> fmt::Display for GenericError<'de> {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 self.0.fmt(f)
155 }
156}
157
158impl Error for GenericError<'_> {}
159
160impl IntoStatic for GenericError<'_> {
161 type Output = GenericError<'static>;
162 fn into_static(self) -> Self::Output {
163 GenericError(self.0.into_static())
164 }
165}
166
167#[derive(Debug, Default, Clone)]
169pub struct CallOptions<'a> {
170 pub auth: Option<AuthorizationToken<'a>>,
172 pub atproto_proxy: Option<CowStr<'a>>,
174 pub atproto_accept_labelers: Option<Vec<CowStr<'a>>>,
176 pub extra_headers: Vec<(HeaderName, HeaderValue)>,
178}
179
180impl IntoStatic for CallOptions<'_> {
181 type Output = CallOptions<'static>;
182
183 fn into_static(self) -> Self::Output {
184 CallOptions {
185 auth: self.auth.map(|auth| auth.into_static()),
186 atproto_proxy: self.atproto_proxy.map(|proxy| proxy.into_static()),
187 atproto_accept_labelers: self
188 .atproto_accept_labelers
189 .map(|labelers| labelers.into_static()),
190 extra_headers: self.extra_headers,
191 }
192 }
193}
194
195pub trait XrpcExt: HttpClient {
223 fn xrpc<'a>(&'a self, base: Url) -> XrpcCall<'a, Self>
225 where
226 Self: Sized,
227 {
228 XrpcCall {
229 client: self,
230 base,
231 opts: CallOptions::default(),
232 }
233 }
234}
235
236impl<T: HttpClient> XrpcExt for T {}
237
238pub trait XrpcClient: HttpClient {
240 fn base_uri(&self) -> Url;
242
243 fn opts(&self) -> impl Future<Output = CallOptions<'_>> {
245 async { CallOptions::default() }
246 }
247 fn send<'s, R>(
249 &self,
250 request: R,
251 ) -> impl Future<Output = XrpcResult<Response<<R as XrpcRequest<'s>>::Response>>>
252 where
253 R: XrpcRequest<'s>;
254}
255
256pub struct XrpcCall<'a, C: HttpClient> {
288 pub(crate) client: &'a C,
289 pub(crate) base: Url,
290 pub(crate) opts: CallOptions<'a>,
291}
292
293impl<'a, C: HttpClient> XrpcCall<'a, C> {
294 pub fn auth(mut self, token: AuthorizationToken<'a>) -> Self {
296 self.opts.auth = Some(token);
297 self
298 }
299 pub fn proxy(mut self, proxy: CowStr<'a>) -> Self {
301 self.opts.atproto_proxy = Some(proxy);
302 self
303 }
304 pub fn accept_labelers(mut self, labelers: Vec<CowStr<'a>>) -> Self {
306 self.opts.atproto_accept_labelers = Some(labelers);
307 self
308 }
309 pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
311 self.opts.extra_headers.push((name, value));
312 self
313 }
314 pub fn with_options(mut self, opts: CallOptions<'a>) -> Self {
316 self.opts = opts;
317 self
318 }
319
320 pub async fn send<'s, R>(
329 self,
330 request: &R,
331 ) -> XrpcResult<Response<<R as XrpcRequest<'s>>::Response>>
332 where
333 R: XrpcRequest<'s>,
334 {
335 let http_request = build_http_request(&self.base, request, &self.opts)
336 .map_err(crate::error::TransportError::from)?;
337
338 let http_response = self
339 .client
340 .send_http(http_request)
341 .await
342 .map_err(|e| crate::error::TransportError::Other(Box::new(e)))?;
343
344 let status = http_response.status();
345 if status.as_u16() == 401 {
348 if let Some(hv) = http_response.headers().get(http::header::WWW_AUTHENTICATE) {
349 return Err(crate::error::ClientError::Auth(
350 crate::error::AuthError::Other(hv.clone()),
351 ));
352 }
353 }
354 let buffer = Bytes::from(http_response.into_body());
355
356 if !status.is_success() && !matches!(status.as_u16(), 400 | 401) {
357 return Err(crate::error::HttpError {
358 status,
359 body: Some(buffer),
360 }
361 .into());
362 }
363
364 Ok(Response::new(buffer, status))
365 }
366}
367
368#[inline]
372pub fn process_response<Resp>(http_response: http::Response<Vec<u8>>) -> XrpcResult<Response<Resp>>
373where
374 Resp: XrpcResp,
375{
376 let status = http_response.status();
377 if status.as_u16() == 401 {
380 if let Some(hv) = http_response.headers().get(http::header::WWW_AUTHENTICATE) {
381 return Err(crate::error::ClientError::Auth(
382 crate::error::AuthError::Other(hv.clone()),
383 ));
384 }
385 }
386 let buffer = Bytes::from(http_response.into_body());
387
388 if !status.is_success() && !matches!(status.as_u16(), 400 | 401) {
389 return Err(crate::error::HttpError {
390 status,
391 body: Some(buffer),
392 }
393 .into());
394 }
395
396 Ok(Response::new(buffer, status))
397}
398
399pub enum Header {
401 ContentType,
403 Authorization,
405 AtprotoProxy,
409 AtprotoAcceptLabelers,
411}
412
413impl From<Header> for HeaderName {
414 fn from(value: Header) -> Self {
415 match value {
416 Header::ContentType => CONTENT_TYPE,
417 Header::Authorization => AUTHORIZATION,
418 Header::AtprotoProxy => HeaderName::from_static("atproto-proxy"),
419 Header::AtprotoAcceptLabelers => HeaderName::from_static("atproto-accept-labelers"),
420 }
421 }
422}
423
424pub fn build_http_request<'s, R>(
426 base: &Url,
427 req: &R,
428 opts: &CallOptions<'_>,
429) -> core::result::Result<Request<Vec<u8>>, crate::error::TransportError>
430where
431 R: XrpcRequest<'s>,
432{
433 let mut url = base.clone();
434 let mut path = url.path().trim_end_matches('/').to_owned();
435 path.push_str("/xrpc/");
436 path.push_str(<R as XrpcRequest<'s>>::NSID);
437 url.set_path(&path);
438
439 if let XrpcMethod::Query = <R as XrpcRequest<'s>>::METHOD {
440 let qs = serde_html_form::to_string(&req)
441 .map_err(|e| crate::error::TransportError::InvalidRequest(e.to_string()))?;
442 if !qs.is_empty() {
443 url.set_query(Some(&qs));
444 } else {
445 url.set_query(None);
446 }
447 }
448
449 let method = match <R as XrpcRequest<'_>>::METHOD {
450 XrpcMethod::Query => http::Method::GET,
451 XrpcMethod::Procedure(_) => http::Method::POST,
452 };
453
454 let mut builder = Request::builder().method(method).uri(url.as_str());
455
456 if let XrpcMethod::Procedure(encoding) = <R as XrpcRequest<'_>>::METHOD {
457 builder = builder.header(Header::ContentType, encoding);
458 }
459 let output_encoding = <R::Response as XrpcResp>::ENCODING;
460 builder = builder.header(http::header::ACCEPT, output_encoding);
461
462 if let Some(token) = &opts.auth {
463 let hv = match token {
464 AuthorizationToken::Bearer(t) => {
465 HeaderValue::from_str(&format!("Bearer {}", t.as_ref()))
466 }
467 AuthorizationToken::Dpop(t) => HeaderValue::from_str(&format!("DPoP {}", t.as_ref())),
468 }
469 .map_err(|e| {
470 TransportError::InvalidRequest(format!("Invalid authorization token: {}", e))
471 })?;
472 builder = builder.header(Header::Authorization, hv);
473 }
474
475 if let Some(proxy) = &opts.atproto_proxy {
476 builder = builder.header(Header::AtprotoProxy, proxy.as_ref());
477 }
478 if let Some(labelers) = &opts.atproto_accept_labelers {
479 if !labelers.is_empty() {
480 let joined = labelers
481 .iter()
482 .map(|s| s.as_ref())
483 .collect::<Vec<_>>()
484 .join(", ");
485 builder = builder.header(Header::AtprotoAcceptLabelers, joined);
486 }
487 }
488 for (name, value) in &opts.extra_headers {
489 builder = builder.header(name, value);
490 }
491
492 let body = if let XrpcMethod::Procedure(_) = R::METHOD {
493 req.encode_body()
494 .map_err(|e| TransportError::InvalidRequest(e.to_string()))?
495 } else {
496 vec![]
497 };
498
499 builder
500 .body(body)
501 .map_err(|e| TransportError::InvalidRequest(e.to_string()))
502}
503
504pub struct Response<Resp>
509where
510 Resp: XrpcResp, {
512 _marker: PhantomData<fn() -> Resp>,
513 buffer: Bytes,
514 status: StatusCode,
515}
516
517impl<Resp> Response<Resp>
518where
519 Resp: XrpcResp,
520{
521 pub fn new(buffer: Bytes, status: StatusCode) -> Self {
523 Self {
524 buffer,
525 status,
526 _marker: PhantomData,
527 }
528 }
529
530 pub fn status(&self) -> StatusCode {
532 self.status
533 }
534
535 pub fn buffer(&self) -> &Bytes {
537 &self.buffer
538 }
539
540 pub fn parse<'s>(
542 &'s self,
543 ) -> Result<<Resp as XrpcResp>::Output<'s>, XrpcError<<Resp as XrpcResp>::Err<'s>>> {
544 if self.status.is_success() {
546 match serde_json::from_slice::<_>(&self.buffer) {
547 Ok(output) => Ok(output),
548 Err(e) => Err(XrpcError::Decode(e)),
549 }
550 } else if self.status.as_u16() == 400 {
552 match serde_json::from_slice::<_>(&self.buffer) {
553 Ok(error) => Err(XrpcError::Xrpc(error)),
554 Err(_) => {
555 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) {
557 Ok(mut generic) => {
558 generic.nsid = Resp::NSID;
559 generic.method = ""; generic.http_status = self.status;
561 match generic.error.as_str() {
563 "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)),
564 "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)),
565 _ => Err(XrpcError::Generic(generic)),
566 }
567 }
568 Err(e) => Err(XrpcError::Decode(e)),
569 }
570 }
571 }
572 } else {
574 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) {
575 Ok(mut generic) => {
576 generic.nsid = Resp::NSID;
577 generic.method = ""; generic.http_status = self.status;
579 match generic.error.as_str() {
580 "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)),
581 "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)),
582 _ => Err(XrpcError::Auth(AuthError::NotAuthenticated)),
583 }
584 }
585 Err(e) => Err(XrpcError::Decode(e)),
586 }
587 }
588 }
589
590 pub fn parse_data<'s>(&'s self) -> Result<Data<'s>, XrpcError<<Resp as XrpcResp>::Err<'s>>> {
594 if self.status.is_success() {
596 match serde_json::from_slice::<_>(&self.buffer) {
597 Ok(output) => Ok(output),
598 Err(e) => Err(XrpcError::Decode(e)),
599 }
600 } else if self.status.as_u16() == 400 {
602 match serde_json::from_slice::<_>(&self.buffer) {
603 Ok(error) => Err(XrpcError::Xrpc(error)),
604 Err(_) => {
605 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) {
607 Ok(mut generic) => {
608 generic.nsid = Resp::NSID;
609 generic.method = ""; generic.http_status = self.status;
611 match generic.error.as_str() {
613 "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)),
614 "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)),
615 _ => Err(XrpcError::Generic(generic)),
616 }
617 }
618 Err(e) => Err(XrpcError::Decode(e)),
619 }
620 }
621 }
622 } else {
624 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) {
625 Ok(mut generic) => {
626 generic.nsid = Resp::NSID;
627 generic.method = ""; generic.http_status = self.status;
629 match generic.error.as_str() {
630 "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)),
631 "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)),
632 _ => Err(XrpcError::Auth(AuthError::NotAuthenticated)),
633 }
634 }
635 Err(e) => Err(XrpcError::Decode(e)),
636 }
637 }
638 }
639
640 pub fn parse_raw<'s>(&'s self) -> Result<RawData<'s>, XrpcError<<Resp as XrpcResp>::Err<'s>>> {
644 if self.status.is_success() {
646 match serde_json::from_slice::<_>(&self.buffer) {
647 Ok(output) => Ok(output),
648 Err(e) => Err(XrpcError::Decode(e)),
649 }
650 } else if self.status.as_u16() == 400 {
652 match serde_json::from_slice::<_>(&self.buffer) {
653 Ok(error) => Err(XrpcError::Xrpc(error)),
654 Err(_) => {
655 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) {
657 Ok(mut generic) => {
658 generic.nsid = Resp::NSID;
659 generic.method = ""; generic.http_status = self.status;
661 match generic.error.as_str() {
663 "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)),
664 "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)),
665 _ => Err(XrpcError::Generic(generic)),
666 }
667 }
668 Err(e) => Err(XrpcError::Decode(e)),
669 }
670 }
671 }
672 } else {
674 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) {
675 Ok(mut generic) => {
676 generic.nsid = Resp::NSID;
677 generic.method = ""; generic.http_status = self.status;
679 match generic.error.as_str() {
680 "ExpiredToken" => Err(XrpcError::Auth(AuthError::TokenExpired)),
681 "InvalidToken" => Err(XrpcError::Auth(AuthError::InvalidToken)),
682 _ => Err(XrpcError::Auth(AuthError::NotAuthenticated)),
683 }
684 }
685 Err(e) => Err(XrpcError::Decode(e)),
686 }
687 }
688 }
689}
690
691impl<Resp> Response<Resp>
692where
693 Resp: XrpcResp,
694{
695 pub fn into_output(
697 self,
698 ) -> Result<<Resp as XrpcResp>::Output<'static>, XrpcError<<Resp as XrpcResp>::Err<'static>>>
699 where
700 for<'a> <Resp as XrpcResp>::Output<'a>:
701 IntoStatic<Output = <Resp as XrpcResp>::Output<'static>>,
702 for<'a> <Resp as XrpcResp>::Err<'a>: IntoStatic<Output = <Resp as XrpcResp>::Err<'static>>,
703 {
704 fn parse_output<'b, R: XrpcResp>(
706 buffer: &'b [u8],
707 ) -> Result<R::Output<'b>, serde_json::Error> {
708 serde_json::from_slice(buffer)
709 }
710
711 fn parse_error<'b, R: XrpcResp>(buffer: &'b [u8]) -> Result<R::Err<'b>, serde_json::Error> {
712 serde_json::from_slice(buffer)
713 }
714
715 if self.status.is_success() {
717 match parse_output::<Resp>(&self.buffer) {
718 Ok(output) => {
719 return Ok(output.into_static());
720 }
721 Err(e) => Err(XrpcError::Decode(e)),
722 }
723 } else if self.status.as_u16() == 400 {
725 let error = match parse_error::<Resp>(&self.buffer) {
726 Ok(error) => XrpcError::Xrpc(error),
727 Err(_) => {
728 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) {
730 Ok(mut generic) => {
731 generic.nsid = Resp::NSID;
732 generic.method = ""; generic.http_status = self.status;
734 match generic.error.as_ref() {
736 "ExpiredToken" => XrpcError::Auth(AuthError::TokenExpired),
737 "InvalidToken" => XrpcError::Auth(AuthError::InvalidToken),
738 _ => XrpcError::Generic(generic),
739 }
740 }
741 Err(e) => XrpcError::Decode(e),
742 }
743 }
744 };
745 Err(error.into_static())
746 } else {
748 let error: XrpcError<<Resp as XrpcResp>::Err<'_>> =
749 match serde_json::from_slice::<GenericXrpcError>(&self.buffer) {
750 Ok(mut generic) => {
751 let status = self.status;
752 generic.nsid = Resp::NSID;
753 generic.method = ""; generic.http_status = status;
755 match generic.error.as_ref() {
756 "ExpiredToken" => XrpcError::Auth(AuthError::TokenExpired),
757 "InvalidToken" => XrpcError::Auth(AuthError::InvalidToken),
758 _ => XrpcError::Auth(AuthError::NotAuthenticated),
759 }
760 }
761 Err(e) => XrpcError::Decode(e),
762 };
763
764 Err(error.into_static())
765 }
766 }
767}
768
769#[derive(Debug, Clone, Deserialize, Serialize)]
773pub struct GenericXrpcError {
774 pub error: SmolStr,
776 pub message: Option<SmolStr>,
778 #[serde(skip)]
780 pub nsid: &'static str,
781 #[serde(skip)]
783 pub method: &'static str,
784 #[serde(skip)]
786 pub http_status: StatusCode,
787}
788
789impl std::fmt::Display for GenericXrpcError {
790 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
791 if let Some(msg) = &self.message {
792 write!(
793 f,
794 "{}: {} (nsid={}, method={}, status={})",
795 self.error, msg, self.nsid, self.method, self.http_status
796 )
797 } else {
798 write!(
799 f,
800 "{} (nsid={}, method={}, status={})",
801 self.error, self.nsid, self.method, self.http_status
802 )
803 }
804 }
805}
806
807impl IntoStatic for GenericXrpcError {
808 type Output = Self;
809
810 fn into_static(self) -> Self::Output {
811 self
812 }
813}
814
815impl std::error::Error for GenericXrpcError {}
816
817#[derive(Debug, thiserror::Error, miette::Diagnostic)]
822pub enum XrpcError<E: std::error::Error + IntoStatic> {
823 #[error("XRPC error: {0}")]
825 #[diagnostic(code(jacquard_common::xrpc::typed))]
826 Xrpc(E),
827
828 #[error("Authentication error: {0}")]
830 #[diagnostic(code(jacquard_common::xrpc::auth))]
831 Auth(#[from] AuthError),
832
833 #[error("XRPC error: {0}")]
835 #[diagnostic(code(jacquard_common::xrpc::generic))]
836 Generic(GenericXrpcError),
837
838 #[error("Failed to decode response: {0}")]
840 #[diagnostic(code(jacquard_common::xrpc::decode))]
841 Decode(#[from] serde_json::Error),
842}
843
844impl<E> IntoStatic for XrpcError<E>
845where
846 E: std::error::Error + IntoStatic,
847 E::Output: std::error::Error + IntoStatic,
848 <E as IntoStatic>::Output: std::error::Error + IntoStatic,
849{
850 type Output = XrpcError<E::Output>;
851 fn into_static(self) -> Self::Output {
852 match self {
853 XrpcError::Xrpc(e) => XrpcError::Xrpc(e.into_static()),
854 XrpcError::Auth(e) => XrpcError::Auth(e.into_static()),
855 XrpcError::Generic(e) => XrpcError::Generic(e),
856 XrpcError::Decode(e) => XrpcError::Decode(e),
857 }
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864 use serde::{Deserialize, Serialize};
865
866 #[derive(Serialize, Deserialize)]
867 #[allow(dead_code)]
868 struct DummyReq;
869
870 #[derive(Deserialize, Debug, thiserror::Error)]
871 #[error("{0}")]
872 struct DummyErr<'a>(#[serde(borrow)] CowStr<'a>);
873
874 impl IntoStatic for DummyErr<'_> {
875 type Output = DummyErr<'static>;
876 fn into_static(self) -> Self::Output {
877 DummyErr(self.0.into_static())
878 }
879 }
880
881 struct DummyResp;
882
883 impl XrpcResp for DummyResp {
884 const NSID: &'static str = "test.dummy";
885 const ENCODING: &'static str = "application/json";
886 type Output<'de> = ();
887 type Err<'de> = DummyErr<'de>;
888 }
889
890 impl<'de> XrpcRequest<'de> for DummyReq {
891 const NSID: &'static str = "test.dummy";
892 const METHOD: XrpcMethod = XrpcMethod::Procedure("application/json");
893 type Response = DummyResp;
894 }
895
896 #[test]
897 fn generic_error_carries_context() {
898 let body = serde_json::json!({"error":"InvalidRequest","message":"missing"});
899 let buf = Bytes::from(serde_json::to_vec(&body).unwrap());
900 let resp: Response<DummyResp> = Response::new(buf, StatusCode::BAD_REQUEST);
901 match resp.parse().unwrap_err() {
902 XrpcError::Generic(g) => {
903 assert_eq!(g.error.as_str(), "InvalidRequest");
904 assert_eq!(g.message.as_deref(), Some("missing"));
905 assert_eq!(g.nsid, DummyResp::NSID);
906 assert_eq!(g.method, ""); assert_eq!(g.http_status, StatusCode::BAD_REQUEST);
908 }
909 other => panic!("unexpected: {other:?}"),
910 }
911 }
912
913 #[test]
914 fn auth_error_mapping() {
915 for (code, expect) in [
916 ("ExpiredToken", AuthError::TokenExpired),
917 ("InvalidToken", AuthError::InvalidToken),
918 ] {
919 let body = serde_json::json!({"error": code});
920 let buf = Bytes::from(serde_json::to_vec(&body).unwrap());
921 let resp: Response<DummyResp> = Response::new(buf, StatusCode::UNAUTHORIZED);
922 match resp.parse().unwrap_err() {
923 XrpcError::Auth(e) => match (e, expect) {
924 (AuthError::TokenExpired, AuthError::TokenExpired) => {}
925 (AuthError::InvalidToken, AuthError::InvalidToken) => {}
926 other => panic!("mismatch: {other:?}"),
927 },
928 other => panic!("unexpected: {other:?}"),
929 }
930 }
931 }
932
933 #[test]
934 fn no_double_slash_in_path() {
935 #[derive(Serialize, Deserialize)]
936 struct Req;
937 #[derive(Deserialize, Debug, thiserror::Error)]
938 #[error("{0}")]
939 struct Err<'a>(#[serde(borrow)] CowStr<'a>);
940 impl IntoStatic for Err<'_> {
941 type Output = Err<'static>;
942 fn into_static(self) -> Self::Output {
943 Err(self.0.into_static())
944 }
945 }
946 struct Resp;
947 impl XrpcResp for Resp {
948 const NSID: &'static str = "com.example.test";
949 const ENCODING: &'static str = "application/json";
950 type Output<'de> = ();
951 type Err<'de> = Err<'de>;
952 }
953 impl<'de> XrpcRequest<'de> for Req {
954 const NSID: &'static str = "com.example.test";
955 const METHOD: XrpcMethod = XrpcMethod::Query;
956 type Response = Resp;
957 }
958
959 let opts = CallOptions::default();
960 for base in [
961 Url::parse("https://pds").unwrap(),
962 Url::parse("https://pds/").unwrap(),
963 Url::parse("https://pds/base/").unwrap(),
964 ] {
965 let req = build_http_request(&base, &Req, &opts).unwrap();
966 let uri = req.uri().to_string();
967 assert!(uri.contains("/xrpc/com.example.test"));
968 assert!(!uri.contains("//xrpc"));
969 }
970 }
971}