use bytes::Bytes;
use http::{HeaderValue, Response, StatusCode, header};
use serde::Serialize;
use jsonapi_core::{Document, FieldsetConfig, JsonApiMediaType, ResourceObject, sparse_filter};
#[must_use]
pub fn content_type_value(media_type: &JsonApiMediaType) -> HeaderValue {
HeaderValue::try_from(media_type.to_header_value())
.expect("a negotiated JSON:API media type is always a valid header value")
}
fn response_from_body(
status: StatusCode,
content_type: HeaderValue,
body: Vec<u8>,
) -> Response<Bytes> {
Response::builder()
.status(status)
.header(header::CONTENT_TYPE, content_type)
.body(Bytes::from(body))
.expect("status and content-type are valid")
}
const SERIALIZE_PANIC: &str = "serializing a JSON:API document cannot fail for standard resources; use the `try_*` variant to handle a failing custom `Serialize`";
#[must_use]
pub fn document_response<P, I>(
document: &Document<P, I>,
media_type: &JsonApiMediaType,
) -> Response<Bytes>
where
P: ResourceObject,
I: Serialize,
{
try_document_response(document, media_type).expect(SERIALIZE_PANIC)
}
pub fn try_document_response<P, I>(
document: &Document<P, I>,
media_type: &JsonApiMediaType,
) -> Result<Response<Bytes>, serde_json::Error>
where
P: ResourceObject,
I: Serialize,
{
try_json_api_response(StatusCode::OK, content_type_value(media_type), document)
}
#[must_use]
pub fn json_api_response<P, I>(
status: StatusCode,
content_type: HeaderValue,
document: &Document<P, I>,
) -> Response<Bytes>
where
P: ResourceObject,
I: Serialize,
{
try_json_api_response(status, content_type, document).expect(SERIALIZE_PANIC)
}
pub fn try_json_api_response<P, I>(
status: StatusCode,
content_type: HeaderValue,
document: &Document<P, I>,
) -> Result<Response<Bytes>, serde_json::Error>
where
P: ResourceObject,
I: Serialize,
{
let body = serde_json::to_vec(document)?;
Ok(response_from_body(status, content_type, body))
}
#[must_use]
pub fn json_api_response_filtered<P, I>(
status: StatusCode,
content_type: HeaderValue,
document: &Document<P, I>,
fields: &FieldsetConfig,
) -> Response<Bytes>
where
P: ResourceObject,
I: Serialize,
{
try_json_api_response_filtered(status, content_type, document, fields).expect(SERIALIZE_PANIC)
}
pub fn try_json_api_response_filtered<P, I>(
status: StatusCode,
content_type: HeaderValue,
document: &Document<P, I>,
fields: &FieldsetConfig,
) -> Result<Response<Bytes>, serde_json::Error>
where
P: ResourceObject,
I: Serialize,
{
let value = serde_json::to_value(document)?;
let filtered = sparse_filter(&value, fields);
let body = serde_json::to_vec(&filtered)?;
Ok(response_from_body(status, content_type, body))
}
#[cfg(test)]
mod tests {
use super::*;
use jsonapi_core::Resource;
use serde_json::Value;
#[test]
fn content_type_value_plain() {
let ct = content_type_value(&JsonApiMediaType::plain());
assert_eq!(ct, "application/vnd.api+json");
}
#[test]
fn content_type_value_with_ext() {
let mt = JsonApiMediaType::with_ext(["https://jsonapi.org/ext/atomic"]);
let ct = content_type_value(&mt);
assert_eq!(
ct.to_str().unwrap(),
"application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\""
);
}
#[test]
fn content_type_value_with_profile() {
let mt =
JsonApiMediaType::parse("application/vnd.api+json; profile=\"https://example.com/p\"")
.unwrap();
let ct = content_type_value(&mt);
assert_eq!(
ct.to_str().unwrap(),
"application/vnd.api+json; profile=\"https://example.com/p\""
);
}
#[test]
fn document_response_sets_status_content_type_and_body() {
let document: Document<Resource> =
serde_json::from_str(r#"{"data":{"type":"articles","id":"1"}}"#).unwrap();
let response = document_response(&document, &JsonApiMediaType::plain());
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok()),
Some("application/vnd.api+json")
);
let body: Value = serde_json::from_slice(response.body()).unwrap();
assert_eq!(body["data"]["type"], "articles");
assert_eq!(body["data"]["id"], "1");
}
#[test]
fn json_api_response_filtered_trims_attributes_but_keeps_type_and_id() {
let document: Document<Resource> = serde_json::from_str(
r#"{"data":{"type":"articles","id":"1",
"attributes":{"title":"Hi","body":"World"}}}"#,
)
.unwrap();
let fields = FieldsetConfig::new().fields("articles", &["title"]);
let response = json_api_response_filtered(
StatusCode::OK,
content_type_value(&JsonApiMediaType::plain()),
&document,
&fields,
);
let body: Value = serde_json::from_slice(response.body()).unwrap();
assert_eq!(body["data"]["type"], "articles");
assert_eq!(body["data"]["id"], "1");
assert_eq!(body["data"]["attributes"]["title"], "Hi");
assert!(body["data"]["attributes"].get("body").is_none());
}
#[test]
fn json_api_response_honors_custom_status() {
let document: Document<Resource> =
serde_json::from_str(r#"{"data":{"type":"articles","id":"1"}}"#).unwrap();
let response = json_api_response(
StatusCode::CREATED,
content_type_value(&JsonApiMediaType::plain()),
&document,
);
assert_eq!(response.status(), StatusCode::CREATED);
}
#[test]
fn try_json_api_response_returns_ok_with_serialized_body() {
let document: Document<Resource> =
serde_json::from_str(r#"{"data":{"type":"articles","id":"1"}}"#).unwrap();
let response = try_json_api_response(
StatusCode::OK,
content_type_value(&JsonApiMediaType::plain()),
&document,
)
.expect("a serializable document must produce Ok");
assert_eq!(response.status(), StatusCode::OK);
let body: Value = serde_json::from_slice(response.body()).unwrap();
assert_eq!(body["data"]["id"], "1");
}
}