1#![deny(clippy::pedantic)]
2pub 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 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 let effect = cmd.effects().next().unwrap();
90 let Effect::Http(mut request) = effect else {
91 panic!("Expected a HTTP effect")
92 };
93
94 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 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 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 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 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}