ferro_rs/http/resources/
resource_collection.rs1use super::pagination::PaginationMeta;
2use super::resource::Resource;
3use crate::http::{HttpResponse, Request};
4use serde_json::{json, Value};
5
6pub struct ResourceCollection<T: Resource> {
31 items: Vec<T>,
32 pagination: Option<PaginationMeta>,
33 additional: Option<Value>,
34}
35
36impl<T: Resource> ResourceCollection<T> {
37 pub fn new(items: Vec<T>) -> Self {
39 Self {
40 items,
41 pagination: None,
42 additional: None,
43 }
44 }
45
46 pub fn paginated(items: Vec<T>, meta: PaginationMeta) -> Self {
48 Self {
49 items,
50 pagination: Some(meta),
51 additional: None,
52 }
53 }
54
55 pub fn additional(mut self, value: Value) -> Self {
57 self.additional = Some(value);
58 self
59 }
60
61 pub fn to_response(&self, req: &Request) -> HttpResponse {
67 let data: Vec<Value> = self
68 .items
69 .iter()
70 .map(|item| item.to_resource(req))
71 .collect();
72
73 let mut response = json!({ "data": data });
74
75 if let Some(meta) = &self.pagination {
76 let path = req.path();
77 let query = req.inner().uri().query();
78 let links = meta.links(path, query);
79
80 if let Some(obj) = response.as_object_mut() {
81 obj.insert("meta".to_string(), serde_json::to_value(meta).unwrap());
82 obj.insert("links".to_string(), serde_json::to_value(&links).unwrap());
83 }
84 }
85
86 if let Some(additional) = &self.additional {
87 if let (Some(obj), Some(add)) = (response.as_object_mut(), additional.as_object()) {
88 for (k, v) in add {
89 obj.insert(k.clone(), v.clone());
90 }
91 }
92 }
93
94 HttpResponse::json(response)
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::http::resources::ResourceMap;
102 use serde_json::json;
103 use std::sync::Arc;
104 use tokio::sync::oneshot;
105
106 struct TestItem {
107 id: i32,
108 name: String,
109 }
110
111 impl Resource for TestItem {
112 fn to_resource(&self, _req: &Request) -> Value {
113 ResourceMap::new()
114 .field("id", json!(self.id))
115 .field("name", json!(self.name))
116 .build()
117 }
118 }
119
120 async fn with_test_request_uri<F, R>(uri: &str, f: F) -> R
123 where
124 F: FnOnce(Request) -> R + Send + 'static,
125 R: Send + 'static,
126 {
127 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
128 let addr = listener.local_addr().unwrap();
129 let (result_tx, result_rx) = oneshot::channel();
130
131 let callback = Arc::new(std::sync::Mutex::new(Some(f)));
132 let tx_holder = Arc::new(std::sync::Mutex::new(Some(result_tx)));
133
134 let server = tokio::spawn(async move {
135 let (stream, _) = listener.accept().await.unwrap();
136 let io = hyper_util::rt::TokioIo::new(stream);
137
138 let _ = hyper::server::conn::http1::Builder::new()
139 .serve_connection(
140 io,
141 hyper::service::service_fn({
142 let callback = callback.clone();
143 let tx_holder = tx_holder.clone();
144 move |req| {
145 let cb = callback.lock().unwrap().take();
146 let tx = tx_holder.lock().unwrap().take();
147 let ferro_req = Request::new(req);
148 if let (Some(cb), Some(tx)) = (cb, tx) {
149 let result = cb(ferro_req);
150 let _ = tx.send(result);
151 }
152 async {
153 Ok::<_, std::convert::Infallible>(hyper::Response::new(
154 http_body_util::Full::new(bytes::Bytes::from("ok")),
155 ))
156 }
157 }
158 }),
159 )
160 .await;
161 });
162
163 let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
164 let io = hyper_util::rt::TokioIo::new(stream);
165 let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
166 tokio::spawn(conn);
167
168 let req = hyper::Request::builder()
169 .uri(uri)
170 .body(http_body_util::Empty::<bytes::Bytes>::new())
171 .unwrap();
172 let _ = sender.send_request(req).await;
173
174 let result = result_rx.await.expect("server should have sent result");
175 server.abort();
176 result
177 }
178
179 fn make_items(count: usize) -> Vec<TestItem> {
180 (1..=count)
181 .map(|i| TestItem {
182 id: i as i32,
183 name: format!("Item {i}"),
184 })
185 .collect()
186 }
187
188 #[tokio::test]
189 async fn test_collection_no_pagination() {
190 let items = make_items(2);
191 let result = with_test_request_uri("/items", move |req| {
192 let collection = ResourceCollection::new(items);
193 let response = collection.to_response(&req);
194 let body: Value = serde_json::from_str(response.body()).unwrap();
195 body
196 })
197 .await;
198
199 assert_eq!(
200 result,
201 json!({
202 "data": [
203 {"id": 1, "name": "Item 1"},
204 {"id": 2, "name": "Item 2"}
205 ]
206 })
207 );
208 }
209
210 #[tokio::test]
211 async fn test_collection_with_pagination() {
212 let items = make_items(2);
213 let result = with_test_request_uri("/items?page=1", move |req| {
214 let meta = PaginationMeta::new(1, 2, 6);
215 let collection = ResourceCollection::paginated(items, meta);
216 let response = collection.to_response(&req);
217 let body: Value = serde_json::from_str(response.body()).unwrap();
218 body
219 })
220 .await;
221
222 let obj = result.as_object().unwrap();
223 assert!(obj.contains_key("data"));
224 assert!(obj.contains_key("meta"));
225 assert!(obj.contains_key("links"));
226
227 let meta = &obj["meta"];
228 assert_eq!(meta["current_page"], 1);
229 assert_eq!(meta["per_page"], 2);
230 assert_eq!(meta["total"], 6);
231 assert_eq!(meta["last_page"], 3);
232 assert_eq!(meta["from"], 1);
233 assert_eq!(meta["to"], 2);
234
235 let links = &obj["links"];
236 assert_eq!(links["first"], "/items?page=1");
237 assert_eq!(links["last"], "/items?page=3");
238 assert!(links["prev"].is_null());
239 assert_eq!(links["next"], "/items?page=2");
240 }
241
242 #[tokio::test]
243 async fn test_collection_with_additional() {
244 let items = make_items(1);
245 let result = with_test_request_uri("/items", move |req| {
246 let collection = ResourceCollection::new(items).additional(json!({"version": "v1"}));
247 let response = collection.to_response(&req);
248 let body: Value = serde_json::from_str(response.body()).unwrap();
249 body
250 })
251 .await;
252
253 let obj = result.as_object().unwrap();
254 assert!(obj.contains_key("data"));
255 assert_eq!(obj["version"], "v1");
256 }
257
258 #[tokio::test]
259 async fn test_collection_empty() {
260 let items: Vec<TestItem> = vec![];
261 let result = with_test_request_uri("/items", move |req| {
262 let collection = ResourceCollection::new(items);
263 let response = collection.to_response(&req);
264 let body: Value = serde_json::from_str(response.body()).unwrap();
265 body
266 })
267 .await;
268
269 assert_eq!(result, json!({"data": []}));
270 }
271}