Skip to content

Add optional relation support #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
//! ### Basic Usage with Macro
//!
//! Using the [`jsonapi_model!`][jsonapi_model] macro a struct can be converted
//! into a [`JsonApiDocument`][JsonApiDocument] or [`Resource`][Resource]. It is
//! into a [`JsonApiDocument`][api::JsonApiDocument] or [`Resource`][Resource]. It is
//! required that the struct have an `id` property whose type is `String`. The
//! second argument in the [`jsonapi_model!`][jsonapi_model] marco defines the
//! `type` member as required by the [JSON:API] specification
Expand Down Expand Up @@ -63,6 +63,10 @@
//! variable type in `Result`
//!
//! ```rust
//! # #[macro_use] extern crate serde_json;
//! # #[macro_use] extern crate jsonapi;
//! # use jsonapi::api::JsonApiDocument;
//! # use serde_json;
//! let serialized = r#"
//! {
//! "data": [{
Expand All @@ -88,19 +92,49 @@
//! }
//! ]
//! }"#;
//! let data: Result<Resource, serde_json::Error> = serde_json::from_str(&serialized);
//! let data: Result<JsonApiDocument, serde_json::Error> = serde_json::from_str(&serialized);
//! assert_eq!(data.is_ok(), true);
//! ```
//!
//! Or parse the `String` directly using the
//! [Resource::from_str](api/struct.Resource.html) trait implementation
//!
//! ```rust
//! let data = Resource::from_str(&serialized);
//! # #[macro_use] extern crate serde_json;
//! # #[macro_use] extern crate jsonapi;
//! # use jsonapi::api::JsonApiDocument;
//! # use serde_json;
//! # use std::str::FromStr;
//! let serialized = r#"
//! {
//! "data": [{
//! "type": "articles",
//! "id": "1",
//! "attributes": {
//! "title": "JSON:API paints my bikeshed!",
//! "body": "The shortest article. Ever."
//! },
//! "relationships": {
//! "author": {
//! "data": {"id": "42", "type": "people"}
//! }
//! }
//! }],
//! "included": [
//! {
//! "type": "people",
//! "id": "42",
//! "attributes": {
//! "name": "John"
//! }
//! }
//! ]
//! }"#;
//! let data = JsonApiDocument::from_str(&serialized);
//! assert_eq!(data.is_ok(), true);
//! ```
//!
//! [`JsonApiDocument`][JsonApiDocument] implements `PartialEq` which allows two
//! [`JsonApiDocument`][api::JsonApiDocument] implements `PartialEq` which allows two
//! documents to be compared for equality. If two documents possess the **same
//! contents** the ordering of the attributes and fields within the JSON:API
//! document are irrelevant and their equality will be `true`.
Expand Down
32 changes: 23 additions & 9 deletions src/model.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
//! Defines the `JsonApiModel` trait. This is primarily used in conjunction with
//! the [`jsonapi_model!`](../macro.jsonapi_model.html) macro to allow arbitrary
//! structs which implement `Deserialize` to be converted to/from a
//! [`JsonApiDocument`](../api/struct.JsonApiDocument.html) or
//! [`Resource`](../api/struct.Resource.html)
//! [`JsonApiDocument`](crate::api::JsonApiDocument) or
//! [`Resource`](crate::api::Resource)
pub use std::collections::HashMap;
pub use crate::api::*;
use crate::errors::*;
use serde::{Deserialize, Serialize};
use serde_json::{from_value, to_value, Value, Map};

/// A trait for any struct that can be converted from/into a
/// [`Resource`](api/struct.Resource.tml). The only requirement is that your
/// [`Resource`](crate::api::Resource). The only requirement is that your
/// struct has an `id: String` field.
/// You shouldn't be implementing JsonApiModel manually, look at the
/// `jsonapi_model!` macro instead.
Expand Down Expand Up @@ -249,7 +249,7 @@ where
}

