Skip to content

Commit d38093e

Browse files
committed
model serialization and deserialization working
1 parent 0505bf0 commit d38093e

File tree

6 files changed

+306
-13
lines changed

6 files changed

+306
-13
lines changed

Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ categories = []
1212
license = "MIT"
1313

1414
[dependencies]
15-
serde = "0.9"
16-
serde_json = "0.9"
17-
serde_derive = "0.9"
15+
serde = "1.0.12"
16+
serde_json = "1.0.3"
17+
serde_derive = "1.0.12"
1818
queryst = "1"
1919
log = "0.3"
20+
error-chain = "0.11.0"
2021

2122
[dev-dependencies]
2223
env_logger = "0.3"

src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
error_chain!{
2+
errors {
3+
ResourceToModelError(t: String) {
4+
description("Error converting Resource to Model")
5+
display("Error converting Resource to Model: '{}'", t)
6+
}
7+
}
8+
}

src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@
1616
1717
extern crate serde;
1818
extern crate serde_json;
19-
#[macro_use]
20-
extern crate serde_derive;
19+
#[macro_use] extern crate serde_derive;
2120

2221
extern crate queryst;
2322

24-
#[macro_use]
25-
extern crate log;
23+
#[macro_use] extern crate log;
24+
25+
#[macro_use] extern crate error_chain;
2626

2727
pub mod api;
2828
pub mod query;
2929
pub mod model;
30+
pub mod errors;

src/model.rs

Lines changed: 241 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,243 @@
1-
use api::Resource;
1+
pub use std::collections::HashMap;
2+
pub use api::*;
3+
use errors::*;
4+
use serde::{Deserialize, Serialize};
5+
use serde_json::{from_value, to_value, Value, Map};
26

