1use futures_util::StreamExt;
2use itertools::Itertools;
3
4use crate::{
5 targets::{js, ts},
6 transitive_closure, DynTapi, Tapi,
7};
8
9#[derive(Debug)]
10pub enum Method {
11 Get,
12 Post,
13 Put,
14 Delete,
15}
16impl Method {
17 pub fn from_str(s: &str) -> Option<Self> {
18 match s {
19 "GET" => Some(Self::Get),
20 "POST" => Some(Self::Post),
21 "PUT" => Some(Self::Put),
22 "DELETE" => Some(Self::Delete),
23 _ => None,
24 }
25 }
26 pub fn as_str(&self) -> &'static str {
27 match self {
28 Self::Get => "GET",
29 Self::Post => "POST",
30 Self::Put => "PUT",
31 Self::Delete => "DELETE",
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy)]
37pub enum RequestStructureBody {
38 Query(DynTapi),
39 Json(DynTapi),
40 PlainText,
41}
42#[derive(Debug)]
43pub struct RequestStructure {
44 pub path: Option<DynTapi>,
45 pub method: Method,
46 pub body: Option<RequestStructureBody>,
47}
48
49impl RequestStructure {
50 pub fn new(method: Method) -> Self {
51 Self {
52 path: None,
53 method,
54 body: None,
55 }
56 }
57 pub fn merge_with(&mut self, req: RequestTapi) {
58 match req {
59 RequestTapi::Path(ty) => {
60 self.path = Some(ty);
61 }
62 RequestTapi::Query(ty) => {
63 self.body = Some(RequestStructureBody::Query(ty));
64 }
65 RequestTapi::Json(ty) => {
66 self.body = Some(RequestStructureBody::Json(ty));
67 }
68 RequestTapi::None => {}
69 }
70 }
71}
72pub trait Endpoint<AppState> {
73 fn path(&self) -> &'static str;
74 fn method(&self) -> Method;
75 fn bind_to(&self, router: axum::Router<AppState>) -> axum::Router<AppState>;
76 fn body(&self) -> RequestStructure;
77 fn res(&self) -> ResponseTapi;
78 fn tys(&self) -> Vec<DynTapi> {
79 let mut tys = Vec::new();
80 if let Some(path) = self.body().path {
81 tys.push(path);
82 }
83 if let Some(body) = self.body().body {
84 match body {
85 RequestStructureBody::Query(ty) => {
86 tys.push(ty);
87 }
88 RequestStructureBody::Json(ty) => {
89 tys.push(ty);
90 }
91 RequestStructureBody::PlainText => {}
92 }
93 }
94 tys.push(self.res().ty());
95 tys
96 }
97 fn ts_client(&self) -> String {
110 use std::fmt::Write;
111 let mut s = String::new();
112 match (self.body(), self.res()) {
113 (
114 RequestStructure {
115 body: None, path, ..
116 },
117 ResponseTapi::Sse(ty),
118 ) => {
119 let mut params = Vec::new();
120 let final_path = self
121 .path()
122 .split('/')
123 .filter(|p| !p.is_empty())
124 .map(|p| {
125 if let Some(name) = p.strip_prefix(':') {
126 params.push(name);
127 format!("/${{{name}}}")
128 } else {
129 format!("/{p}")
130 }
131 })
132 .join("");
133 let final_path = format!("`{final_path}`");
134 if let Some(path_param) = path {
135 write!(
136 s,
137 "sse<[{}], {}>(({}) => {final_path}, \"json\")",
138 ts::full_ty_name(path_param),
139 ts::full_ty_name(ty),
140 params.iter().format(", "),
141 )
142 .unwrap();
143 } else {
144 write!(
146 s,
147 "sse<[{}], {}>(({}) => {final_path}, \"json\")",
148 "",
149 ts::full_ty_name(ty),
150 params.iter().format(", "),
151 )
152 .unwrap();
153 }
154 }
155 (RequestStructure { body, .. }, res) => {
156 write!(
157 s,
158 "request<{}, {}>({:?}, {:?}, {:?}, {:?})",
159 match body {
160 Some(RequestStructureBody::Query(ty)) => ts::full_ty_name(ty),
161 Some(RequestStructureBody::Json(ty)) => ts::full_ty_name(ty),
162 Some(RequestStructureBody::PlainText) =>
164 "Record<string, never>".to_string(),
165 None => "Record<string, never>".to_string(),
166 },
167 ts::full_ty_name(res.ty()),
168 match body {
169 Some(RequestStructureBody::Query(_)) => "query",
170 Some(RequestStructureBody::Json(_)) => "json",
171 Some(RequestStructureBody::PlainText) => "none",
172 None => "none",
173 },
174 self.method().as_str(),
175 self.path(),
176 match res {
177 ResponseTapi::PlainText => "text",
178 ResponseTapi::Bytes => "bytes",
179 ResponseTapi::Json(_) => "json",
180 ResponseTapi::Html => "html",
181 ResponseTapi::Sse(_) => "sse",
182 ResponseTapi::None => "none",
183 }
184 )
185 .unwrap();
186 }
187 }
188 s
189 }
190 fn js_client(&self) -> String {
191 use std::fmt::Write;
192 let mut s = String::new();
193 match (self.body(), self.res()) {
194 (
195 RequestStructure {
196 body: None, path, ..
197 },
198 ResponseTapi::Sse(ty),
199 ) => {
200 let mut params = Vec::new();
201 let final_path = self
202 .path()
203 .split('/')
204 .filter(|p| !p.is_empty())
205 .map(|p| {
206 if let Some(name) = p.strip_prefix(':') {
207 params.push(name);
208 format!("/${{{name}}}")
209 } else {
210 format!("/{p}")
211 }
212 })
213 .join("");
214 let final_path = format!("`{final_path}`");
215 if let Some(path_param) = path {
216 write!(
217 s,
218 "/** @type {{ReturnType<typeof sse<[{}], {}>>}} */ (\n sse(({}) => {final_path}, \"json\")\n )",
219 ts::full_ty_name(path_param),
220 ts::full_ty_name(ty),
221 params.iter().format(", "),
222 )
223 .unwrap();
224 } else {
225 write!(
227 s,
228 "/** @type {{ReturnType<typeof sse<[{}], {}>>}} */ (\n sse(({}) => {final_path}, \"json\")\n )",
229 "",
230 ts::full_ty_name(ty),
231 params.iter().format(", "),
232 )
233 .unwrap();
234 }
235 }
236 (RequestStructure { body, .. }, res) => {
237 write!(
238 s,
239 "/** @type {{ReturnType<typeof request<{}, {}>>}} */ (\n request({:?}, {:?}, {:?}, {:?})\n )",
240 match body {
241 Some(RequestStructureBody::Query(ty)) => ts::full_ty_name(ty),
242 Some(RequestStructureBody::Json(ty)) => ts::full_ty_name(ty),
243 Some(RequestStructureBody::PlainText) =>
245 "Record<string, never>".to_string(),
246 None => "Record<string, never>".to_string(),
247 },
248 ts::full_ty_name(res.ty()),
249 match body {
250 Some(RequestStructureBody::Query(_)) => "query",
251 Some(RequestStructureBody::Json(_)) => "json",
252 Some(RequestStructureBody::PlainText) => "none",
253 None => "none",
254 },
255 self.method().as_str(),
256 self.path(),
257 match res {
258 ResponseTapi::PlainText => "text",
259 ResponseTapi::Bytes => "bytes",
260 ResponseTapi::Json(_) => "json",
261 ResponseTapi::Html => "html",
262 ResponseTapi::Sse(_) => "sse",
263 ResponseTapi::None => "none",
264 }
265 )
266 .unwrap();
267 }
268 }
269 s
270 }
271}
272impl<'a, AppState, T> Endpoint<AppState> for &'a T
273where
274 T: Endpoint<AppState>,
275{
276 fn path(&self) -> &'static str {
277 (*self).path()
278 }
279 fn method(&self) -> Method {
280 (*self).method()
281 }
282 fn bind_to(&self, router: axum::Router<AppState>) -> axum::Router<AppState> {
283 (*self).bind_to(router)
284 }
285 fn body(&self) -> RequestStructure {
286 (*self).body()
287 }
288 fn res(&self) -> ResponseTapi {
289 (*self).res()
290 }
291}
292
293pub struct Endpoints<'a, AppState> {
294 endpoints: Vec<&'a dyn Endpoint<AppState>>,
295 extra_tys: Vec<DynTapi>,
296}
297impl<'a, AppState> Endpoints<'a, AppState> {
298 pub fn new(endpoints: impl IntoIterator<Item = &'a dyn Endpoint<AppState>>) -> Self {
299 Self {
300 endpoints: endpoints.into_iter().collect(),
301 extra_tys: Vec::new(),
302 }
303 }
304 pub fn with_ty<T: Tapi + 'static>(mut self) -> Self {
305 self.extra_tys.push(T::boxed());
306 self
307 }
308 pub fn tys(&self) -> Vec<DynTapi> {
309 let mut tys = self.extra_tys.clone();
310 for endpoint in &self.endpoints {
311 tys.extend(endpoint.tys());
312 }
313 tys.sort_by_key(|t| t.id());
314 tys.dedup_by_key(|t| t.id());
315 transitive_closure(tys)
316 }
317 pub fn ts_client(&self) -> String {
318 let mut s = ts::builder().types(self.tys());
319
320 s.push_str("export const api = {\n");
321 for endpoint in &self.endpoints {
322 let name = heck::AsLowerCamelCase(endpoint.path()).to_string();
323 let name = if name.is_empty() { "index" } else { &name };
324 s.push_str(&format!(" {name}: {},\n", endpoint.ts_client()));
325 }
326 s.push_str("};\n");
327 s
328 }
329 pub fn js_client(&self) -> String {
330 let mut s = js::builder().types(self.tys());
331
332 s.push_str("export const api = {\n");
333 for endpoint in &self.endpoints {
334 let name = heck::AsLowerCamelCase(endpoint.path()).to_string();
335 let name = if name.is_empty() { "index" } else { &name };
336 s.push_str(&format!(" {name}: {},\n", endpoint.js_client()));
337 }
338 s.push_str("};\n");
339 s
340 }
341}
342impl<'a, AppState> IntoIterator for Endpoints<'a, AppState> {
343 type Item = &'a dyn Endpoint<AppState>;
344 type IntoIter = std::vec::IntoIter<Self::Item>;
345 fn into_iter(self) -> Self::IntoIter {
346 self.endpoints.into_iter()
347 }
348}
349impl<'s, 'a, AppState> IntoIterator for &'s Endpoints<'a, AppState> {
350 type Item = &'a dyn Endpoint<AppState>;
351 type IntoIter = std::iter::Copied<std::slice::Iter<'s, &'a dyn Endpoint<AppState>>>;
352 fn into_iter(self) -> Self::IntoIter {
353 self.endpoints.iter().copied()
354 }
355}
356
357pub trait RouterExt<AppState: 'static> {
358 fn tapi<E: Endpoint<AppState> + ?Sized>(self, endpoint: &E) -> Self;
359 fn tapis<'a>(self, endpoints: impl IntoIterator<Item = &'a dyn Endpoint<AppState>>) -> Self
360 where
361 Self: Sized,
362 {
363 endpoints.into_iter().fold(self, Self::tapi)
364 }
365}
366
367impl<AppState: 'static> RouterExt<AppState> for axum::Router<AppState> {
368 fn tapi<E: Endpoint<AppState> + ?Sized>(self, endpoint: &E) -> Self {
369 E::bind_to(endpoint, self)
370 }
371}
372
373pub struct Sse<T, E = axum::BoxError>(futures_util::stream::BoxStream<'static, Result<T, E>>);
374impl<T, E> Sse<T, E> {
375 pub fn new<S>(stream: S) -> Self
376 where
377 S: futures_util::Stream<Item = Result<T, E>> + Send + 'static,
378 {
379 Self(stream.boxed())
380 }
381}
382impl<T> axum::response::IntoResponse for Sse<T>
383where
384 T: serde::Serialize + 'static,
385{
386 fn into_response(self) -> axum::response::Response {
387 let stream = self
388 .0
389 .map(|s| -> Result<axum::response::sse::Event, axum::BoxError> {
390 let s = serde_json::to_string(&s?)?;
391 Ok(axum::response::sse::Event::default().data(s))
392 });
393 axum::response::sse::Sse::new(stream).into_response()
394 }
395}
396
397#[derive(Debug)]
398pub enum RequestTapi {
399 Path(DynTapi),
400 Query(DynTapi),
401 Json(DynTapi),
402 None,
403}
404pub trait RequestTapiExtractor {
405 fn extract_request() -> RequestTapi;
406}
407impl RequestTapiExtractor for () {
408 fn extract_request() -> RequestTapi {
409 RequestTapi::None
410 }
411}
412impl<T: Tapi + 'static> RequestTapiExtractor for axum::extract::Path<T> {
413 fn extract_request() -> RequestTapi {
414 RequestTapi::Path(<T as Tapi>::boxed())
415 }
416}
417impl<T: Tapi + 'static> RequestTapiExtractor for axum::extract::Query<T> {
418 fn extract_request() -> RequestTapi {
419 RequestTapi::Query(<T as Tapi>::boxed())
420 }
421}
422impl<T: Tapi + 'static> RequestTapiExtractor for axum::Json<T> {
423 fn extract_request() -> RequestTapi {
424 RequestTapi::Json(<T as Tapi>::boxed())
425 }
426}
427impl<T> RequestTapiExtractor for axum::extract::State<T> {
428 fn extract_request() -> RequestTapi {
429 RequestTapi::None
430 }
431}
432
433#[derive(Debug, Clone, Copy)]
434pub enum ResponseTapi {
435 PlainText,
437 Bytes,
439 Json(DynTapi),
441 Html,
443 Sse(DynTapi),
445 None,
446}
447pub trait ResponseTapiExtractor {
448 fn extract_response() -> ResponseTapi;
449}
450impl ResponseTapiExtractor for () {
451 fn extract_response() -> ResponseTapi {
452 ResponseTapi::None
453 }
454}
455impl ResponseTapiExtractor for String {
456 fn extract_response() -> ResponseTapi {
457 ResponseTapi::PlainText
458 }
459}
460impl ResponseTapiExtractor for Vec<u8> {
461 fn extract_response() -> ResponseTapi {
462 ResponseTapi::Bytes
463 }
464}
465impl<T: Tapi + 'static> ResponseTapiExtractor for axum::Json<T> {
466 fn extract_response() -> ResponseTapi {
467 ResponseTapi::Json(<T as Tapi>::boxed())
468 }
469}
470impl<T: Tapi + 'static> ResponseTapiExtractor for axum::response::Html<T> {
471 fn extract_response() -> ResponseTapi {
472 ResponseTapi::Html
473 }
474}
475impl<T: ResponseTapiExtractor, E> ResponseTapiExtractor for Result<T, E> {
476 fn extract_response() -> ResponseTapi {
477 T::extract_response()
478 }
479}
480impl<T: Tapi + 'static> ResponseTapiExtractor for Sse<T> {
481 fn extract_response() -> ResponseTapi {
482 ResponseTapi::Sse(<T as Tapi>::boxed())
483 }
484}
485
486impl RequestTapi {
487 pub fn ty(self) -> DynTapi {
488 match self {
489 Self::Path(ty) => ty,
490 Self::Query(ty) => ty,
491 Self::Json(ty) => ty,
492 Self::None => <() as Tapi>::boxed(),
493 }
494 }
495}
496impl ResponseTapi {
497 pub fn ty(self) -> DynTapi {
498 match self {
499 Self::PlainText => <String as Tapi>::boxed(),
500 Self::Bytes => <Vec<u8> as Tapi>::boxed(),
501 Self::Json(ty) => ty,
502 Self::Html => <String as Tapi>::boxed(),
503 Self::Sse(ty) => ty,
504 Self::None => <() as Tapi>::boxed(),
505 }
506 }
507}
508
509