Skip to main content

alux_http_typescript/
route.rs

1//! Composes the calls a program states into the module that makes them.
2
3use 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/// Carries route-selection meaning, which a call reads as a method and a path template.
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct TsSelector {
18    parts: Vec<TsSelectorPart>,
19}
20
21impl TsSelector {
22    /// Returns the composed path this selector matches.
23    ///
24    /// A described path is already a template, so a call carries it unchanged and the runtime fills
25    /// each slot with the argument whose role said it goes there.
26    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    /// Returns the selected method and path, using `*` when no method is selected.
34    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/// One call, before the method and path it is made under are composed around it.
48#[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    /// Returns the name a caller writes this call under.
58    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    /// Writes what the operation was documented as, as the comment a caller reads.
69    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/// A client module: the declarations its calls depend on, and the calls themselves.
105#[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    /// Writes the module: every declaration a call depends on, then the program the calls form.
114    pub fn render(&self) -> String {
115        let declarations = self.declarations.values().map(String::as_str).collect::<Vec<_>>();
116        // An entry states its own documentation above it, so indenting is a matter of every line.
117        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    /// Returns the name each call is written under, in name order.
135    pub fn call_names(&self) -> Vec<&str> {
136        self.entries.keys().map(String::as_str).collect()
137    }
138
139    /// Returns each composed selector as `METHOD /path`, in declaration order.
140    pub fn labels(&self) -> Vec<String> {
141        self.calls.iter().map(|(selector, _)| selector.label()).collect()
142    }
143
144    /// Returns each composed path template, in declaration order.
145    pub fn paths(&self) -> Vec<String> {
146        self.calls.iter().map(|(selector, _)| selector.path()).collect()
147    }
148
149    /// Writes every call whose method and path are now composed.
150    ///
151    /// A call composed without a method states nothing a caller could make, so it is left out.
152    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/// Composes route selection as the method and path a call is made under.
168#[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}