3-
/// A trait for structs that can be represented as or built from a `Resource`
4-
pub trait JsonApiModel {
5-
fn to_jsonapi_resource(&self) -> Resource;
6-
fn from_jsonapi_resource(resource: Resource) -> Self;
7+
/* A trait for any struct that can be converted from/into a Resource.
8+
* The only requirement is that your struct has a field that implements
9+
* `ToString` and can be used as an ID. (not necesarilly called ID though).
10+
* You shouldn't be implementing JsonApiModel manually, look at the
11+
* `jsonapi_model` macro instead.
12+
*/
13+
pub trait JsonApiModel: Serialize
14+
where for<'de> Self: Deserialize<'de>
15+
{
16+
fn jsonapi_type(&self) -> String;
17+
fn jsonapi_id(&self) -> String;
18+
fn relationship_fields() -> Option<&'static [&'static str]>;
19+
fn build_relationships(&self) -> Option<Relationships>;
20+
fn build_included(&self) -> Option<Resources>;
21+
22+
fn from_jsonapi_resource(resource: &Resource, included: &Option<Resources>)
23+
-> Result<Self>
24+
{
25+
Self::from_serializable(Self::resource_to_attrs(&resource, &included))
26+
}
27+
28+
fn from_jsonapi_document(doc: &JsonApiDocument) -> Result<Self> {
29+
match doc.data.as_ref() {
30+
Some(primary_data) => {
31+
match *primary_data {
32+
PrimaryData::None => bail!("Document had no data"),
33+
PrimaryData::Single(ref resource) =>
34+
Self::from_jsonapi_resource(&resource, &doc.included),
35+
PrimaryData::Multiple(ref resources) => {
36+
let all: Vec<ResourceAttributes> = resources
37+
.iter()
38+
.map(|r| Self::resource_to_attrs(&r, &doc.included))
39+
.collect();
40+
Self::from_serializable(all)
41+
}
42+
}
43+
},
44+
None => bail!("Document had no data")
45+
}
46+
}
47+
48+
fn to_jsonapi_resource(&self) -> (Resource, Option<Resources>) {
49+
if let Value::Object(mut attrs) = to_value(self).unwrap(){
50+
let id_value = attrs
51+
.remove("id")
52+
.unwrap_or_else(|| Value::String("".into()));
53+
54+
let resource = Resource{
55+
_type: self.jsonapi_type(),
56+
id: from_value(id_value).unwrap_or_else(|_| "".into()),
57+
relationships: self.build_relationships(),
58+
attributes: Self::extract_attributes(&attrs),
59+
..Default::default()
60+
};
61+
62+
(resource, self.build_included())
63+
}else{
64+
panic!(format!("{} is not a Value::Object", self.jsonapi_type()))
65+
}
66+
}
67+
68+
69+
fn to_jsonapi_document(&self) -> JsonApiDocument {
70+
let (resource, included) = self.to_jsonapi_resource();
71+
JsonApiDocument {
72+
data: Some(PrimaryData::Single(Box::new(resource))),
73+
included: included,
74+
..Default::default()
75+
}
76+
}
77+
78+
79+
fn build_has_one<M: JsonApiModel>(model: &M) -> Relationship {
80+
Relationship{
81+
data: IdentifierData::Single(model.as_resource_identifier()),
82+
links: None
83+
}
84+
}
85+
86+
fn build_has_many<M: JsonApiModel>(models: &Vec<M>) -> Relationship {
87+
Relationship{
88+
data: IdentifierData::Multiple(
89+
models.iter().map(|m| m.as_resource_identifier()).collect()
90+
),
91+
links: None
92+
}
93+
}
94+
95+
fn as_resource_identifier(&self) -> ResourceIdentifier {
96+
ResourceIdentifier {
97+
_type: self.jsonapi_type(),
98+
id: self.jsonapi_id(),
99+
}
100+
}
101+
102+
/* Attribute corresponding to the model is removed from the Map
103+
* before calling this, so there's no need to ignore it like we do
104+
* with the attributes that correspond with relationships.
105+
* */
106+
fn extract_attributes(attrs: &Map<String, Value>) -> ResourceAttributes {
107+
attrs.iter().filter(|&(key, _)|{
108+
if let Some(ref fields) = Self::relationship_fields(){
109+
if fields.contains(&key.as_str()) {
110+
return false;
111+
}
112+
}
113+
return true;
114+
}).map(|(k,v)|{ (k.clone(), v.clone()) }).collect()
115+
}
116+
117+
fn to_resources(&self) -> Resources {
118+
let (me, maybe_others) = self.to_jsonapi_resource();
119+
let mut flattened = vec![me];
120+
if let Some(mut others) = maybe_others {
121+
flattened.append(&mut others);
122+
}
123+
flattened
124+
}
125+
126+
fn lookup<'a>(needle: &ResourceIdentifier, haystack: &'a Resources)
127+
-> Option<&'a Resource>
128+
{
129+
for resource in haystack {
130+
if resource._type == needle._type && resource.id == needle.id {
131+
return Some(&resource)
132+
}
133+
}
134+
None
135+
}
136+
137+
fn resource_to_attrs(resource: &Resource, included: &Option<Resources>)
138+
-> ResourceAttributes
139+
{
140+
let mut new_attrs = HashMap::new();
141+
new_attrs.clone_from(&resource.attributes);
142+
new_attrs.insert("id".into(), resource.id.clone().into());
143+
144+
if let Some(relations) = resource.relationships.as_ref() {
145+
if let Some(inc) = included.as_ref() {
146+
for (name, relation) in relations {
147+
let value = match relation.data {
148+
IdentifierData::None => Value::Null,
149+
IdentifierData::Single(ref identifier) => {
150+
let found = Self::lookup(identifier, inc)
151+
.map(|r| Self::resource_to_attrs(r, &included) );
152+
to_value(found)
153+
.expect("Casting Single relation to value")
154+
},
155+
IdentifierData::Multiple(ref identifiers) => {
156+
let found: Vec<Option<ResourceAttributes>> =
157+
identifiers.iter().map(|id|{
158+
Self::lookup(id, inc).map(|r|{
159+
Self::resource_to_attrs(r, &included)
160+
})
161+
}).collect();
162+
to_value(found)
163+
.expect("Casting Multiple relation to value")
164+
},
165+
};
166+
new_attrs.insert(name.to_string(), value);
167+
}
168+
}
169+
}
170+
171+
new_attrs
172+
}
173+
174+
fn from_serializable<S: Serialize>(s: S) -> Result<Self> {
175+
from_value(to_value(s).unwrap())
176+
.chain_err(|| "Error casting via serde_json")
177+
}
178+
}
179+
180+
#[macro_export]
181+
macro_rules! jsonapi_model {
182+
($model:ty; $type:expr) => (
183+
impl JsonApiModel for $model {
184+
fn jsonapi_type(&self) -> String { $type.to_string() }
185+
fn jsonapi_id(&self) -> String { self.id.to_string() }
186+
fn relationship_fields() -> Option<&'static [&'static str]> { None }
187+
fn build_relationships(&self) -> Option<Relationships> { None }
188+
fn build_included(&self) -> Option<Resources> { None }
189+
}
190+
);
191+
($model:ty; $type:expr;
192+
has one $( $has_one:ident ),*
193+
) => (
194+
jsonapi_model!($model; $type; has one $( $has_one ),*; has many);
195+
);
196+
($model:ty; $type:expr;
197+
has many $( $has_many:ident ),*
198+
) => (
199+
jsonapi_model!($model; $type; has one; has many $( $has_many ),*);
200+
);
201+
($model:ty; $type:expr;
202+
has one $( $has_one:ident ),*;
203+
has many $( $has_many:ident ),*
204+
) => (
205+
impl JsonApiModel for $model {
206+
fn jsonapi_type(&self) -> String { $type.to_string() }
207+
fn jsonapi_id(&self) -> String { self.id.to_string() }
208+
209+
fn relationship_fields() -> Option<&'static [&'static str]> {
210+
Some(&[
211+
$( stringify!($has_one),)*
212+
$( stringify!($has_many),)*
213+
])
214+
}
215+
216+
fn build_relationships(&self) -> Option<Relationships> {
217+
let mut relationships = HashMap::new();
218+
$(
219+
relationships.insert(stringify!($has_one).into(),
220+
Self::build_has_one(&self.$has_one)
221+
);
222+
)*
223+
$(
224+
relationships.insert(stringify!($has_many).into(),
225+
Self::build_has_many(&self.$has_many)
226+
);
227+
)*
228+
Some(relationships)
229+
}
230+
231+
fn build_included(&self) -> Option<Resources> {
232+
let mut included:Resources = vec![];
233+
$( included.append(&mut self.$has_one.to_resources()); )*
234+
$(
235+
for model in &self.$has_many {
236+
included.append(&mut model.to_resources());
237+
}
238+
)*
239+
Some(included)
240+
}
241+
}
242+
);
7243
}

