Skip to main content

alux_http_text/
lib.rs

1//! Interprets typed HTTP programs as readable route and type descriptions.
2//!
3//! The text interpretation executes no handler. It records the selectors, extractor roles, argument
4//! product, handler result, and output conversion each endpoint denotes, which makes it the neutral
5//! witness that an HTTP program means a surface rather than a framework callback.
6
7use alux_ext::{ApplyAlg, HandlerContextAlg};
8use alux_http::{
9    FileOutAlg, HandlerAlg, HandlerEndpointAlg, HttpInputAlg, HttpSelectorAlg, JsonOutAlg, OutputAlg, OutputKindAlg,
10    RouteAlg, SelectorAlg, append_path,
11};
12use core::any::type_name;
13use core::marker::PhantomData;
14use std::sync::Arc;
15
16/// Carries interpreted endpoint type information.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct TextEndpoint {
19    handler: &'static str,
20    inputs: &'static str,
21    args: &'static str,
22    result: &'static str,
23    transform: &'static str,
24    output: &'static str,
25}
26
27/// Identifies an HTTP input role in text output.
28pub struct TextInputRole<Role, Input>(PhantomData<fn(Role) -> Input>);
29
30/// Identifies path extraction in text descriptions.
31pub struct PathRole;
32/// Identifies query extraction in text descriptions.
33pub struct QueryRole;
34/// Identifies request-body extraction in text descriptions.
35pub struct BodyRole;
36/// Identifies header extraction in text descriptions.
37pub struct HeaderRole;
38/// Identifies authentication extraction in text descriptions.
39pub struct AuthRole;
40/// Identifies request-context extraction in text descriptions.
41pub struct ContextRole;
42
43/// Interprets JSON output selection in text descriptions.
44pub struct TextJsonOutput;
45
46impl<From> OutputAlg<From> for TextJsonOutput {
47    type Output = From;
48
49    fn output(from: From) -> From {
50        from
51    }
52}
53
54/// Interprets streamed-file output selection in text descriptions.
55pub struct TextFileOutput;
56
57impl<From> OutputAlg<From> for TextFileOutput {
58    type Output = From;
59
60    fn output(from: From) -> From {
61        from
62    }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66enum TextSelectorPart {
67    Method(&'static str),
68    Path(String),
69    Prefix(String),
70}
71
72/// Carries interpreted selector data.
73#[derive(Debug, Clone, Default, PartialEq, Eq)]
74pub struct TextSelector {
75    parts: Vec<TextSelectorPart>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79struct TextRouteEntry {
80    selector: TextSelector,
81    endpoint: TextEndpoint,
82}
83
84/// Carries an interpreted route composition.
85#[derive(Debug, Clone, Default, PartialEq, Eq)]
86pub struct TextRoute {
87    entries: Vec<TextRouteEntry>,
88}
89
90impl TextRoute {
91    /// Returns each interpreted selector as `METHOD /path`, in declaration order.
92    pub fn labels(&self) -> Vec<String> {
93        self.entries.iter().map(|entry| entry.selector.label()).collect()
94    }
95
96    /// Returns each interpreted route path, in declaration order.
97    pub fn paths(&self) -> Vec<String> {
98        self.entries.iter().map(|entry| entry.selector.path()).collect()
99    }
100
101    /// Renders each route as a Markdown description.
102    pub fn lines(&self) -> Vec<String> {
103        self.entries
104            .iter()
105            .map(|entry| {
106                format!(
107                    concat!(
108                        "### {}\n",
109                        "- `handler`: `{}`\n",
110                        "- `inputs`: `{}`\n",
111                        "- `args`: `{}`\n",
112                        "- `result`: `{}`\n",
113                        "- `transform`: `{}`\n",
114                        "- `output`: `{}`",
115                    ),
116                    entry.selector.label(),
117                    entry.endpoint.handler,
118                    entry.endpoint.inputs,
119                    entry.endpoint.args,
120                    entry.endpoint.result,
121                    entry.endpoint.transform,
122                    entry.endpoint.output,
123                )
124            })
125            .collect()
126    }
127}
128
129impl TextSelector {
130    /// Returns the composed absolute path this selector matches.
131    pub fn path(&self) -> String {
132        let mut path = String::new();
133        for part in &self.parts {
134            if let TextSelectorPart::Path(value) | TextSelectorPart::Prefix(value) = part {
135                append_path(&mut path, value);
136            }
137        }
138
139        if path.is_empty() {
140            path.push('/');
141        }
142
143        path
144    }
145
146    /// Returns the selected method and path, using `*` when no method is selected.
147    pub fn label(&self) -> String {
148        let method = self
149            .parts
150            .iter()
151            .rev()
152            .find_map(|part| match part {
153                TextSelectorPart::Method(value) => Some(*value),
154                TextSelectorPart::Path(_) | TextSelectorPart::Prefix(_) => None,
155            })
156            .unwrap_or("*");
157
158        format!("{method} {}", self.path())
159    }
160}
161
162/// Interprets typed APIs as text descriptions.
163#[derive(Debug, Default)]
164pub struct TextHandlerImpl;
165
166impl HandlerAlg for TextHandlerImpl {
167    type Endpoint = TextEndpoint;
168}
169
170impl<Context, Inputs, Args, Transform, Output> HandlerEndpointAlg<Context, Inputs, Args, Transform, Output>
171    for TextHandlerImpl
172where
173    Transform: OutputKindAlg<TextHandlerImpl, Output>,
174{
175    fn finish_handler<Handler>(&self, _handler: Handler) -> TextEndpoint
176    where
177        Handler: ApplyAlg<Context, Args, Output = Output> + Send + Sync + 'static,
178    {
179        TextEndpoint {
180            handler: type_name::<Handler>(),
181            inputs: type_name::<Inputs>(),
182            args: type_name::<Args>(),
183            result: type_name::<Output>(),
184            transform: type_name::<<Transform as OutputKindAlg<Self, Output>>::Transform>(),
185            output: type_name::<<<Transform as OutputKindAlg<Self, Output>>::Transform as OutputAlg<Output>>::Output>(),
186        }
187    }
188}
189
190impl<Context> HandlerContextAlg<Context> for TextHandlerImpl
191where
192    Context: Send + Sync + 'static,
193{
194    type Handle = Arc<Context>;
195}
196
197impl SelectorAlg for TextHandlerImpl {
198    type Selector = TextSelector;
199
200    fn identity(&self) -> TextSelector {
201        TextSelector::default()
202    }
203
204    fn compose(&self, mut first: TextSelector, second: TextSelector) -> TextSelector {
205        first.parts.extend(second.parts);
206        first
207    }
208}
209
210impl RouteAlg for TextHandlerImpl {
211    type Route = TextRoute;
212    type Selector = TextSelector;
213    type Endpoint = TextEndpoint;
214
215    fn initial(&self) -> TextRoute {
216        TextRoute::default()
217    }
218
219    fn coproduct(&self, mut left: TextRoute, right: TextRoute) -> TextRoute {
220        left.entries.extend(right.entries);
221        left
222    }
223
224    fn precompose(&self, selector: TextSelector, mut route: TextRoute) -> TextRoute {
225        for entry in &mut route.entries {
226            entry.selector = self.compose(selector.clone(), core::mem::take(&mut entry.selector));
227        }
228        route
229    }
230
231    fn lift(&self, endpoint: TextEndpoint) -> TextRoute {
232        TextRoute { entries: vec![TextRouteEntry { selector: self.identity(), endpoint }] }
233    }
234}
235
236impl HttpInputAlg for TextHandlerImpl {
237    type Path<Input> = TextInputRole<PathRole, Input>;
238    type Query<Input> = TextInputRole<QueryRole, Input>;
239    type Body<Input> = TextInputRole<BodyRole, Input>;
240    type Header<Input> = TextInputRole<HeaderRole, Input>;
241    type Auth<Input> = TextInputRole<AuthRole, Input>;
242    type Context<Input> = TextInputRole<ContextRole, Input>;
243}
244
245impl JsonOutAlg for TextHandlerImpl {
246    type Json<From> = TextJsonOutput;
247}
248
249impl FileOutAlg for TextHandlerImpl {
250    type File<From> = TextFileOutput;
251}
252
253impl HttpSelectorAlg for TextHandlerImpl {
254    type Selector = TextSelector;
255
256    fn http_get(&self) -> TextSelector {
257        TextSelector { parts: vec![TextSelectorPart::Method("GET")] }
258    }
259
260    fn http_post(&self) -> TextSelector {
261        TextSelector { parts: vec![TextSelectorPart::Method("POST")] }
262    }
263
264    fn http_path(&self, path: &str) -> TextSelector {
265        TextSelector { parts: vec![TextSelectorPart::Path(path.into())] }
266    }
267
268    fn http_prefix(&self, prefix: &str) -> TextSelector {
269        TextSelector { parts: vec![TextSelectorPart::Prefix(prefix.into())] }
270    }
271}