ferro_rs/http/resources/
resource.rs1use crate::http::{HttpResponse, Request};
2
3#[allow(unused_imports)]
4use super::ResourceMap;
5
6pub trait Resource {
35 fn to_resource(&self, req: &Request) -> serde_json::Value;
38
39 fn to_response(&self, req: &Request) -> HttpResponse {
41 HttpResponse::json(self.to_resource(req))
42 }
43
44 fn to_wrapped_response(&self, req: &Request) -> HttpResponse {
46 HttpResponse::json(serde_json::json!({"data": self.to_resource(req)}))
47 }
48
49 fn collection(items: &[Self], req: &Request) -> Vec<serde_json::Value>
61 where
62 Self: Sized,
63 {
64 items.iter().map(|item| item.to_resource(req)).collect()
65 }
66
67 fn to_response_with(&self, req: &Request, additional: serde_json::Value) -> HttpResponse {
76 let mut response = serde_json::json!({"data": self.to_resource(req)});
77 if let (Some(obj), Some(add)) = (response.as_object_mut(), additional.as_object()) {
78 for (k, v) in add {
79 obj.insert(k.clone(), v.clone());
80 }
81 }
82 HttpResponse::json(response)
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use serde_json::json;
90 use std::sync::Arc;
91 use tokio::sync::oneshot;
92
93 struct TestUser {
94 id: i32,
95 name: String,
96 email: String,
97 }
98
99 impl Resource for TestUser {
100 fn to_resource(&self, _req: &Request) -> serde_json::Value {
101 ResourceMap::new()
102 .field("id", json!(self.id))
103 .field("name", json!(self.name))
104 .field("email", json!(self.email))
105 .build()
106 }
107 }
108
109 async fn with_test_request<F, R>(f: F) -> R
112 where
113 F: FnOnce(Request) -> R + Send + 'static,
114 R: Send + 'static,
115 {
116 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
117 let addr = listener.local_addr().unwrap();
118 let (result_tx, result_rx) = oneshot::channel();
119
120 let callback = Arc::new(std::sync::Mutex::new(Some(f)));
121 let tx_holder = Arc::new(std::sync::Mutex::new(Some(result_tx)));
122
123 let server = tokio::spawn(async move {
124 let (stream, _) = listener.accept().await.unwrap();
125 let io = hyper_util::rt::TokioIo::new(stream);
126
127 let _ = hyper::server::conn::http1::Builder::new()
128 .serve_connection(
129 io,
130 hyper::service::service_fn({
131 let callback = callback.clone();
132 let tx_holder = tx_holder.clone();
133 move |req| {
134 let cb = callback.lock().unwrap().take();
135 let tx = tx_holder.lock().unwrap().take();
136 let ferro_req = Request::new(req);
137 if let (Some(cb), Some(tx)) = (cb, tx) {
138 let result = cb(ferro_req);
139 let _ = tx.send(result);
140 }
141 async {
142 Ok::<_, std::convert::Infallible>(hyper::Response::new(
143 http_body_util::Full::new(bytes::Bytes::from("ok")),
144 ))
145 }
146 }
147 }),
148 )
149 .await;
150 });
151
152 let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
154 let io = hyper_util::rt::TokioIo::new(stream);
155 let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
156 tokio::spawn(conn);
157
158 let req = hyper::Request::builder()
159 .uri("/")
160 .body(http_body_util::Empty::<bytes::Bytes>::new())
161 .unwrap();
162 let _ = sender.send_request(req).await;
163
164 let result = result_rx.await.expect("server should have sent result");
165 server.abort();
166 result
167 }
168
169 #[tokio::test]
170 async fn test_resource_to_resource() {
171 let user = TestUser {
172 id: 42,
173 name: "Alice".to_string(),
174 email: "alice@example.com".to_string(),
175 };
176
177 let result = with_test_request(move |req| user.to_resource(&req)).await;
178 assert_eq!(
179 result,
180 json!({"id": 42, "name": "Alice", "email": "alice@example.com"})
181 );
182 }
183
184 #[tokio::test]
185 async fn test_resource_to_response() {
186 let user = TestUser {
187 id: 1,
188 name: "Bob".to_string(),
189 email: "bob@example.com".to_string(),
190 };
191
192 let status = with_test_request(move |req| user.to_response(&req).status_code()).await;
193 assert_eq!(status, 200);
194 }
195
196 #[tokio::test]
197 async fn test_resource_to_wrapped_response() {
198 let user = TestUser {
199 id: 1,
200 name: "Bob".to_string(),
201 email: "bob@example.com".to_string(),
202 };
203
204 let status =
205 with_test_request(move |req| user.to_wrapped_response(&req).status_code()).await;
206 assert_eq!(status, 200);
207 }
208
209 #[tokio::test]
210 async fn test_resource_collection() {
211 let users = vec![
212 TestUser {
213 id: 1,
214 name: "Alice".to_string(),
215 email: "alice@example.com".to_string(),
216 },
217 TestUser {
218 id: 2,
219 name: "Bob".to_string(),
220 email: "bob@example.com".to_string(),
221 },
222 TestUser {
223 id: 3,
224 name: "Charlie".to_string(),
225 email: "charlie@example.com".to_string(),
226 },
227 ];
228
229 let result = with_test_request(move |req| TestUser::collection(&users, &req)).await;
230
231 assert_eq!(result.len(), 3);
232 assert_eq!(
233 result[0],
234 json!({"id": 1, "name": "Alice", "email": "alice@example.com"})
235 );
236 assert_eq!(
237 result[1],
238 json!({"id": 2, "name": "Bob", "email": "bob@example.com"})
239 );
240 assert_eq!(
241 result[2],
242 json!({"id": 3, "name": "Charlie", "email": "charlie@example.com"})
243 );
244 }
245}