/// Converts a `vec!` of structs into
/// [`Resources`](../api/type.Resources.html)
/// [`Resources`](crate::api::Resources)
///
pub fn vec_to_jsonapi_resources<T: JsonApiModel>(
objects: Vec<T>,
Expand All @@ -274,7 +274,7 @@ pub fn vec_to_jsonapi_resources<T: JsonApiModel>(
}

/// Converts a `vec!` of structs into a
/// [`JsonApiDocument`](../api/struct.JsonApiDocument.html)
/// [`JsonApiDocument`](crate::api::JsonApiDocument)
///
/// ```rust
/// #[macro_use] extern crate serde_derive;
Expand Down Expand Up @@ -337,7 +337,7 @@ impl<M: JsonApiModel> JsonApiModel for Box<M> {
}

/// When applied this macro implements the
/// [`JsonApiModel`](model/trait.JsonApiModel.html) trait for the provided type
/// [`JsonApiModel`](crate::api::JsonApiModel) trait for the provided type
///
#[macro_export]
macro_rules! jsonapi_model {
Expand All @@ -353,16 +353,17 @@ macro_rules! jsonapi_model {
($model:ty; $type:expr;
has one $( $has_one:ident ),*
) => (
jsonapi_model!($model; $type; has one $( $has_one ),*; has many);
jsonapi_model!($model; $type; has one $( $has_one ),*; has many; has optional);
);
($model:ty; $type:expr;
has many $( $has_many:ident ),*
) => (
jsonapi_model!($model; $type; has one; has many $( $has_many ),*);
jsonapi_model!($model; $type; has one; has many $( $has_many ),*; has optional);
);
($model:ty; $type:expr;
has one $( $has_one:ident ),*;
has many $( $has_many:ident ),*
has many $( $has_many:ident ),*;
has optional $( $has_opt:ident ),*
) => (
impl JsonApiModel for $model {
fn jsonapi_type(&self) -> String { $type.to_string() }
Expand All @@ -372,6 +373,7 @@ macro_rules! jsonapi_model {
static FIELDS: &'static [&'static str] = &[
$( stringify!($has_one),)*
$( stringify!($has_many),)*
$( stringify!($has_opt),)*
];

Some(FIELDS)
Expand All @@ -393,12 +395,24 @@ macro_rules! jsonapi_model {
}
);
)*
$(
if let Some(model) = &self.$has_opt {
relationships.insert(stringify!($has_opt).into(),
Self::build_has_one(model)
);
}
)*
Some(relationships)
}

fn build_included(&self) -> Option<Resources> {
let mut included:Resources = vec![];
$( included.append(&mut self.$has_one.to_resources()); )*
$(
if let Some(resource) = &self.$has_opt {
included.append(&mut resource.to_resources());
}
)*
$(
for model in self.$has_many.get_models() {
included.append(&mut model.to_resources());
Expand Down
5 changes: 2 additions & 3 deletions tests/helper.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
Expand All @@ -8,14 +7,14 @@ pub fn read_json_file(filename: &str) -> String {
let display = path.display();

let mut file = match File::open(&path) {
Err(why) => panic!("couldn't open {}: {}", display, Error::description(&why)),
Err(why) => panic!("couldn't open {}: {}", display, &why.to_string()),
Ok(file) => file,
};

let mut s = String::new();

if let Err(why) = file.read_to_string(&mut s) {
panic!("couldn't read {}: {}", display, Error::description(&why));
panic!("couldn't read {}: {}", display, &why.to_string());
};

s
Expand Down
28 changes: 27 additions & 1 deletion tests/model_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ jsonapi_model!(Author; "authors"; has many books);
struct Book {
id: String,
title: String,
forward: Option<Chapter>,
first_chapter: Chapter,
chapters: Vec<Chapter>
}
jsonapi_model!(Book; "books"; has one first_chapter; has many chapters);
jsonapi_model!(Book; "books"; has one first_chapter; has many chapters; has optional forward);

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Chapter {
Expand All @@ -39,6 +40,7 @@ fn to_jsonapi_document_and_back() {
let book = Book {
id: "1".into(),
title: "The Fellowship of the Ring".into(),
forward: None,
first_chapter: Chapter { id: "1".into(), title: "A Long-expected Party".into(), ordering: 1 },
chapters: vec![
Chapter { id: "1".into(), title: "A Long-expected Party".into(), ordering: 1 },
Expand All @@ -57,6 +59,30 @@ fn to_jsonapi_document_and_back() {
assert_eq!(book, book_again);
}

#[test]
fn to_jsonapi_document_and_back_optional() {
let book = Book {
id: "1".into(),
title: "The Fellowship of the Ring".into(),
forward: Some(Chapter { id: "0".into(), title: "abc".into(), ordering: 0}),
first_chapter: Chapter { id: "1".into(), title: "A Long-expected Party".into(), ordering: 1 },
chapters: vec![
Chapter { id: "1".into(), title: "A Long-expected Party".into(), ordering: 1 },
Chapter { id: "2".into(), title: "The Shadow of the Past".into(), ordering: 2 },
Chapter { id: "3".into(), title: "Three is Company".into(), ordering: 3 }
],
};

let doc = book.to_jsonapi_document();
let json = serde_json::to_string(&doc).unwrap();
let book_doc: DocumentData = serde_json::from_str(&json)
.expect("Book DocumentData should be created from the book json");
let book_again = Book::from_jsonapi_document(&book_doc)
.expect("Book should be generated from the book_doc");

assert_eq!(book, book_again);
}

#[test]
fn numeric_id() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
Expand Down