1pub 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 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 let effect = cmd.effects().next().unwrap();
86 let Effect::Http(mut request) = effect else {
87 panic!("Expected a HTTP effect")
88 };
89
90 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 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 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 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 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}