Skip to main content

alux_http_conformance/
surface.rs

1//! One declared surface, and the domain it reads.
2//!
3//! This is user code, written once. Every interpretation compiles this same declaration, which is
4//! what makes their agreement evidence rather than coincidence.
5
6use crate::SETTLE;
7use alux_ext::ext;
8use alux_http::{
9    BytesOutAlg, CacheControl, ChunksAlg, ChunksExt, EmptyOutAlg, FromPartsAlg, HeaderOutAlg, HtmlOutAlg, HttpApiAlg,
10    JsonOutAlg, NamedValuesAlg, PartAlg, RedirectOutAlg, ResultOutAlg, StatusOutAlg, StreamOutAlg, TextOutAlg, http,
11};
12use alux_shape::Shape;
13use core::convert::Infallible;
14use core::fmt::Display;
15use core::future::Future;
16use core::time::Duration;
17use serde::{Deserialize, Serialize};
18use std::io::{Error as IoError, ErrorKind};
19use tokio::time::sleep;
20
21/// Who a caller says they are, sent as cookies.
22#[derive(Debug, Serialize, Deserialize, Shape)]
23pub struct Session {
24    /// Which session the caller is in.
25    pub session: String,
26}
27
28/// What a caller said about themselves in the headers they sent.
29#[derive(Debug, Serialize, Deserialize, Shape)]
30pub struct Agent {
31    /// What the caller says they are.
32    pub user_agent: String,
33}
34
35impl NamedValuesAlg for Session {}
36
37impl NamedValuesAlg for Agent {}
38
39/// One reading, sent as a form.
40#[derive(Debug, Serialize, Deserialize, Shape)]
41pub struct Amount {
42    /// What was read.
43    pub value: u32,
44}
45
46/// Keeps whatever readings the domain has been told about.
47pub trait ShopAlg {
48    /// Returns the reading recorded for `id`, or why it could not be read.
49    fn item(&self, id: u32) -> impl Future<Output = Result<u32, IoError>> + Send;
50    /// Returns every reading.
51    fn items(&self) -> impl Future<Output = Vec<u32>> + Send;
52    /// Records `value` and returns it.
53    fn add(&self, value: u32) -> impl Future<Output = u32> + Send;
54    /// Notes what was sent and says what it made of it.
55    fn note(&self, note: String) -> impl Future<Output = String> + Send;
56    /// Forgets every reading.
57    fn clear(&self) -> impl Future<Output = ()> + Send;
58    /// Returns where the readings actually live.
59    fn home(&self) -> impl Future<Output = String> + Send;
60    /// Returns the readings as a page.
61    fn page(&self) -> impl Future<Output = String> + Send;
62    /// Returns the readings as they are stored.
63    fn stored(&self) -> impl Future<Output = Vec<u8>> + Send;
64    /// Returns who the caller is in, as they said.
65    fn who(&self, session: String) -> impl Future<Output = String> + Send;
66    /// Returns what the caller says they are.
67    fn agent(&self, agent: String) -> impl Future<Output = String> + Send;
68    /// Returns the readings and how long they may be kept.
69    fn cached(&self) -> impl Future<Output = (String, Vec<u32>)> + Send;
70}
71
72/// Derives the operations the shared surface exposes.
73#[ext(name = ShopOperationExt, defunc)]
74pub impl<This> This
75where
76    This: ShopAlg,
77{
78    /// Returns one identified reading, or why it could not be read.
79    async fn shop_item(&self, id: u32) -> Result<u32, IoError> {
80        self.item(id).await
81    }
82
83    /// Returns every reading.
84    async fn shop_items(&self) -> Vec<u32> {
85        self.items().await
86    }
87
88    /// Records one reading and returns it.
89    async fn shop_add(&self, value: u32) -> u32 {
90        self.add(value).await
91    }
92
93    /// Records one reading sent as a form and returns it.
94    async fn shop_fill(&self, amount: Amount) -> u32 {
95        self.add(amount.value).await
96    }
97
98    /// Notes what was sent and says what it made of it.
99    async fn shop_note(&self, note: String) -> String {
100        self.note(note).await
101    }
102
103    /// Forgets every reading.
104    async fn shop_clear(&self) {
105        self.clear().await;
106    }
107
108    /// Returns where the readings actually live.
109    async fn shop_home(&self) -> String {
110        self.home().await
111    }
112
113    /// Returns the readings as a page.
114    async fn shop_page(&self) -> String {
115        self.page().await
116    }
117
118    /// Returns the readings as they are stored.
119    async fn shop_stored(&self) -> Vec<u8> {
120        self.stored().await
121    }
122
123    /// Returns who the caller says they are.
124    async fn shop_who(&self, session: Session) -> String {
125        self.who(session.session).await
126    }
127
128    /// Returns what the caller says they are.
129    async fn shop_agent(&self, agent: Agent) -> String {
130        self.agent(agent.user_agent).await
131    }
132
133    /// Returns the readings, and how long a caller may keep them.
134    async fn shop_cached(&self) -> (String, Vec<u32>) {
135        self.cached().await
136    }
137}
138
139/// Declares the shared surface, which every interpretation compiles unchanged.
140#[ext(name = ShopApiExt, defunc(via = http))]
141pub impl<This> This
142where
143    This: HttpApiAlg
144        + HeaderOutAlg
145        + JsonOutAlg
146        + TextOutAlg
147        + HtmlOutAlg
148        + BytesOutAlg
149        + EmptyOutAlg
150        + RedirectOutAlg
151        + StatusOutAlg
152        + ResultOutAlg,
153{
154    /// Declares the surface every interpretation is held to.
155    fn shop_api<Alg>(&self)
156    where
157        Alg: ShopAlg,
158    {
159        self.routes()
160            // One identified reading, its id taken from the path in Poem's spelling.
161            .get("/item/:id", self.op(Alg::shop_item).path::<u32>().json().result())
162            // Every reading.
163            .get("/items", self.op(Alg::shop_items).json())
164            // A recording sent as a document, which creates something and says so.
165            .post("/items", self.op(Alg::shop_add).body::<u32>().json().status::<201>())
166            // The same recording, sent as a form.
167            .put("/items", self.op(Alg::shop_fill).form::<Amount>().json())
168            // A note, taken exactly as it arrived.
169            .patch("/items", self.op(Alg::shop_note).raw_body::<String>().text())
170            // A removal, which answers with nothing at all.
171            .delete("/items", self.op(Alg::shop_clear).empty())
172            // Where the readings actually live.
173            .get("/home", self.op(Alg::shop_home).redirect())
174            // The readings as a page.
175            .get("/page", self.op(Alg::shop_page).html())
176            // The readings as they are stored.
177            .get("/stored", self.op(Alg::shop_stored).bytes())
178            // Who the caller says they are, taken from the cookies they sent.
179            .get("/session", self.op(Alg::shop_who).cookie::<Session>().text())
180            // What the caller says they are, taken from the headers they sent.
181            .get("/agent", self.op(Alg::shop_agent).in_header::<Agent>().text())
182            // Every reading, and how long a caller may keep it.
183            .get("/cached", self.op(Alg::shop_cached).json().out_header::<CacheControl>())
184    }
185}
186
187/// The reference domain every interpretation is held to.
188#[derive(Debug, Default, Clone, Copy)]
189pub struct Shop;
190
191impl ShopAlg for Shop {
192    async fn item(&self, id: u32) -> Result<u32, IoError> {
193        match id {
194            1 => Ok(7),
195            _ => Err(IoError::new(ErrorKind::NotFound, "no such reading")),
196        }
197    }
198
199    async fn items(&self) -> Vec<u32> {
200        vec![7]
201    }
202
203    async fn add(&self, value: u32) -> u32 {
204        value
205    }
206
207    async fn note(&self, note: String) -> String {
208        format!("noted {note}")
209    }
210
211    async fn clear(&self) {}
212
213    async fn home(&self) -> String {
214        "/items".to_owned()
215    }
216
217    async fn page(&self) -> String {
218        "<p>7</p>".to_owned()
219    }
220
221    async fn stored(&self) -> Vec<u8> {
222        b"seven".to_vec()
223    }
224
225    async fn who(&self, session: String) -> String {
226        format!("known as {session}")
227    }
228
229    async fn agent(&self, agent: String) -> String {
230        format!("sent by {agent}")
231    }
232
233    async fn cached(&self) -> (String, Vec<u32>) {
234        ("max-age=60".to_owned(), vec![7])
235    }
236}
237
238/// Every label the declared surface states, in declaration order.
239pub const LABELS: &[&str] = &[
240    "GET /item/{id}",
241    "GET /items",
242    "POST /items",
243    "PUT /items",
244    "PATCH /items",
245    "DELETE /items",
246    "GET /home",
247    "GET /page",
248    "GET /stored",
249    "GET /session",
250    "GET /agent",
251    "GET /cached",
252];
253
254/// Reads however many arguments a caller states.
255pub trait WideAlg {
256    /// Returns what the domain makes of everything stated.
257    fn wide(&self, stated: Vec<String>) -> impl Future<Output = String> + Send;
258}
259
260/// Derives the one operation the widest surface exposes.
261#[ext(name = WideOperationExt, defunc)]
262pub impl<This> This
263where
264    This: WideAlg,
265{
266    /// Returns what the domain makes of sixteen stated arguments.
267    #[allow(clippy::too_many_arguments)]
268    async fn wide_all(
269        &self,
270        first: String,
271        second: String,
272        third: String,
273        fourth: String,
274        fifth: String,
275        sixth: String,
276        seventh: String,
277        eighth: String,
278        ninth: String,
279        tenth: String,
280        eleventh: String,
281        twelfth: String,
282        thirteenth: String,
283        fourteenth: String,
284        fifteenth: String,
285        sixteenth: String,
286    ) -> String {
287        let stated = vec![
288            first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, tenth, eleventh, twelfth, thirteenth,
289            fourteenth, fifteenth, sixteenth,
290        ];
291
292        self.wide(stated).await
293    }
294}
295
296/// Declares the widest endpoint the specification states, which every interpretation compiles.
297///
298/// Sixteen is what the products accumulate to, so one endpoint reading sixteen arguments is what
299/// holds every interpretation to the same width. One role is repeated because a role a framework
300/// reads once is a framework's business, and what is being stated here is the width.
301#[ext(name = WideApiExt, defunc(via = http))]
302pub impl<This> This
303where
304    This: HttpApiAlg + TextOutAlg,
305{
306    /// Declares one endpoint reading sixteen arguments.
307    fn wide_api<Alg>(&self)
308    where
309        Alg: WideAlg,
310    {
311        self.routes().post(
312            "/wide",
313            self.op(Alg::wide_all)
314                .raw_body::<String>()
315                .raw_body::<String>()
316                .raw_body::<String>()
317                .raw_body::<String>()
318                .raw_body::<String>()
319                .raw_body::<String>()
320                .raw_body::<String>()
321                .raw_body::<String>()
322                .raw_body::<String>()
323                .raw_body::<String>()
324                .raw_body::<String>()
325                .raw_body::<String>()
326                .raw_body::<String>()
327                .raw_body::<String>()
328                .raw_body::<String>()
329                .raw_body::<String>()
330                .text(),
331        )
332    }
333}
334
335impl WideAlg for Shop {
336    async fn wide(&self, stated: Vec<String>) -> String {
337        stated.len().to_string()
338    }
339}
340
341/// A body the domain produces a piece at a time.
342///
343/// Nothing here names a stream type. The domain states what a chunk is and how the next one is
344/// taken, and whichever interpretation carries the answer chooses how the bytes actually move.
345#[derive(Debug, Default)]
346pub struct Ticks {
347    left: Vec<&'static str>,
348}
349
350impl ChunksAlg for Ticks {
351    type Chunk = Vec<u8>;
352    type Error = Infallible;
353
354    async fn next_chunk(&mut self) -> Option<Result<Self::Chunk, Self::Error>> {
355        self.left.pop().map(|tick| Ok(tick.as_bytes().to_vec()))
356    }
357}
358
359/// Answers with a body it produces a piece at a time.
360pub trait TicksAlg {
361    /// Returns what the domain has to say, a piece at a time.
362    fn ticks(&self) -> impl Future<Output = Ticks> + Send;
363}
364
365impl TicksAlg for Shop {
366    async fn ticks(&self) -> Ticks {
367        Ticks { left: vec!["three", "two", "one"] }
368    }
369}
370
371/// Derives the one operation the streamed surface exposes.
372#[ext(name = TicksOperationExt, defunc)]
373pub impl<This> This
374where
375    This: TicksAlg,
376{
377    /// Returns what the domain has to say, a piece at a time.
378    async fn shop_ticks(&self) -> Ticks {
379        self.ticks().await
380    }
381}
382
383/// Declares the streamed surface, which every interpretation carrying a produced body compiles.
384#[ext(name = StreamApiExt, defunc(via = http))]
385pub impl<This> This
386where
387    This: HttpApiAlg + StreamOutAlg,
388{
389    /// Declares one endpoint answering with a body produced over time.
390    fn stream_api<Alg>(&self)
391    where
392        Alg: TicksAlg,
393    {
394        self.routes().get("/ticks", self.op(Alg::shop_ticks).stream())
395    }
396}
397
398/// How long after a close begins the slow endpoint would answer.
399///
400/// Far longer than any drain an interpretation states, so a caller waiting on it is a caller whose
401/// connection outlives the close rather than one the close waits out.
402pub const ANSWERS_LATE: Duration = Duration::from_secs(30);
403
404/// How long after a close begins the pausing endpoint answers.
405///
406/// Shorter than any drain an interpretation states, so a caller waiting on it is answered while the
407/// server is closing rather than cut off by it.
408pub const ANSWERS_SOON: Duration = Duration::from_secs(1);
409
410/// How long the slow endpoint takes to answer.
411pub const SLOW: Duration = ANSWERS_LATE.saturating_add(SETTLE);
412
413/// How long the pausing endpoint takes to answer.
414///
415/// Both endpoints carry [`SETTLE`], the wait between sending a request and beginning the close, so
416/// each answers the stated time after the close begins rather than after the request. That is what
417/// lets a measurement be read against the drain directly.
418pub const PAUSE: Duration = ANSWERS_SOON.saturating_add(SETTLE);
419
420/// Answers after taking some time, which is how a caller holds a request in flight.
421pub trait SlowAlg {
422    /// Returns what the domain has to say, once it has taken longer than any drain.
423    fn slow(&self) -> impl Future<Output = String> + Send;
424
425    /// Returns what the domain has to say, once it has paused for less than any drain.
426    fn pause(&self) -> impl Future<Output = String> + Send;
427}
428
429impl SlowAlg for Shop {
430    async fn slow(&self) -> String {
431        sleep(SLOW).await;
432        "waited".to_owned()
433    }
434
435    async fn pause(&self) -> String {
436        sleep(PAUSE).await;
437        "paused".to_owned()
438    }
439}
440
441/// Derives the one operation the slow surface exposes.
442#[ext(name = SlowOperationExt, defunc)]
443pub impl<This> This
444where
445    This: SlowAlg,
446{
447    /// Answers slowly enough that the caller is still waiting when the server closes.
448    ///
449    /// Takes longer to answer than any interpretation waits before closing anyway, so a caller
450    /// asking for this holds a connection the server is still producing an answer on. It exists for
451    /// the lifecycle scenario, which needs a connection that outlives a close.
452    async fn shop_slow(&self) -> String {
453        self.slow().await
454    }
455
456    /// Answers while the server is closing, rather than after it has closed.
457    ///
458    /// Takes less time to answer than any interpretation waits before closing anyway, so a caller
459    /// asking for this is answered by a server that is already shutting down. It is the other half
460    /// of what the lifecycle scenario needs: a request in flight that a close does not cut off.
461    async fn shop_pause(&self) -> String {
462        self.pause().await
463    }
464}
465
466/// Declares the surface the lifecycle scenario serves.
467///
468/// Three endpoints and nothing else. One answers at once, so a caller can state that a server is
469/// serving. The other two take time, which is how a caller holds a request in flight across a
470/// close: one takes longer than any drain, the other less. None can fail, because being busy is not
471/// a failure.
472#[ext(name = LifecycleApiExt, defunc(via = http))]
473pub impl<This> This
474where
475    This: HttpApiAlg + JsonOutAlg + TextOutAlg,
476{
477    /// Declares one endpoint answering at once and one taking longer than any drain.
478    fn lifecycle_api<Alg>(&self)
479    where
480        Alg: ShopAlg + SlowAlg,
481    {
482        self.routes()
483            // Something answered at once, which is how a caller states that a server is serving.
484            .get("/items", self.op(Alg::shop_items).json())
485            // Something not answered in any useful time, which is how a caller holds a connection
486            // that outlives the close.
487            .get("/slow", self.op(Alg::shop_slow).text())
488            // Something answered after a moment, which is how a caller holds a request the close
489            // has time to finish.
490            .get("/pause", self.op(Alg::shop_pause).text())
491    }
492}
493
494/// What a caller sent as parts, read the same way by every interpretation.
495///
496/// Nothing here names a reader. The domain states what it makes of a sequence of parts, and each
497/// interpretation hands it whichever reader it has.
498#[derive(Debug, Default)]
499pub struct Upload {
500    /// What each part was sent under, and what it carried.
501    pub stated: Vec<String>,
502}
503
504impl<Parts> FromPartsAlg<Parts> for Upload
505where
506    Parts: ChunksAlg + Send,
507    Parts::Error: Display + Send,
508    Parts::Chunk: PartAlg + Send,
509    <Parts::Chunk as PartAlg>::Content: ChunksAlg<Chunk = Vec<u8>> + Send + 'static,
510    <<Parts::Chunk as PartAlg>::Content as ChunksAlg>::Error: Display,
511{
512    type Error = String;
513
514    async fn from_parts(mut parts: Parts) -> Result<Self, Self::Error> {
515        let mut stated = Vec::new();
516        while let Some(part) = parts.next_chunk().await {
517            let part = part.map_err(|error| error.to_string())?;
518            let name = part.part_name().unwrap_or_default().to_owned();
519            let carried = part.part_content().gathered().await.map_err(|error| error.to_string())?;
520            let carried = String::from_utf8_lossy(&carried.concat()).into_owned();
521            stated.push(format!("{name}={carried}"));
522        }
523
524        Ok(Self { stated })
525    }
526}
527
528/// Reads a body a caller sent as parts.
529pub trait UploadAlg {
530    /// Returns what the domain makes of everything the parts stated.
531    fn uploaded(&self, stated: Vec<String>) -> impl Future<Output = String> + Send;
532}
533
534impl UploadAlg for Shop {
535    async fn uploaded(&self, stated: Vec<String>) -> String {
536        stated.join(",")
537    }
538}
539
540/// Derives the one operation the parts surface exposes.
541#[ext(name = UploadOperationExt, defunc)]
542pub impl<This> This
543where
544    This: UploadAlg,
545{
546    /// Returns what the domain makes of a body sent as parts.
547    async fn shop_upload(&self, upload: Upload) -> String {
548        self.uploaded(upload.stated).await
549    }
550}
551
552/// Declares the parts surface, which every interpretation reading a body as parts compiles.
553#[ext(name = MultipartApiExt, defunc(via = http))]
554pub impl<This> This
555where
556    This: HttpApiAlg + TextOutAlg,
557{
558    /// Declares one endpoint reading a body that arrives as parts.
559    fn multipart_api<Alg>(&self)
560    where
561        Alg: UploadAlg,
562    {
563        self.routes().post("/upload", self.op(Alg::shop_upload).multipart::<Upload>().text())
564    }
565}