use serde::de::{self, DeserializeOwned};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::{ApiError, JsonApiObject, Links, Meta, Resource, ResourceObject};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum PrimaryData<T> {
Null,
Single(Box<T>),
Many(Vec<T>),
}
impl<T: Serialize> Serialize for PrimaryData<T> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
PrimaryData::Null => serializer.serialize_none(),
PrimaryData::Single(item) => item.serialize(serializer),
PrimaryData::Many(items) => items.serialize(serializer),
}
}
}
impl<'de, T: DeserializeOwned> Deserialize<'de> for PrimaryData<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::Null => Ok(PrimaryData::Null),
serde_json::Value::Array(arr) => {
let items: Vec<T> = arr
.into_iter()
.map(serde_json::from_value)
.collect::<Result<_, _>>()
.map_err(de::Error::custom)?;
Ok(PrimaryData::Many(items))
}
other => {
let item: T = serde_json::from_value(other).map_err(de::Error::custom)?;
Ok(PrimaryData::Single(Box::new(item)))
}
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum Document<P, I = Resource> {
Data {
data: PrimaryData<P>,
included: Vec<I>,
meta: Option<Meta>,
jsonapi: Option<JsonApiObject>,
links: Option<Links>,
},
Errors {
errors: Vec<ApiError>,
meta: Option<Meta>,
jsonapi: Option<JsonApiObject>,
links: Option<Links>,
},
Meta {
meta: Meta,
jsonapi: Option<JsonApiObject>,
links: Option<Links>,
},
}
impl<P: ResourceObject, I: Serialize> Serialize for Document<P, I> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = serializer.serialize_map(None)?;
match self {
Document::Data {
data,
included,
meta,
jsonapi,
links,
} => {
map.serialize_entry("data", data)?;
if !included.is_empty() {
map.serialize_entry("included", included)?;
}
if let Some(m) = meta {
map.serialize_entry("meta", m)?;
}
if let Some(j) = jsonapi {
map.serialize_entry("jsonapi", j)?;
}
if let Some(l) = links {
map.serialize_entry("links", l)?;
}
}
Document::Errors {
errors,
meta,
jsonapi,
links,
} => {
map.serialize_entry("errors", errors)?;
if let Some(m) = meta {
map.serialize_entry("meta", m)?;
}
if let Some(j) = jsonapi {
map.serialize_entry("jsonapi", j)?;
}
if let Some(l) = links {
map.serialize_entry("links", l)?;
}
}
Document::Meta {
meta,
jsonapi,
links,
} => {
map.serialize_entry("meta", meta)?;
if let Some(j) = jsonapi {
map.serialize_entry("jsonapi", j)?;
}
if let Some(l) = links {
map.serialize_entry("links", l)?;
}
}
}
map.end()
}
}
impl<'de, P, I> Deserialize<'de> for Document<P, I>
where
P: ResourceObject + DeserializeOwned,
I: DeserializeOwned,
{
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
let obj = value
.as_object()
.ok_or_else(|| de::Error::custom("document must be a JSON object"))?;
let has_data = obj.contains_key("data");
let has_errors = obj.contains_key("errors");
if has_data && has_errors {
return Err(de::Error::custom(
"document must not contain both `data` and `errors`",
));
}
let meta: Option<Meta> = obj
.get("meta")
.map(|v| serde_json::from_value(v.clone()))
.transpose()
.map_err(de::Error::custom)?;
let jsonapi: Option<JsonApiObject> = obj
.get("jsonapi")
.map(|v| serde_json::from_value(v.clone()))
.transpose()
.map_err(de::Error::custom)?;
let links: Option<Links> = obj
.get("links")
.map(|v| serde_json::from_value(v.clone()))
.transpose()
.map_err(de::Error::custom)?;
if has_data {
let data: PrimaryData<P> = serde_json::from_value(obj["data"].clone())
.map_err(|e| de::Error::custom(format!("in primary data: {e}")))?;
let included: Vec<I> = match obj.get("included") {
Some(v) => {
let arr = v
.as_array()
.ok_or_else(|| de::Error::custom("`included` must be a JSON array"))?;
let mut out = Vec::with_capacity(arr.len());
for (idx, entry) in arr.iter().enumerate() {
let parsed: I = serde_json::from_value(entry.clone())
.map_err(|e| de::Error::custom(format!("in included[{idx}]: {e}")))?;
out.push(parsed);
}
out
}
None => Vec::new(),
};
Ok(Document::Data {
data,
included,
meta,
jsonapi,
links,
})
} else if has_errors {
let errors: Vec<ApiError> =
serde_json::from_value(obj["errors"].clone()).map_err(de::Error::custom)?;
Ok(Document::Errors {
errors,
meta,
jsonapi,
links,
})
} else if let Some(m) = meta {
Ok(Document::Meta {
meta: m,
jsonapi,
links,
})
} else {
Err(de::Error::custom(
"document must contain `data`, `errors`, or `meta`",
))
}
}
}
impl<P, I: ResourceObject> Document<P, I> {
pub fn registry(&self) -> crate::Result<crate::registry::Registry> {
match self {
Document::Data { included, .. } => crate::registry::Registry::from_included(included),
_ => Ok(crate::registry::Registry::new()),
}
}
}
impl<P, I> Document<P, I> {
pub fn into_single(self) -> crate::Result<P> {
match self {
Document::Data {
data: PrimaryData::Single(boxed),
..
} => Ok(*boxed),
Document::Data {
data: PrimaryData::Many(_),
..
} => Err(unexpected_shape("single resource", "resource collection")),
Document::Data {
data: PrimaryData::Null,
..
} => Err(unexpected_shape("single resource", "null primary data")),
Document::Errors { .. } => Err(unexpected_shape("single resource", "errors document")),
Document::Meta { .. } => Err(unexpected_shape("single resource", "meta-only document")),
}
}
pub fn into_many(self) -> crate::Result<Vec<P>> {
match self {
Document::Data {
data: PrimaryData::Many(items),
..
} => Ok(items),
Document::Data {
data: PrimaryData::Single(_),
..
} => Err(unexpected_shape("resource collection", "single resource")),
Document::Data {
data: PrimaryData::Null,
..
} => Err(unexpected_shape("resource collection", "null primary data")),
Document::Errors { .. } => {
Err(unexpected_shape("resource collection", "errors document"))
}
Document::Meta { .. } => Err(unexpected_shape(
"resource collection",
"meta-only document",
)),
}
}
pub fn into_meta(self) -> crate::Result<Meta> {
match self {
Document::Meta { meta, .. } => Ok(meta),
Document::Data {
meta: Some(meta), ..
} => Ok(meta),
Document::Errors {
meta: Some(meta), ..
} => Ok(meta),
Document::Data { .. } => Err(unexpected_shape(
"meta-only or data-with-meta",
"data document without meta",
)),
Document::Errors { .. } => Err(unexpected_shape(
"meta-only or errors-with-meta",
"errors document without meta",
)),
}
}
pub fn as_single(&self) -> crate::Result<&P> {
match self {
Document::Data {
data: PrimaryData::Single(boxed),
..
} => Ok(boxed.as_ref()),
Document::Data {
data: PrimaryData::Many(_),
..
} => Err(unexpected_shape("single resource", "resource collection")),
Document::Data {
data: PrimaryData::Null,
..
} => Err(unexpected_shape("single resource", "null primary data")),
Document::Errors { .. } => Err(unexpected_shape("single resource", "errors document")),
Document::Meta { .. } => Err(unexpected_shape("single resource", "meta-only document")),
}
}
pub fn as_many(&self) -> crate::Result<&[P]> {
match self {
Document::Data {
data: PrimaryData::Many(items),
..
} => Ok(items.as_slice()),
Document::Data {
data: PrimaryData::Single(_),
..
} => Err(unexpected_shape("resource collection", "single resource")),
Document::Data {
data: PrimaryData::Null,
..
} => Err(unexpected_shape("resource collection", "null primary data")),
Document::Errors { .. } => {
Err(unexpected_shape("resource collection", "errors document"))
}
Document::Meta { .. } => Err(unexpected_shape(
"resource collection",
"meta-only document",
)),
}
}
pub fn primary(&self) -> crate::Result<&PrimaryData<P>> {
match self {
Document::Data { data, .. } => Ok(data),
Document::Errors { .. } => Err(unexpected_shape("data document", "errors document")),
Document::Meta { .. } => Err(unexpected_shape("data document", "meta-only document")),
}
}
#[must_use]
pub fn included(&self) -> &[I] {
match self {
Document::Data { included, .. } => included.as_slice(),
_ => &[],
}
}
}
#[inline]
fn unexpected_shape(expected: &'static str, found: &'static str) -> crate::Error {
crate::Error::UnexpectedDocumentShape { expected, found }
}
impl<P, I> Document<P, I>
where
P: ResourceObject + DeserializeOwned,
I: DeserializeOwned,
{
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> crate::Result<Self> {
let value: serde_json::Value = serde_json::from_str(s)?;
Self::from_value(value)
}
pub fn from_slice(bytes: &[u8]) -> crate::Result<Self> {
let value: serde_json::Value = serde_json::from_slice(bytes)?;
Self::from_value(value)
}
pub fn from_value(value: serde_json::Value) -> crate::Result<Self> {
prevalidate::<P>(&value)?;
serde_json::from_value(value).map_err(crate::Error::Json)
}
}
fn build_included_set(value: &serde_json::Value) -> std::collections::HashSet<(String, String)> {
let mut set = std::collections::HashSet::new();
let Some(arr) = value.get("included").and_then(|v| v.as_array()) else {
return set;
};
for entry in arr {
let Some(obj) = entry.as_object() else {
continue;
};
let (Some(type_), Some(id)) = (
obj.get("type").and_then(|v| v.as_str()),
obj.get("id").and_then(|v| v.as_str()),
) else {
continue;
};
set.insert((type_.to_string(), id.to_string()));
}
set
}
fn prevalidate<P: ResourceObject>(value: &serde_json::Value) -> crate::Result<()> {
let info = P::type_info();
let obj = match value.as_object() {
Some(obj) => obj,
None => return Ok(()),
};
let Some(data) = obj.get("data") else {
return Ok(());
};
let included_set = build_included_set(value);
match data {
serde_json::Value::Object(_) => {
check_resource_full(data, "data", &info, &included_set)?;
}
serde_json::Value::Array(arr) => {
for (idx, item) in arr.iter().enumerate() {
let location = format!("data[{idx}]");
check_resource_full(item, &location, &info, &included_set)?;
}
}
_ => {}
}
Ok(())
}
fn check_resource_full(
item: &serde_json::Value,
location: &str,
info: &crate::TypeInfo,
included_set: &std::collections::HashSet<(String, String)>,
) -> crate::Result<()> {
let obj = match item.as_object() {
Some(o) => o,
None => return Ok(()),
};
if !info.type_name.is_empty()
&& let Some(serde_json::Value::String(got)) = obj.get("type")
&& got != info.type_name
{
return Err(crate::Error::TypeMismatch {
expected: info.type_name,
got: got.clone(),
location: location.to_string(),
});
}
if !info.type_name.is_empty() && !info.required_attribute_names.is_empty() {
match obj.get("attributes") {
Some(serde_json::Value::Object(attrs)) => {
for &required in info.required_attribute_names {
if !attrs.contains_key(required) {
return Err(crate::Error::MissingAttribute {
resource_type: info.type_name,
attribute: required,
location: location.to_string(),
});
}
}
}
None => {
return Err(crate::Error::MissingAttribute {
resource_type: info.type_name,
attribute: info.required_attribute_names[0],
location: location.to_string(),
});
}
Some(_) => {
}
}
}
if let Some(rels) = obj.get("relationships").and_then(|v| v.as_object()) {
for (name, rel_value) in rels {
check_relationship(name, rel_value, location, included_set)?;
}
}
Ok(())
}
fn check_included_ref(
name: &str,
rel_location: &str,
identity: &serde_json::Value,
included_set: &std::collections::HashSet<(String, String)>,
) -> crate::Result<()> {
if included_set.is_empty() {
return Ok(());
}
let Some(obj) = identity.as_object() else {
return Ok(());
};
let Some(type_) = obj.get("type").and_then(|v| v.as_str()) else {
return Ok(());
};
let Some(id) = obj.get("id").and_then(|v| v.as_str()) else {
return Ok(());
};
let key = (type_.to_string(), id.to_string());
if !included_set.contains(&key) {
return Err(crate::Error::IncludedRefMissing {
name: name.to_string(),
type_: type_.to_string(),
id: id.to_string(),
location: rel_location.to_string(),
});
}
Ok(())
}
fn check_relationship(
name: &str,
rel_value: &serde_json::Value,
location: &str,
included_set: &std::collections::HashSet<(String, String)>,
) -> crate::Result<()> {
let rel_obj = rel_value
.as_object()
.ok_or_else(|| crate::Error::MalformedRelationship {
name: name.to_string(),
location: location.to_string(),
reason: "relationship value must be an object".into(),
})?;
let rel_location = format!("{location}.relationships.{name}");
if let Some(data) = rel_obj.get("data") {
match data {
serde_json::Value::Null => {}
serde_json::Value::Object(_) => {
check_included_ref(name, &rel_location, data, included_set)?;
}
serde_json::Value::Array(arr) => {
for entry in arr {
check_included_ref(name, &rel_location, entry, included_set)?;
}
}
other => {
let kind = match other {
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
_ => "unknown",
};
return Err(crate::Error::MalformedRelationship {
name: name.to_string(),
location: location.to_string(),
reason: format!("`data` must be null, an object, or an array; got {kind}"),
});
}
}
}
Ok(())
}
#[cfg(test)]
mod prepass_helpers {
use super::*;
use std::collections::HashSet;
#[test]
fn build_included_set_indexes_type_and_id_pairs() {
let v: serde_json::Value = serde_json::from_str(
r#"{
"included": [
{ "type": "people", "id": "1", "attributes": {} },
{ "type": "tags", "id": "5", "attributes": {} }
]
}"#,
)
.unwrap();
let set = build_included_set(&v);
let expected: HashSet<(String, String)> = [
("people".to_string(), "1".to_string()),
("tags".to_string(), "5".to_string()),
]
.into_iter()
.collect();
assert_eq!(set, expected);
}
#[test]
fn build_included_set_returns_empty_when_included_absent() {
let v: serde_json::Value = serde_json::from_str(r#"{ "data": null }"#).unwrap();
let set = build_included_set(&v);
assert!(set.is_empty());
}
#[test]
fn build_included_set_skips_entries_missing_type_or_id() {
let v: serde_json::Value = serde_json::from_str(
r#"{
"included": [
{ "type": "people", "id": "1" },
{ "type": "people" },
{ "id": "2" },
{ "type": "tags", "id": "5" }
]
}"#,
)
.unwrap();
let set = build_included_set(&v);
assert_eq!(set.len(), 2);
assert!(set.contains(&("people".to_string(), "1".to_string())));
assert!(set.contains(&("tags".to_string(), "5".to_string())));
}
#[test]
fn check_included_ref_passes_for_present_id() {
let identity = serde_json::json!({ "type": "people", "id": "1" });
let mut set = HashSet::new();
set.insert(("people".to_string(), "1".to_string()));
assert!(check_included_ref("author", "data.relationships.author", &identity, &set).is_ok());
}
#[test]
fn check_included_ref_fails_for_absent_id() {
let identity = serde_json::json!({ "type": "people", "id": "9" });
let mut set: HashSet<(String, String)> = HashSet::new();
set.insert(("people".to_string(), "1".to_string()));
let err =
check_included_ref("author", "data.relationships.author", &identity, &set).unwrap_err();
assert!(
matches!(
&err,
crate::Error::IncludedRefMissing { name, type_, id, location }
if name == "author"
&& type_ == "people"
&& id == "9"
&& location == "data.relationships.author"
),
"got: {err:?}",
);
}
#[test]
fn check_included_ref_skips_when_set_is_empty() {
let identity = serde_json::json!({ "type": "people", "id": "9" });
let set: HashSet<(String, String)> = HashSet::new();
assert!(check_included_ref("author", "data.relationships.author", &identity, &set).is_ok());
}
#[test]
fn check_included_ref_skips_lid_only_identifier() {
let identity = serde_json::json!({ "type": "people", "lid": "tmp-1" });
let mut set: HashSet<(String, String)> = HashSet::new();
set.insert(("other".to_string(), "x".to_string()));
assert!(check_included_ref("author", "data.relationships.author", &identity, &set).is_ok());
}
#[test]
fn check_included_ref_skips_non_object_identity() {
let identity = serde_json::Value::Null;
let mut set: HashSet<(String, String)> = HashSet::new();
set.insert(("other".to_string(), "x".to_string()));
assert!(check_included_ref("author", "data.relationships.author", &identity, &set).is_ok());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::Resource;
#[test]
fn test_primary_data_null() {
let json = "null";
let data: PrimaryData<Resource> = serde_json::from_str(json).unwrap();
assert!(matches!(data, PrimaryData::Null));
assert_eq!(serde_json::to_string(&data).unwrap(), json);
}
#[test]
fn test_primary_data_single() {
let json = r#"{"type":"articles","id":"1","attributes":{"title":"Hello"}}"#;
let data: PrimaryData<Resource> = serde_json::from_str(json).unwrap();
match &data {
PrimaryData::Single(r) => assert_eq!(r.resource_type(), "articles"),
_ => panic!("expected Single"),
}
}
#[test]
fn test_primary_data_many() {
let json = r#"[{"type":"articles","id":"1","attributes":{}},{"type":"articles","id":"2","attributes":{}}]"#;
let data: PrimaryData<Resource> = serde_json::from_str(json).unwrap();
match &data {
PrimaryData::Many(v) => assert_eq!(v.len(), 2),
_ => panic!("expected Many"),
}
}
#[test]
fn test_document_data() {
let json = r#"{
"data": {"type":"articles","id":"1","attributes":{"title":"Hello"}},
"jsonapi": {"version":"1.1"}
}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
match &doc {
Document::Data { data, jsonapi, .. } => {
assert!(matches!(data, PrimaryData::Single(_)));
assert_eq!(jsonapi.as_ref().unwrap().version.as_deref(), Some("1.1"));
}
_ => panic!("expected Document::Data"),
}
}
#[test]
fn test_document_data_collection() {
let json = r#"{"data":[{"type":"articles","id":"1","attributes":{}},{"type":"articles","id":"2","attributes":{}}]}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
match &doc {
Document::Data {
data: PrimaryData::Many(v),
..
} => assert_eq!(v.len(), 2),
_ => panic!("expected Document::Data with Many"),
}
}
#[test]
fn test_document_data_null() {
let json = r#"{"data":null}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
match &doc {
Document::Data {
data: PrimaryData::Null,
..
} => {}
_ => panic!("expected Document::Data with Null"),
}
}
#[test]
fn test_document_errors() {
let json = r#"{"errors":[{"status":"404","title":"Not Found"}]}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
match &doc {
Document::Errors { errors, .. } => {
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].status.as_deref(), Some("404"));
}
_ => panic!("expected Document::Errors"),
}
}
#[test]
fn test_document_meta_only() {
let json = r#"{"meta":{"total":0}}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
assert!(matches!(doc, Document::Meta { .. }));
}
#[test]
fn test_document_rejects_data_and_errors() {
let json = r#"{"data":null,"errors":[]}"#;
let result: std::result::Result<Document<Resource>, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_document_with_included() {
let json = r#"{
"data": {"type":"articles","id":"1","attributes":{"title":"Hello"}},
"included": [{"type":"people","id":"9","attributes":{"name":"Dan"}}]
}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
match &doc {
Document::Data { included, .. } => {
assert_eq!(included.len(), 1);
assert_eq!(included[0].resource_type(), "people");
}
_ => panic!("expected Document::Data"),
}
}
#[test]
fn test_document_data_round_trip() {
let doc: Document<Resource> = Document::Data {
data: PrimaryData::Single(Box::new(Resource {
type_: "articles".into(),
id: Some("1".into()),
lid: None,
attributes: serde_json::json!({"title": "Hello"}),
relationships: std::collections::BTreeMap::new(),
links: None,
meta: None,
})),
included: vec![],
meta: None,
jsonapi: None,
links: None,
};
let json = serde_json::to_string(&doc).unwrap();
let deserialized: Document<Resource> = serde_json::from_str(&json).unwrap();
match (&doc, &deserialized) {
(Document::Data { data: d1, .. }, Document::Data { data: d2, .. }) => match (d1, d2) {
(PrimaryData::Single(a), PrimaryData::Single(b)) => {
assert_eq!(a.type_, b.type_);
assert_eq!(a.id, b.id);
}
_ => panic!("mismatch"),
},
_ => panic!("mismatch"),
}
}
#[test]
fn test_document_errors_round_trip() {
let doc: Document<Resource> = Document::Errors {
errors: vec![ApiError {
id: None,
links: None,
status: Some("500".into()),
code: None,
title: Some("Internal Server Error".into()),
detail: None,
source: None,
meta: None,
}],
meta: None,
jsonapi: None,
links: None,
};
let json = serde_json::to_string(&doc).unwrap();
let deserialized: Document<Resource> = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, Document::Errors { .. }));
}
#[test]
fn test_document_registry_with_included() {
let json = r#"{
"data": {"type":"articles","id":"1","attributes":{"title":"Hello"}},
"included": [{"type":"people","id":"9","attributes":{"name":"Dan"}}]
}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
let registry = doc.registry().unwrap();
let person: Resource = registry.get_by_id("people", "9").unwrap();
assert_eq!(person.attributes["name"], "Dan");
}
#[test]
fn test_document_registry_empty_included() {
let json = r#"{"data":{"type":"articles","id":"1","attributes":{}}}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
let registry = doc.registry().unwrap();
let result: std::result::Result<Resource, _> = registry.get_by_id("people", "9");
assert!(result.is_err());
}
#[test]
fn test_document_registry_errors_variant() {
let json = r#"{"errors":[{"status":"404","title":"Not Found"}]}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
let registry = doc.registry().unwrap();
let result: std::result::Result<Resource, _> = registry.get_by_id("people", "9");
assert!(result.is_err());
}
fn single_doc_json() -> &'static str {
r#"{"data":{"type":"articles","id":"1","attributes":{"title":"Hello"}}}"#
}
fn many_doc_json() -> &'static str {
r#"{"data":[{"type":"articles","id":"1","attributes":{}},{"type":"articles","id":"2","attributes":{}}]}"#
}
fn null_doc_json() -> &'static str {
r#"{"data":null}"#
}
fn errors_doc_json() -> &'static str {
r#"{"errors":[{"status":"404","title":"Not Found"}]}"#
}
fn meta_doc_json() -> &'static str {
r#"{"meta":{"total":42}}"#
}
#[test]
fn into_single_returns_resource() {
let doc: Document<Resource> = serde_json::from_str(single_doc_json()).unwrap();
let resource = doc.into_single().unwrap();
assert_eq!(resource.resource_type(), "articles");
assert_eq!(resource.resource_id(), Some("1"));
}
#[test]
fn into_single_rejects_collection() {
let doc: Document<Resource> = serde_json::from_str(many_doc_json()).unwrap();
let err = doc.into_single().unwrap_err();
match err {
crate::Error::UnexpectedDocumentShape { expected, found } => {
assert_eq!(expected, "single resource");
assert_eq!(found, "resource collection");
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn into_single_rejects_null() {
let doc: Document<Resource> = serde_json::from_str(null_doc_json()).unwrap();
let err = doc.into_single().unwrap_err();
assert!(matches!(
err,
crate::Error::UnexpectedDocumentShape {
found: "null primary data",
..
}
));
}
#[test]
fn into_single_rejects_errors() {
let doc: Document<Resource> = serde_json::from_str(errors_doc_json()).unwrap();
let err = doc.into_single().unwrap_err();
assert!(matches!(
err,
crate::Error::UnexpectedDocumentShape {
found: "errors document",
..
}
));
}
#[test]
fn into_single_rejects_meta() {
let doc: Document<Resource> = serde_json::from_str(meta_doc_json()).unwrap();
let err = doc.into_single().unwrap_err();
assert!(matches!(
err,
crate::Error::UnexpectedDocumentShape {
found: "meta-only document",
..
}
));
}
#[test]
fn into_many_returns_collection() {
let doc: Document<Resource> = serde_json::from_str(many_doc_json()).unwrap();
let resources = doc.into_many().unwrap();
assert_eq!(resources.len(), 2);
assert_eq!(resources[0].resource_id(), Some("1"));
assert_eq!(resources[1].resource_id(), Some("2"));
}
#[test]
fn into_many_rejects_single() {
let doc: Document<Resource> = serde_json::from_str(single_doc_json()).unwrap();
let err = doc.into_many().unwrap_err();
assert!(matches!(
err,
crate::Error::UnexpectedDocumentShape {
expected: "resource collection",
found: "single resource",
}
));
}
#[test]
fn into_meta_returns_meta() {
let doc: Document<Resource> = serde_json::from_str(meta_doc_json()).unwrap();
let meta = doc.into_meta().unwrap();
assert_eq!(meta["total"], 42);
}
#[test]
fn into_meta_returns_meta_from_data_doc() {
let json = r#"{"data":{"type":"articles","id":"1","attributes":{}},"meta":{"page":7}}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
let meta = doc.into_meta().unwrap();
assert_eq!(meta["page"], 7);
}
#[test]
fn into_meta_rejects_data_without_meta() {
let doc: Document<Resource> = serde_json::from_str(single_doc_json()).unwrap();
let err = doc.into_meta().unwrap_err();
assert!(matches!(
err,
crate::Error::UnexpectedDocumentShape {
found: "data document without meta",
..
}
));
}
#[test]
fn as_single_borrows_without_consuming() {
let doc: Document<Resource> = serde_json::from_str(single_doc_json()).unwrap();
let r1 = doc.as_single().unwrap();
assert_eq!(r1.resource_id(), Some("1"));
let r2 = doc.as_single().unwrap();
assert_eq!(r2.resource_id(), Some("1"));
}
#[test]
fn as_many_borrows_slice() {
let doc: Document<Resource> = serde_json::from_str(many_doc_json()).unwrap();
let slice = doc.as_many().unwrap();
assert_eq!(slice.len(), 2);
let _ = doc.as_many().unwrap();
}
#[test]
fn primary_returns_envelope() {
let doc: Document<Resource> = serde_json::from_str(many_doc_json()).unwrap();
match doc.primary().unwrap() {
PrimaryData::Many(v) => assert_eq!(v.len(), 2),
_ => panic!("expected Many"),
}
}
#[test]
fn primary_rejects_errors() {
let doc: Document<Resource> = serde_json::from_str(errors_doc_json()).unwrap();
let err = doc.primary().unwrap_err();
assert!(matches!(
err,
crate::Error::UnexpectedDocumentShape {
expected: "data document",
found: "errors document",
}
));
}
#[test]
fn included_returns_slice_for_data() {
let json = r#"{
"data": {"type":"articles","id":"1","attributes":{"title":"Hello"}},
"included": [{"type":"people","id":"9","attributes":{"name":"Dan"}}]
}"#;
let doc: Document<Resource> = serde_json::from_str(json).unwrap();
assert_eq!(doc.included().len(), 1);
assert_eq!(doc.included()[0].resource_type(), "people");
}
#[test]
fn included_returns_empty_for_errors_and_meta() {
let errors: Document<Resource> = serde_json::from_str(errors_doc_json()).unwrap();
assert!(errors.included().is_empty());
let meta: Document<Resource> = serde_json::from_str(meta_doc_json()).unwrap();
assert!(meta.included().is_empty());
}
}