Skip to main content

typeway_client/
call.rs

1//! The [`CallEndpoint`] trait — type-safe endpoint invocation.
2
3use serde::de::DeserializeOwned;
4use serde::Serialize;
5
6use typeway_core::*;
7
8use crate::error::ClientError;
9
10/// Describes how to call a specific endpoint: what arguments it needs,
11/// how to build the URL, and how to parse the response.
12pub trait CallEndpoint {
13    /// The arguments needed to call this endpoint.
14    type Args;
15
16    /// The response type returned on success.
17    type Response;
18
19    /// The HTTP method.
20    fn method() -> http::Method;
21
22    /// Build the URL path by substituting captures into the pattern.
23    fn build_path(args: &Self::Args) -> String;
24
25    /// Serialize the request body, if any.
26    fn request_body(args: &Self::Args) -> Option<Result<Vec<u8>, ClientError>>;
27
28    /// Deserialize the response body.
29    fn parse_response(bytes: &[u8]) -> Result<Self::Response, ClientError>;
30}
31
32// ---------------------------------------------------------------------------
33// BuildPath: URL construction from capture tuples
34// ---------------------------------------------------------------------------
35
36/// Builds a URL path by substituting capture values into `{}` placeholders.
37pub trait BuildPath {
38    fn build_path(captures: &Self, pattern: &str) -> String;
39}
40
41impl BuildPath for () {
42    fn build_path(_: &(), pattern: &str) -> String {
43        pattern.to_string()
44    }
45}
46
47macro_rules! impl_build_path {
48    ($($idx:tt : $T:ident),+) => {
49        impl<$($T: std::fmt::Display,)+> BuildPath for ($($T,)+) {
50            fn build_path(captures: &Self, pattern: &str) -> String {
51                let mut result = pattern.to_string();
52                $(
53                    result = result.replacen("{}", &captures.$idx.to_string(), 1);
54                )+
55                result
56            }
57        }
58    };
59}
60
61impl_build_path!(0: A);
62impl_build_path!(0: A, 1: B);
63impl_build_path!(0: A, 1: B, 2: C);
64impl_build_path!(0: A, 1: B, 2: C, 3: D);
65impl_build_path!(0: A, 1: B, 2: C, 3: D, 4: E);
66impl_build_path!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F);
67impl_build_path!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G);
68impl_build_path!(0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H);
69
70// ---------------------------------------------------------------------------
71// Bodyless endpoints (GET, DELETE, HEAD, OPTIONS)
72// ---------------------------------------------------------------------------
73
74macro_rules! impl_call_bodyless {
75    ($Method:ty) => {
76        impl<P, Res, Q, Err> CallEndpoint for Endpoint<$Method, P, NoBody, Res, Q, Err>
77        where
78            P: PathSpec + ExtractPath,
79            P::Captures: BuildPath,
80            Res: DeserializeOwned,
81        {
82            type Args = P::Captures;
83            type Response = Res;
84
85            fn method() -> http::Method {
86                <$Method as HttpMethod>::METHOD
87            }
88
89            fn build_path(args: &Self::Args) -> String {
90                BuildPath::build_path(args, &P::pattern())
91            }
92
93            fn request_body(_args: &Self::Args) -> Option<Result<Vec<u8>, ClientError>> {
94                None
95            }
96
97            fn parse_response(bytes: &[u8]) -> Result<Self::Response, ClientError> {
98                serde_json::from_slice(bytes).map_err(|e| ClientError::Deserialize(e.to_string()))
99            }
100        }
101    };
102}
103
104impl_call_bodyless!(Get);
105impl_call_bodyless!(Delete);
106impl_call_bodyless!(Head);
107impl_call_bodyless!(Options);
108
109// ---------------------------------------------------------------------------
110// Body endpoints (POST, PUT, PATCH)
111// Args = (Captures, RequestBody)
112// ---------------------------------------------------------------------------
113
114macro_rules! impl_call_with_body {
115    ($Method:ty) => {
116        impl<P, Req, Res, Q, Err> CallEndpoint for Endpoint<$Method, P, Req, Res, Q, Err>
117        where
118            P: PathSpec + ExtractPath,
119            P::Captures: BuildPath,
120            Req: Serialize,
121            Res: DeserializeOwned,
122        {
123            type Args = (P::Captures, Req);
124            type Response = Res;
125
126            fn method() -> http::Method {
127                <$Method as HttpMethod>::METHOD
128            }
129
130            fn build_path(args: &Self::Args) -> String {
131                BuildPath::build_path(&args.0, &P::pattern())
132            }
133
134            fn request_body(args: &Self::Args) -> Option<Result<Vec<u8>, ClientError>> {
135                Some(serde_json::to_vec(&args.1).map_err(|e| ClientError::Serialize(e.to_string())))
136            }
137
138            fn parse_response(bytes: &[u8]) -> Result<Self::Response, ClientError> {
139                serde_json::from_slice(bytes).map_err(|e| ClientError::Deserialize(e.to_string()))
140            }
141        }
142    };
143}
144
145impl_call_with_body!(Post);
146impl_call_with_body!(Put);
147impl_call_with_body!(Patch);