1use crate::models::pagination::Meta;
47use serde::de::DeserializeOwned;
48use serde::{Deserialize, Deserializer};
49use serde_json::Value;
50
51#[derive(Debug, Clone)]
92pub struct BrowseEnvelope<T> {
93 pub items: Vec<T>,
95 pub meta: Meta,
97}
98
99impl<'de, T: DeserializeOwned> Deserialize<'de> for BrowseEnvelope<T> {
100 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101 use serde::de::Error;
102
103 let mut map: serde_json::Map<String, Value> = serde_json::Map::deserialize(deserializer)?;
104
105 let meta_val = map.remove("meta").unwrap_or(Value::Null);
108 let meta = serde_json::from_value::<Meta>(meta_val).map_err(Error::custom)?;
109
110 let items_val = map
113 .into_values()
114 .find(|v| v.is_array())
115 .ok_or_else(|| Error::custom("no resource array found in browse envelope"))?;
116
117 let items = serde_json::from_value::<Vec<T>>(items_val).map_err(Error::custom)?;
118
119 Ok(Self { items, meta })
120 }
121}
122
123#[derive(Debug, Clone)]
161pub struct SingleEnvelope<T> {
162 pub item: T,
164}
165
166impl<'de, T: DeserializeOwned> Deserialize<'de> for SingleEnvelope<T> {
167 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
168 use serde::de::Error;
169
170 let map: serde_json::Map<String, Value> = serde_json::Map::deserialize(deserializer)?;
171
172 let items_val = map
175 .into_iter()
176 .filter(|(k, _)| k != "meta")
177 .find_map(|(_, v)| v.is_array().then_some(v))
178 .ok_or_else(|| Error::custom("no resource array found in single-item envelope"))?;
179
180 let mut items = serde_json::from_value::<Vec<T>>(items_val).map_err(Error::custom)?;
181
182 let item = items
183 .drain(..)
184 .next()
185 .ok_or_else(|| Error::custom("resource array is empty in single-item envelope"))?;
186
187 Ok(Self { item })
188 }
189}
190
191#[cfg(test)]
194mod tests {
195 use super::*;
196 use serde::Deserialize;
197 use serde_json::json;
198
199 #[derive(Debug, Clone, PartialEq, Deserialize)]
202 struct Post {
203 id: String,
204 title: String,
205 #[serde(default)]
206 status: String,
207 }
208
209 #[derive(Debug, Clone, PartialEq, Deserialize)]
210 struct Tag {
211 id: String,
212 name: String,
213 slug: String,
214 }
215
216 #[derive(Debug, Clone, PartialEq, Deserialize)]
217 struct Author {
218 id: String,
219 email: String,
220 }
221
222 fn pagination(page: u32, total: u32) -> serde_json::Value {
223 json!({
224 "pagination": {
225 "page": page, "limit": 15,
226 "pages": 1, "total": total,
227 "next": null, "prev": null
228 }
229 })
230 }
231
232 #[test]
235 fn test_browse_envelope_posts() {
236 let v = json!({
237 "posts": [
238 {"id": "1", "title": "First", "status": "published"},
239 {"id": "2", "title": "Second", "status": "draft"}
240 ],
241 "meta": pagination(1, 2)
242 });
243 let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
244 assert_eq!(env.items.len(), 2);
245 assert_eq!(env.items[0].id, "1");
246 assert_eq!(env.items[1].title, "Second");
247 assert_eq!(env.meta.pagination.total, 2);
248 assert_eq!(env.meta.pagination.page, 1);
249 }
250
251 #[test]
252 fn test_browse_envelope_tags() {
253 let v = json!({
254 "tags": [{"id": "t1", "name": "Rust", "slug": "rust"}],
255 "meta": pagination(1, 1)
256 });
257 let env: BrowseEnvelope<Tag> = serde_json::from_value(v).unwrap();
258 assert_eq!(env.items.len(), 1);
259 assert_eq!(env.items[0].slug, "rust");
260 assert_eq!(env.meta.pagination.total, 1);
261 }
262
263 #[test]
264 fn test_browse_envelope_authors() {
265 let v = json!({
266 "authors": [{"id": "a1", "email": "jane@example.com"}],
267 "meta": pagination(1, 1)
268 });
269 let env: BrowseEnvelope<Author> = serde_json::from_value(v).unwrap();
270 assert_eq!(env.items[0].email, "jane@example.com");
271 }
272
273 #[test]
274 fn test_browse_envelope_empty_list() {
275 let v = json!({
276 "posts": [],
277 "meta": pagination(1, 0)
278 });
279 let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
280 assert!(env.items.is_empty());
281 assert_eq!(env.meta.pagination.total, 0);
282 }
283
284 #[test]
285 fn test_browse_envelope_multiple_pages() {
286 let v = json!({
287 "posts": [{"id": "p1", "title": "Page 2 post"}],
288 "meta": {
289 "pagination": {
290 "page": 2, "limit": 1,
291 "pages": 3, "total": 3,
292 "next": 3, "prev": 1
293 }
294 }
295 });
296 let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
297 assert_eq!(env.meta.pagination.page, 2);
298 assert_eq!(env.meta.pagination.pages, 3);
299 assert_eq!(env.meta.pagination.next, Some(3));
300 assert_eq!(env.meta.pagination.prev, Some(1));
301 }
302
303 #[test]
304 fn test_browse_envelope_extra_fields_ignored() {
305 let v = json!({
307 "posts": [{"id": "x", "title": "X"}],
308 "meta": pagination(1, 1),
309 "some_future_key": "value"
310 });
311 let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
312 assert_eq!(env.items.len(), 1);
313 }
314
315 #[test]
316 fn test_browse_envelope_many_items() {
317 let posts: Vec<serde_json::Value> = (1..=20)
318 .map(|i| json!({"id": i.to_string(), "title": format!("Post {i}")}))
319 .collect();
320 let v = json!({"posts": posts, "meta": pagination(1, 20)});
321 let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
322 assert_eq!(env.items.len(), 20);
323 assert_eq!(env.items[19].title, "Post 20");
324 }
325
326 #[test]
329 fn test_browse_envelope_missing_array_errors() {
330 let v = json!({"meta": pagination(1, 0)});
331 let err = serde_json::from_value::<BrowseEnvelope<Post>>(v).unwrap_err();
332 assert!(err.to_string().contains("no resource array"));
333 }
334
335 #[test]
336 fn test_browse_envelope_not_an_object_errors() {
337 let v = json!([1, 2, 3]);
338 assert!(serde_json::from_value::<BrowseEnvelope<Post>>(v).is_err());
339 }
340
341 #[test]
342 fn test_browse_envelope_null_errors() {
343 assert!(serde_json::from_value::<BrowseEnvelope<Post>>(json!(null)).is_err());
344 }
345
346 #[test]
347 fn test_browse_envelope_bad_item_type_errors() {
348 let v = json!({
351 "posts": [{"id": 99, "title": "Bad", "status": 0}],
352 "meta": pagination(1, 1)
353 });
354 assert!(serde_json::from_value::<BrowseEnvelope<Post>>(v).is_err());
356 }
357
358 #[test]
361 fn test_single_envelope_posts() {
362 let v = json!({"posts": [{"id": "abc", "title": "Hello", "status": "published"}]});
363 let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
364 assert_eq!(env.item.id, "abc");
365 assert_eq!(env.item.title, "Hello");
366 assert_eq!(env.item.status, "published");
367 }
368
369 #[test]
370 fn test_single_envelope_tags() {
371 let v = json!({"tags": [{"id": "t1", "name": "Rust", "slug": "rust"}]});
372 let env: SingleEnvelope<Tag> = serde_json::from_value(v).unwrap();
373 assert_eq!(env.item.name, "Rust");
374 assert_eq!(env.item.slug, "rust");
375 }
376
377 #[test]
378 fn test_single_envelope_authors() {
379 let v = json!({"authors": [{"id": "a1", "email": "jane@example.com"}]});
380 let env: SingleEnvelope<Author> = serde_json::from_value(v).unwrap();
381 assert_eq!(env.item.email, "jane@example.com");
382 }
383
384 #[test]
385 fn test_single_envelope_ignores_meta_if_present() {
386 let v = json!({
388 "posts": [{"id": "x", "title": "With meta"}],
389 "meta": pagination(1, 1)
390 });
391 let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
392 assert_eq!(env.item.id, "x");
393 }
394
395 #[test]
396 fn test_single_envelope_extra_fields_ignored() {
397 let v = json!({
398 "posts": [{"id": "y", "title": "Extra"}],
399 "some_key": "ignored"
400 });
401 let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
402 assert_eq!(env.item.id, "y");
403 }
404
405 #[test]
406 fn test_single_envelope_picks_first_item_when_multiple() {
407 let v = json!({"posts": [
409 {"id": "first", "title": "First"},
410 {"id": "second", "title": "Second"}
411 ]});
412 let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
413 assert_eq!(env.item.id, "first");
414 }
415
416 #[test]
419 fn test_single_envelope_empty_array_errors() {
420 let v = json!({"posts": []});
421 let err = serde_json::from_value::<SingleEnvelope<Post>>(v).unwrap_err();
422 assert!(err.to_string().contains("empty"));
423 }
424
425 #[test]
426 fn test_single_envelope_missing_array_errors() {
427 let v = json!({"not_an_array": "hello"});
428 let err = serde_json::from_value::<SingleEnvelope<Post>>(v).unwrap_err();
429 assert!(err.to_string().contains("no resource array"));
430 }
431
432 #[test]
433 fn test_single_envelope_not_an_object_errors() {
434 assert!(serde_json::from_value::<SingleEnvelope<Post>>(json!(42)).is_err());
435 }
436
437 #[test]
438 fn test_single_envelope_null_errors() {
439 assert!(serde_json::from_value::<SingleEnvelope<Post>>(json!(null)).is_err());
440 }
441
442 #[test]
445 fn test_browse_envelope_clone() {
446 let v = json!({
447 "posts": [{"id": "1", "title": "Clone me"}],
448 "meta": pagination(1, 1)
449 });
450 let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
451 let cloned = env.clone();
452 assert_eq!(cloned.items[0].id, env.items[0].id);
453 assert_eq!(cloned.meta.pagination.total, env.meta.pagination.total);
454 }
455
456 #[test]
457 fn test_browse_envelope_debug() {
458 let v = json!({
459 "posts": [{"id": "d1", "title": "Debug"}],
460 "meta": pagination(1, 1)
461 });
462 let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
463 let debug = format!("{env:?}");
464 assert!(debug.contains("BrowseEnvelope"));
465 }
466
467 #[test]
468 fn test_single_envelope_clone() {
469 let v = json!({"posts": [{"id": "c1", "title": "Clone"}]});
470 let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
471 let cloned = env.clone();
472 assert_eq!(cloned.item.id, env.item.id);
473 }
474
475 #[test]
476 fn test_single_envelope_debug() {
477 let v = json!({"posts": [{"id": "d2", "title": "Debug"}]});
478 let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
479 let debug = format!("{env:?}");
480 assert!(debug.contains("SingleEnvelope"));
481 }
482
483 #[test]
486 fn test_browse_envelope_with_real_post_model() {
487 use crate::models::post::Post;
488 let v = json!({
489 "posts": [{
490 "id": "5ddc9141c35e7700383b2937",
491 "title": "Welcome",
492 "slug": "welcome",
493 "status": "published"
494 }],
495 "meta": pagination(1, 1)
496 });
497 let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
498 assert_eq!(env.items[0].id, "5ddc9141c35e7700383b2937");
499 assert_eq!(env.items[0].title, "Welcome");
500 }
501
502 #[test]
503 fn test_single_envelope_with_real_post_model() {
504 use crate::models::post::Post;
505 let v = json!({
506 "posts": [{
507 "id": "abc123",
508 "title": "Created Post",
509 "slug": "created-post",
510 "status": "draft"
511 }]
512 });
513 let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
514 assert_eq!(env.item.id, "abc123");
515 assert_eq!(env.item.title, "Created Post");
516 }
517}