doctest_support/
lib.rs

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