use crate::models::pagination::Meta;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer};
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct BrowseEnvelope<T> {
pub items: Vec<T>,
pub meta: Meta,
}
impl<'de, T: DeserializeOwned> Deserialize<'de> for BrowseEnvelope<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let mut map: serde_json::Map<String, Value> = serde_json::Map::deserialize(deserializer)?;
let meta_val = map.remove("meta").unwrap_or(Value::Null);
let meta = serde_json::from_value::<Meta>(meta_val).map_err(Error::custom)?;
let items_val = map
.into_values()
.find(|v| v.is_array())
.ok_or_else(|| Error::custom("no resource array found in browse envelope"))?;
let items = serde_json::from_value::<Vec<T>>(items_val).map_err(Error::custom)?;
Ok(Self { items, meta })
}
}
#[derive(Debug, Clone)]
pub struct SingleEnvelope<T> {
pub item: T,
}
impl<'de, T: DeserializeOwned> Deserialize<'de> for SingleEnvelope<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let map: serde_json::Map<String, Value> = serde_json::Map::deserialize(deserializer)?;
let items_val = map
.into_iter()
.filter(|(k, _)| k != "meta")
.find_map(|(_, v)| v.is_array().then_some(v))
.ok_or_else(|| Error::custom("no resource array found in single-item envelope"))?;
let mut items = serde_json::from_value::<Vec<T>>(items_val).map_err(Error::custom)?;
let item = items
.drain(..)
.next()
.ok_or_else(|| Error::custom("resource array is empty in single-item envelope"))?;
Ok(Self { item })
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
use serde_json::json;
#[derive(Debug, Clone, PartialEq, Deserialize)]
struct Post {
id: String,
title: String,
#[serde(default)]
status: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
struct Tag {
id: String,
name: String,
slug: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
struct Author {
id: String,
email: String,
}
fn pagination(page: u32, total: u32) -> serde_json::Value {
json!({
"pagination": {
"page": page, "limit": 15,
"pages": 1, "total": total,
"next": null, "prev": null
}
})
}
#[test]
fn test_browse_envelope_posts() {
let v = json!({
"posts": [
{"id": "1", "title": "First", "status": "published"},
{"id": "2", "title": "Second", "status": "draft"}
],
"meta": pagination(1, 2)
});
let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.items.len(), 2);
assert_eq!(env.items[0].id, "1");
assert_eq!(env.items[1].title, "Second");
assert_eq!(env.meta.pagination.total, 2);
assert_eq!(env.meta.pagination.page, 1);
}
#[test]
fn test_browse_envelope_tags() {
let v = json!({
"tags": [{"id": "t1", "name": "Rust", "slug": "rust"}],
"meta": pagination(1, 1)
});
let env: BrowseEnvelope<Tag> = serde_json::from_value(v).unwrap();
assert_eq!(env.items.len(), 1);
assert_eq!(env.items[0].slug, "rust");
assert_eq!(env.meta.pagination.total, 1);
}
#[test]
fn test_browse_envelope_authors() {
let v = json!({
"authors": [{"id": "a1", "email": "jane@example.com"}],
"meta": pagination(1, 1)
});
let env: BrowseEnvelope<Author> = serde_json::from_value(v).unwrap();
assert_eq!(env.items[0].email, "jane@example.com");
}
#[test]
fn test_browse_envelope_empty_list() {
let v = json!({
"posts": [],
"meta": pagination(1, 0)
});
let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
assert!(env.items.is_empty());
assert_eq!(env.meta.pagination.total, 0);
}
#[test]
fn test_browse_envelope_multiple_pages() {
let v = json!({
"posts": [{"id": "p1", "title": "Page 2 post"}],
"meta": {
"pagination": {
"page": 2, "limit": 1,
"pages": 3, "total": 3,
"next": 3, "prev": 1
}
}
});
let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.meta.pagination.page, 2);
assert_eq!(env.meta.pagination.pages, 3);
assert_eq!(env.meta.pagination.next, Some(3));
assert_eq!(env.meta.pagination.prev, Some(1));
}
#[test]
fn test_browse_envelope_extra_fields_ignored() {
let v = json!({
"posts": [{"id": "x", "title": "X"}],
"meta": pagination(1, 1),
"some_future_key": "value"
});
let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.items.len(), 1);
}
#[test]
fn test_browse_envelope_many_items() {
let posts: Vec<serde_json::Value> = (1..=20)
.map(|i| json!({"id": i.to_string(), "title": format!("Post {i}")}))
.collect();
let v = json!({"posts": posts, "meta": pagination(1, 20)});
let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.items.len(), 20);
assert_eq!(env.items[19].title, "Post 20");
}
#[test]
fn test_browse_envelope_missing_array_errors() {
let v = json!({"meta": pagination(1, 0)});
let err = serde_json::from_value::<BrowseEnvelope<Post>>(v).unwrap_err();
assert!(err.to_string().contains("no resource array"));
}
#[test]
fn test_browse_envelope_not_an_object_errors() {
let v = json!([1, 2, 3]);
assert!(serde_json::from_value::<BrowseEnvelope<Post>>(v).is_err());
}
#[test]
fn test_browse_envelope_null_errors() {
assert!(serde_json::from_value::<BrowseEnvelope<Post>>(json!(null)).is_err());
}
#[test]
fn test_browse_envelope_bad_item_type_errors() {
let v = json!({
"posts": [{"id": 99, "title": "Bad", "status": 0}],
"meta": pagination(1, 1)
});
assert!(serde_json::from_value::<BrowseEnvelope<Post>>(v).is_err());
}
#[test]
fn test_single_envelope_posts() {
let v = json!({"posts": [{"id": "abc", "title": "Hello", "status": "published"}]});
let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.item.id, "abc");
assert_eq!(env.item.title, "Hello");
assert_eq!(env.item.status, "published");
}
#[test]
fn test_single_envelope_tags() {
let v = json!({"tags": [{"id": "t1", "name": "Rust", "slug": "rust"}]});
let env: SingleEnvelope<Tag> = serde_json::from_value(v).unwrap();
assert_eq!(env.item.name, "Rust");
assert_eq!(env.item.slug, "rust");
}
#[test]
fn test_single_envelope_authors() {
let v = json!({"authors": [{"id": "a1", "email": "jane@example.com"}]});
let env: SingleEnvelope<Author> = serde_json::from_value(v).unwrap();
assert_eq!(env.item.email, "jane@example.com");
}
#[test]
fn test_single_envelope_ignores_meta_if_present() {
let v = json!({
"posts": [{"id": "x", "title": "With meta"}],
"meta": pagination(1, 1)
});
let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.item.id, "x");
}
#[test]
fn test_single_envelope_extra_fields_ignored() {
let v = json!({
"posts": [{"id": "y", "title": "Extra"}],
"some_key": "ignored"
});
let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.item.id, "y");
}
#[test]
fn test_single_envelope_picks_first_item_when_multiple() {
let v = json!({"posts": [
{"id": "first", "title": "First"},
{"id": "second", "title": "Second"}
]});
let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.item.id, "first");
}
#[test]
fn test_single_envelope_empty_array_errors() {
let v = json!({"posts": []});
let err = serde_json::from_value::<SingleEnvelope<Post>>(v).unwrap_err();
assert!(err.to_string().contains("empty"));
}
#[test]
fn test_single_envelope_missing_array_errors() {
let v = json!({"not_an_array": "hello"});
let err = serde_json::from_value::<SingleEnvelope<Post>>(v).unwrap_err();
assert!(err.to_string().contains("no resource array"));
}
#[test]
fn test_single_envelope_not_an_object_errors() {
assert!(serde_json::from_value::<SingleEnvelope<Post>>(json!(42)).is_err());
}
#[test]
fn test_single_envelope_null_errors() {
assert!(serde_json::from_value::<SingleEnvelope<Post>>(json!(null)).is_err());
}
#[test]
fn test_browse_envelope_clone() {
let v = json!({
"posts": [{"id": "1", "title": "Clone me"}],
"meta": pagination(1, 1)
});
let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
let cloned = env.clone();
assert_eq!(cloned.items[0].id, env.items[0].id);
assert_eq!(cloned.meta.pagination.total, env.meta.pagination.total);
}
#[test]
fn test_browse_envelope_debug() {
let v = json!({
"posts": [{"id": "d1", "title": "Debug"}],
"meta": pagination(1, 1)
});
let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
let debug = format!("{env:?}");
assert!(debug.contains("BrowseEnvelope"));
}
#[test]
fn test_single_envelope_clone() {
let v = json!({"posts": [{"id": "c1", "title": "Clone"}]});
let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
let cloned = env.clone();
assert_eq!(cloned.item.id, env.item.id);
}
#[test]
fn test_single_envelope_debug() {
let v = json!({"posts": [{"id": "d2", "title": "Debug"}]});
let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
let debug = format!("{env:?}");
assert!(debug.contains("SingleEnvelope"));
}
#[test]
fn test_browse_envelope_with_real_post_model() {
use crate::models::post::Post;
let v = json!({
"posts": [{
"id": "5ddc9141c35e7700383b2937",
"title": "Welcome",
"slug": "welcome",
"status": "published"
}],
"meta": pagination(1, 1)
});
let env: BrowseEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.items[0].id, "5ddc9141c35e7700383b2937");
assert_eq!(env.items[0].title, "Welcome");
}
#[test]
fn test_single_envelope_with_real_post_model() {
use crate::models::post::Post;
let v = json!({
"posts": [{
"id": "abc123",
"title": "Created Post",
"slug": "created-post",
"status": "draft"
}]
});
let env: SingleEnvelope<Post> = serde_json::from_value(v).unwrap();
assert_eq!(env.item.id, "abc123");
assert_eq!(env.item.title, "Created Post");
}
}