tests/model_test.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#[macro_use] extern crate jsonapi;
2+
#[macro_use] extern crate serde_derive;
3+
extern crate serde_json;
4+
use jsonapi::model::*;
5+
6+
#[derive(Debug, PartialEq, Serialize, Deserialize)]
7+
struct Dog {
8+
id: String,
9+
name: String,
10+
age: i32,
11+
main_flea: Flea,
12+
fleas: Vec<Flea>,
13+
}
14+
jsonapi_model!(Dog; "dog"; has one main_flea; has many fleas);
15+
16+
#[derive(Debug, PartialEq, Serialize, Deserialize)]
17+
struct Flea {
18+
id: String,
19+
name: String,
20+
}
21+
jsonapi_model!(Flea; "flea");
22+
23+
#[test]
24+
fn to_jsonapi_document_and_back(){
25+
let dog = Dog{
26+
id: "1".into(),
27+
name: "fido".into(),
28+
age: 2,
29+
main_flea: Flea{id: "1".into(), name: "general flea".into() },
30+
fleas: vec![
31+
Flea{id: "2".into(), name: "rick".into()},
32+
Flea{id: "3".into(), name: "morty".into()}
33+
],
34+
};
35+
let doc = dog.to_jsonapi_document();
36+
let json = serde_json::to_string(&doc).unwrap();
37+
println!("JSON IS:");
38+
let dog_doc: JsonApiDocument = serde_json::from_str(&json)
39+
.expect("Dog JsonApiDocument should be created from the dog json");;
40+
let dog_again = Dog::from_jsonapi_document(&dog_doc)
41+
.expect("Dog should be generated from the dog_doc");
42+
43+
assert_eq!(dog, dog_again);
44+
}
45+
46+
#[test]
47+
fn from_jsonapi_document_and_back(){
48+
}

tests/query_test.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
extern crate jsonapi;
2-
extern crate serde_json;
32
extern crate env_logger;
43

54
use jsonapi::query::*;

0 commit comments

Comments
 (0)