|
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}; |
2 | 6 |
|
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 | + ); |
7 | 243 | }
|
0 commit comments