doctest_support/
lib.rs

1#![deny(clippy::pedantic)]
2//! This is support code for doc tests
3
4pub mod basic_delay;
5pub mod delay;
6
7pub mod command {
8    use crux_core::{capability::Operation, Request};
9    use serde::{Deserialize, Serialize};
10
11    #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
12    pub enum AnOperation {
13        One(u8),
14        Two(u8),
15    }
16
17    #[derive(Debug, PartialEq, Deserialize)]
18    pub enum AnOperationOutput {
19        One(u8),
20        Two(u8),
21    }
22
23    impl Operation for AnOperation {
24        type Output = AnOperationOutput;
25    }
26
27    pub enum Effect {
28        AnEffect(Request<AnOperation>),
29        Http(Request<crux_http::protocol::HttpRequest>),
30        Render(Request<crux_core::render::RenderOperation>),
31    }
32
33    impl From<Request<AnOperation>> for Effect {
34        fn from(request: Request<AnOperation>) -> Self {
35            Self::AnEffect(request)
36        }
37    }
38
39    impl From<Request<crux_http::protocol::HttpRequest>> for Effect {
40        fn from(request: Request<crux_http::protocol::HttpRequest>) -> Self {
41            Self::Http(request)
42        }
43    }
44
45    impl From<Request<crux_core::render::RenderOperation>> for Effect {
46        fn from(request: Request<crux_core::render::RenderOperation>) -> Self {
47            Self::Render(request)
48        }
49    }
50
51    #[derive(Debug, PartialEq, Deserialize, Serialize)]
52    pub struct Post {
53        pub url: String,
54        pub title: String,
55        pub body: String,
56    }
57
58    #[derive(Debug, PartialEq)]
59    pub enum Event {
60        Start,
61        Completed(AnOperationOutput),
62        Aborted,
63        GotPost(Result<crux_http::Response<Post>, crux_http::HttpError>),
64    }
65
66    #[cfg(test)]
67    mod tests {
68        use crux_http::{
69            command::Http,
70            protocol::{HttpRequest, HttpResponse, HttpResult},
71            testing::ResponseBuilder,
72        };
73
74        use crate::command::{Effect, Event, Post};
75
76        #[test]
77        fn http_post() {
78            const API_URL: &str = "https://example.com/api/posts";
79
80            // Create a command to post a new Post to API_URL
81            // and then dispatch an event with the result
82            let mut cmd = Http::post(API_URL)
83                .body(serde_json::json!({"title":"New Post", "body":"Hello!"}))
84                .expect_json()
85                .build()
86                .then_send(Event::GotPost);
87
88            // Check the effect is an HTTP request ...
89            let effect = cmd.effects().next().unwrap();
90            let Effect::Http(mut request) = effect else {
91                panic!("Expected a HTTP effect")
92            };
93
94            // ... and the request is a POST to API_URL
95            assert_eq!(
96                &request.operation,
97                &HttpRequest::post(API_URL)
98                    .header("content-type", "application/json")
99                    .body(r#"{"body":"Hello!","title":"New Post"}"#)
100                    .build()
101            );
102
103            // Resolve the request with a successful response
104            let body = Post {
105                url: API_URL.to_string(),
106                title: "New Post".to_string(),
107                body: "Hello!".to_string(),
108            };
109            request
110                .resolve(HttpResult::Ok(HttpResponse::ok().json(&body).build()))
111                .expect("Resolve should succeed");
112
113            // Check the event is a GotPost event with the successful response
114            let actual = cmd.events().next().unwrap();
115            let expected = Event::GotPost(Ok(ResponseBuilder::ok().body(body).build()));
116            assert_eq!(actual, expected);
117
118            assert!(cmd.is_done());
119        }
120    }
121}
122
123pub mod compose {
124    pub mod capabilities {
125        pub mod capability_one {
126            use crux_core::capability::{CapabilityContext, Operation};
127            use serde::{Deserialize, Serialize};
128
129            #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)]
130            pub struct OpOne {
131                number: usize,
132            }
133
134            impl Operation for OpOne {
135                type Output = usize;
136            }
137
138            pub struct CapabilityOne<E> {
139                context: CapabilityContext<OpOne, E>,
140            }
141
142            // Needed to allow 'this = (*self).clone()' without requiring E: Clone
143            // See https://github.com/rust-lang/rust/issues/26925
144            impl<E> Clone for CapabilityOne<E> {
145                fn clone(&self) -> Self {
146                    Self {
147                        context: self.context.clone(),
148                    }
149                }
150            }
151
152            impl<E> CapabilityOne<E> {
153                #[must_use]
154                pub fn new(context: CapabilityContext<OpOne, E>) -> Self {
155                    Self { context }
156                }
157
158                pub fn one<F>(&self, number: usize, event: F)
159                where
160                    F: FnOnce(usize) -> E + Send + 'static,
161                    E: 'static,
162                {
163                    let this = Clone::clone(self);
164
165                    this.context.spawn({
166                        let this = this.clone();
167
168                        async move {
169                            let result = this.one_async(number).await;
170
171                            this.context.update_app(event(result));
172                        }
173                    });
174                }
175
176                pub async fn one_async(&self, number: usize) -> usize
177                where
178                    E: 'static,
179                {
180                    self.context.request_from_shell(OpOne { number }).await
181                }
182            }
183
184            impl<Ev> crux_core::Capability<Ev> for CapabilityOne<Ev> {
185                type Operation = OpOne;
186                type MappedSelf<MappedEv> = CapabilityOne<MappedEv>;
187
188                fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
189                where
190                    F: Fn(NewEv) -> Ev + Send + Sync + 'static,
191                    Ev: 'static,
192                    NewEv: 'static,
193                {
194                    CapabilityOne::new(self.context.map_event(f))
195                }
196            }
197        }
198
199        pub mod capability_two {
200            use crux_core::capability::{CapabilityContext, Operation};
201            use serde::{Deserialize, Serialize};
202
203            #[derive(PartialEq, Clone, Serialize, Deserialize, Debug)]
204            pub struct OpTwo {
205                number: usize,
206            }
207
208            impl Operation for OpTwo {
209                type Output = usize;
210            }
211
212            pub struct CapabilityTwo<E> {
213                context: CapabilityContext<OpTwo, E>,
214            }
215
216            // Needed to allow 'this = (*self).clone()' without requiring E: Clone
217            // See https://github.com/rust-lang/rust/issues/26925
218            impl<E> Clone for CapabilityTwo<E> {
219                fn clone(&self) -> Self {
220                    Self {
221                        context: self.context.clone(),
222                    }
223                }
224            }
225
226            impl<E> CapabilityTwo<E> {
227                #[must_use]
228                pub fn new(context: CapabilityContext<OpTwo, E>) -> Self {
229                    Self { context }
230                }
231
232                pub fn two<F>(&self, number: usize, event: F)
233                where
234                    F: FnOnce(usize) -> E + Send + 'static,
235                    E: 'static,
236                {
237                    let this = Clone::clone(self);
238
239                    this.context.spawn({
240                        let this = this.clone();
241
242                        async move {
243                            let result = this.two_async(number).await;
244
245                            this.context.update_app(event(result));
246                        }
247                    });
248                }
249
250                pub async fn two_async(&self, number: usize) -> usize
251                where
252                    E: 'static,
253                {
254                    self.context.request_from_shell(OpTwo { number }).await
255                }
256            }
257
258            impl<Ev> crux_core::Capability<Ev> for CapabilityTwo<Ev> {
259                type Operation = OpTwo;
260                type MappedSelf<MappedEv> = CapabilityTwo<MappedEv>;
261
262                fn map_event<F, NewEv>(&self, f: F) -> Self::MappedSelf<NewEv>
263                where
264                    F: Fn(NewEv) -> Ev + Send + Sync + 'static,
265                    Ev: 'static,
266                    NewEv: 'static,
267                {
268                    CapabilityTwo::new(self.context.map_event(f))
269                }
270            }
271        }
272    }
273}