alux_http_typescript/
route.rs1use crate::TsArgument;
4use alux_http::{HttpMethod, HttpSelectorAlg, RouteAlg, RoutePath, SelectorAlg, describe_path};
5use alux_shape_typescript::TsType;
6use std::collections::BTreeMap;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9enum TsSelectorPart {
10 Method(HttpMethod),
11 Path(RoutePath),
12 Prefix(RoutePath),
13}
14
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct TsSelector {
18 parts: Vec<TsSelectorPart>,
19}
20
21impl TsSelector {
22 pub fn path(&self) -> String {
27 describe_path(self.parts.iter().filter_map(|part| match part {
28 TsSelectorPart::Path(path) | TsSelectorPart::Prefix(path) => Some(path),
29 TsSelectorPart::Method(_) => None,
30 }))
31 }
32
33 pub fn label(&self) -> String {
35 let method = self.method().map_or("*", HttpMethod::label);
36 format!("{method} {}", self.path())
37 }
38
39 fn method(&self) -> Option<HttpMethod> {
40 self.parts.iter().rev().find_map(|part| match part {
41 TsSelectorPart::Method(method) => Some(*method),
42 TsSelectorPart::Path(_) | TsSelectorPart::Prefix(_) => None,
43 })
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct TsCall {
50 pub(crate) name: String,
51 pub(crate) doc: &'static str,
52 pub(crate) parameters: Vec<(String, TsArgument)>,
53 pub(crate) answer: TsType,
54}
55
56impl TsCall {
57 pub fn name(&self) -> &str {
59 &self.name
60 }
61
62 fn declarations(&self) -> BTreeMap<String, String> {
63 let shapes = self.parameters.iter().map(|(_, argument)| &argument.shape).chain([&self.answer]);
64
65 shapes.flat_map(|shape| shape.declarations().map(|(name, text)| (name.to_owned(), text.to_owned()))).collect()
66 }
67
68 fn documented(&self) -> String {
70 let doc = self.doc.trim();
71 if doc.is_empty() {
72 return String::new();
73 }
74 if let Some(line) = doc.lines().next().filter(|_| doc.lines().count() == 1) {
75 return format!("/** {line} */\n");
76 }
77
78 let lines = doc.lines().map(|line| format!(" * {line}").trim_end().to_owned()).collect::<Vec<_>>();
79
80 format!("/**\n{}\n */\n", lines.join("\n"))
81 }
82
83 fn entry(&self, method: HttpMethod, path: &str) -> String {
84 let parameters = self
85 .parameters
86 .iter()
87 .map(|(name, argument)| format!("{name}: {}", argument.shape.expr()))
88 .collect::<Vec<_>>();
89 let roles =
90 self.parameters.iter().map(|(_, argument)| format!("\"{}\"", argument.source.label())).collect::<Vec<_>>();
91
92 format!(
93 "{}{}: endpoint<[{}], {}>(\"{}\", \"{path}\", [{}])",
94 self.documented(),
95 self.name,
96 parameters.join(", "),
97 self.answer.expr(),
98 method.label(),
99 roles.join(", "),
100 )
101 }
102}
103
104#[derive(Debug, Clone, Default)]
106pub struct TsHttpModule {
107 declarations: BTreeMap<String, String>,
108 entries: BTreeMap<String, String>,
109 calls: Vec<(TsSelector, TsCall)>,
110}
111
112impl TsHttpModule {
113 pub fn render(&self) -> String {
115 let declarations = self.declarations.values().map(String::as_str).collect::<Vec<_>>();
116 let entries = self
118 .entries
119 .values()
120 .map(|entry| {
121 let mut lines = entry.lines().map(|line| format!(" {line}").trim_end().to_owned()).collect::<Vec<_>>();
122 if let Some(last) = lines.last_mut() {
123 last.push(',');
124 }
125
126 lines.join("\n")
127 })
128 .collect::<Vec<_>>();
129 let program = format!("export const program = {{\n{}\n}} as const", entries.join("\n"));
130
131 if declarations.is_empty() { program } else { format!("{}\n\n{program}", declarations.join("\n\n")) }
132 }
133
134 pub fn call_names(&self) -> Vec<&str> {
136 self.entries.keys().map(String::as_str).collect()
137 }
138
139 pub fn labels(&self) -> Vec<String> {
141 self.calls.iter().map(|(selector, _)| selector.label()).collect()
142 }
143
144 pub fn paths(&self) -> Vec<String> {
146 self.calls.iter().map(|(selector, _)| selector.path()).collect()
147 }
148
149 fn written(mut self) -> Self {
153 self.entries = self
154 .calls
155 .iter()
156 .filter_map(|(selector, call)| {
157 let method = selector.method()?;
158
159 Some((call.name.clone(), call.entry(method, &selector.path())))
160 })
161 .collect();
162
163 self
164 }
165}
166
167#[derive(Debug, Default)]
169pub struct TsRouteImpl;
170
171impl SelectorAlg for TsRouteImpl {
172 type Selector = TsSelector;
173
174 fn identity(&self) -> TsSelector {
175 TsSelector::default()
176 }
177
178 fn compose(&self, mut first: TsSelector, second: TsSelector) -> TsSelector {
179 first.parts.extend(second.parts);
180 first
181 }
182}
183
184impl RouteAlg for TsRouteImpl {
185 type Route = TsHttpModule;
186 type Selector = TsSelector;
187 type Endpoint = TsCall;
188
189 fn initial(&self) -> TsHttpModule {
190 TsHttpModule::default()
191 }
192
193 fn coproduct(&self, mut left: TsHttpModule, right: TsHttpModule) -> TsHttpModule {
194 left.declarations.extend(right.declarations);
195 left.calls.extend(right.calls);
196
197 left.written()
198 }
199
200 fn precompose(&self, selector: TsSelector, mut route: TsHttpModule) -> TsHttpModule {
201 for (composed, _) in &mut route.calls {
202 *composed = self.compose(selector.clone(), core::mem::take(composed));
203 }
204
205 route.written()
206 }
207
208 fn lift(&self, call: TsCall) -> TsHttpModule {
209 let module = TsHttpModule {
210 declarations: call.declarations(),
211 entries: BTreeMap::new(),
212 calls: vec![(TsSelector::default(), call)],
213 };
214
215 module.written()
216 }
217}
218
219impl HttpSelectorAlg for TsRouteImpl {
220 type Selector = TsSelector;
221
222 fn http_method(&self, method: HttpMethod) -> TsSelector {
223 TsSelector { parts: vec![TsSelectorPart::Method(method)] }
224 }
225
226 fn http_path(&self, path: &RoutePath) -> TsSelector {
227 TsSelector { parts: vec![TsSelectorPart::Path(path.clone())] }
228 }
229
230 fn http_prefix(&self, prefix: &RoutePath) -> TsSelector {
231 TsSelector { parts: vec![TsSelectorPart::Prefix(prefix.clone())] }
232 }
233}