jsonapi_rs/
api.rs

1//! Defines custom types and structs primarily that composite the JSON:API
2//! document
3use serde_json;
4use std::collections::HashMap;
5use crate::errors::*;
6use std::str::FromStr;
7use std;
8use serde::{Deserialize, Serialize};
9
10/// Permitted JSON-API values (all JSON Values)
11pub type JsonApiValue = serde_json::Value;
12
13/// Vector of `Resource`
14pub type Resources = Vec<Resource>;
15/// Vector of `ResourceIdentifiers`
16pub type ResourceIdentifiers = Vec<ResourceIdentifier>;
17pub type Links = HashMap<String, JsonApiValue>;
18/// Meta-data object, can contain any data
19pub type Meta = HashMap<String, JsonApiValue>;
20/// Resource Attributes, can be any JSON value
21pub type ResourceAttributes = HashMap<String, JsonApiValue>;
22/// Map of relationships with other objects
23pub type Relationships = HashMap<String, Relationship>;
24/// Side-loaded Resources
25pub type Included = Vec<Resource>;
26/// Data-related errors
27pub type JsonApiErrors = Vec<JsonApiError>;
28
29pub type JsonApiId = String;
30pub type JsonApiIds<'a> = Vec<&'a JsonApiId>;
31
32/// Resource Identifier
33#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
34pub struct ResourceIdentifier {
35    #[serde(rename = "type")]
36    pub _type: String,
37    pub id: JsonApiId,
38}
39
40/// Representation of a JSON:API resource. This is a struct that contains
41/// attributes that map to the JSON:API specification of `id`, `type`,
42/// `attributes`, `relationships`, `links`, and `meta`
43#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
44pub struct Resource {
45    #[serde(rename = "type")]
46    pub _type: String,
47    pub id: JsonApiId,
48    #[serde(default)]
49    pub attributes: ResourceAttributes,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub relationships: Option<Relationships>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub links: Option<Links>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub meta: Option<Meta>,
56}
57
58/// Relationship with another object
59#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
60pub struct Relationship {
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub data: Option<IdentifierData>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub links: Option<Links>,
65}
66
67/// Valid data Resource (can be None)
68#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
69#[serde(untagged)]
70pub enum PrimaryData {
71    None,
72    Single(Box<Resource>),
73    Multiple(Resources),
74}
75
76/// Valid Resource Identifier (can be None)
77#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
78#[serde(untagged)]
79pub enum IdentifierData {
80    None,
81    Single(ResourceIdentifier),
82    Multiple(ResourceIdentifiers),
83}
84
85/// A struct that defines an error state for a JSON:API document
86#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
87pub struct DocumentError {
88    pub errors: JsonApiErrors,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub links: Option<Links>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub meta: Option<Meta>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub jsonapi: Option<JsonApiInfo>,
95}
96
97/// A struct that defines properties for a JSON:API document that contains no errors
98#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
99pub struct DocumentData {
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub data: Option<PrimaryData>,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub included: Option<Resources>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub links: Option<Links>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub meta: Option<Meta>,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub jsonapi: Option<JsonApiInfo>,
110}
111
112/// An enum that defines the possible composition of a JSON:API document - one which contains `error` or
113/// `data` - but not both.  Rely on Rust's type system to handle this basic validation instead of
114/// running validators on parsed documents
115#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
116#[serde(untagged)]
117pub enum JsonApiDocument {
118    Error(DocumentError),
119    Data(DocumentData),
120}
121
122/// Error location
123#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
124pub struct ErrorSource {
125    pub pointer: Option<String>,
126    pub parameter: Option<String>,
127}
128
129/// Retpresentation of a JSON:API error (all fields are optional)
130#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
131pub struct JsonApiError {
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub id: Option<String>,
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub links: Option<Links>,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub status: Option<String>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub code: Option<String>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub title: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub detail: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub source: Option<ErrorSource>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub meta: Option<Meta>,
148}
149
150/// Optional `JsonApiDocument` payload identifying the JSON-API version the
151/// server implements
152#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
153pub struct JsonApiInfo {
154    pub version: Option<String>,
155    pub meta: Option<Meta>,
156}
157
158/// Pagination links
159#[derive(Serialize, Deserialize, Debug)]
160pub struct Pagination {
161    pub first: Option<String>,
162    pub prev: Option<String>,
163    pub next: Option<String>,
164    pub last: Option<String>,
165}
166
167
168#[derive(Debug)]
169pub struct Patch {
170    pub patch_type: PatchType,
171    pub subject: String,
172    pub previous: JsonApiValue,
173    pub next: JsonApiValue,
174}
175
176#[derive(Debug)]
177pub struct PatchSet {
178    pub resource_type: String,
179    pub resource_id: String,
180    pub patches: Vec<Patch>,
181}
182
183impl PatchSet {
184    pub fn new_for(resource: &Resource) -> Self {
185        PatchSet {
186            resource_type: resource._type.clone(),
187            resource_id: resource.id.clone(),
188            patches: Vec::<Patch>::new(),
189        }
190    }
191
192    pub fn push(&mut self, patch: Patch) {
193        self.patches.push(patch);
194    }
195}
196
197impl DocumentData {
198    fn has_meta(&self) -> bool {
199        self.meta.is_some()
200    }
201    fn has_included(&self) -> bool {
202        self.included.is_some()
203    }
204    fn has_data(&self) -> bool {
205        self.data.is_some()
206    }
207}
208
209/// Top-level JSON-API Document
210/// An "error" document can be valid, just as a "data" document can be valid
211impl JsonApiDocument {
212    /// This function returns `false` if the `JsonApiDocument` contains any violations of the
213    /// specification. See [`DocumentValidationError`](enum.DocumentValidationError.html)
214    ///
215    /// The spec dictates that the document must have least one of `data`, `errors` or `meta`.
216    /// Of these, `data` and `errors` must not co-exist.
217    /// The optional field `included` may only be present if the `data` field is present too.
218    pub fn is_valid(&self) -> bool {
219        self.validate().is_none()
220    }
221
222    /// This function returns a `Vec` with identified specification violations enumerated in
223    /// `DocumentValidationError`
224    ///
225    /// ```
226    /// // Simulate an error where `included` has data but `data` does not
227    /// use jsonapi_rs::api::*;
228    /// use std::str::FromStr;
229    ///
230    /// let serialized = r#"{
231    ///   "id":"1",
232    ///   "type":"post",
233    ///   "attributes":{
234    ///     "title": "Rails is Omakase",
235    ///     "likes": 250
236    ///   },
237    ///   "relationships":{},
238    ///   "links" :{}
239    /// }"#;
240    ///
241    /// let resource = Resource::from_str(&serialized);
242    ///
243    /// let data = DocumentData {
244    ///     data: None,
245    ///     included: Some(vec![resource.unwrap()]),
246    ///     ..Default::default()
247    /// };
248    ///
249    /// let doc = JsonApiDocument::Data(data);
250    ///
251    /// match doc.validate() {
252    ///   Some(errors) => {
253    ///     assert!(
254    ///       errors.contains(
255    ///         &DocumentValidationError::IncludedWithoutData
256    ///       )
257    ///     )
258    ///   }
259    ///   None => assert!(false)
260    /// }
261    /// ```
262    pub fn validate(&self) -> Option<Vec<DocumentValidationError>> {
263        let mut errors = Vec::<DocumentValidationError>::new();
264
265        match self {
266            JsonApiDocument::Error(_x) => None,
267            JsonApiDocument::Data(doc) => {
268                if doc.has_included() && !doc.has_data() {
269                    errors.push(DocumentValidationError::IncludedWithoutData);
270                }
271
272                if !(doc.has_data() || doc.has_meta()) {
273                    errors.push(DocumentValidationError::MissingContent);
274                }
275                match errors.len() {
276                    0 => None,
277                    _ => Some(errors),
278                }
279            }
280        }
281    }
282}
283
284impl FromStr for JsonApiDocument {
285    type Err = Error;
286
287    /// Instantiate from string
288    ///
289    /// ```
290    /// use jsonapi_rs::api::JsonApiDocument;
291    /// use std::str::FromStr;
292    ///
293    /// let serialized = r#"{
294    ///   "data" : [
295    ///     { "id":"1", "type":"post", "attributes":{}, "relationships":{}, "links" :{} },
296    ///     { "id":"2", "type":"post", "attributes":{}, "relationships":{}, "links" :{} },
297    ///     { "id":"3", "type":"post", "attributes":{}, "relationships":{}, "links" :{} }
298    ///   ]
299    /// }"#;
300    /// let doc = JsonApiDocument::from_str(&serialized);
301    /// assert_eq!(doc.is_ok(), true);
302    /// ```
303    fn from_str(s: &str) -> Result<Self> {
304        serde_json::from_str(s).chain_err(|| "Error parsing Document")
305    }
306}
307
308impl Resource {
309    pub fn get_relationship(&self, name: &str) -> Option<&Relationship> {
310        match self.relationships {
311            None => None,
312            Some(ref relationships) => {
313                match relationships.get(name) {
314                    None => None,
315                    Some(rel) => Some(rel),
316                }
317            }
318        }
319    }
320
321    /// Get an attribute `JsonApiValue`
322    ///
323    /// ```
324    /// use jsonapi_rs::api::Resource;
325    /// use std::str::FromStr;
326    ///
327    /// let serialized = r#"{
328    ///   "id":"1",
329    ///   "type":"post",
330    ///   "attributes":{
331    ///     "title": "Rails is Omakase",
332    ///     "likes": 250
333    ///   },
334    ///   "relationships":{},
335    ///   "links" :{}
336    /// }"#;
337    ///
338    /// match Resource::from_str(&serialized) {
339    ///   Err(_)=> assert!(false),
340    ///   Ok(resource)=> {
341    ///     match resource.get_attribute("title") {
342    ///       None => assert!(false),
343    ///       Some(attr) => {
344    ///         match attr.as_str() {
345    ///           None => assert!(false),
346    ///           Some(s) => {
347    ///               assert_eq!(s, "Rails is Omakase");
348    ///           }
349    ///         }
350    ///       }
351    ///     }
352    ///   }
353    /// }
354    pub fn get_attribute(&self, name: &str) -> Option<&JsonApiValue> {
355        match self.attributes.get(name) {
356            None => None,
357            Some(val) => Some(val),
358        }
359    }
360
361    pub fn diff(&self, other: Resource) -> std::result::Result<PatchSet, DiffPatchError> {
362        if self._type != other._type {
363            Err(DiffPatchError::IncompatibleTypes(
364                self._type.clone(),
365                other._type.clone(),
366            ))
367        } else {
368
369            let mut self_keys: Vec<String> =
370                self.attributes.iter().map(|(key, _)| key.clone()).collect();
371
372            self_keys.sort();
373
374            let mut other_keys: Vec<String> = other
375                .attributes
376                .iter()
377                .map(|(key, _)| key.clone())
378                .collect();
379
380            other_keys.sort();
381
382            let matching = self_keys
383                .iter()
384                .zip(other_keys.iter())
385                .filter(|&(a, b)| a == b)
386                .count();
387
388            if matching != self_keys.len() {
389                Err(DiffPatchError::DifferentAttributeKeys)
390            } else {
391                let mut patchset = PatchSet::new_for(self);
392
393                for (attr, self_value) in &self.attributes {
394                    match other.attributes.get(attr) {
395                        None => {
396                            error!(
397                                "Resource::diff unable to find attribute {:?} in {:?}",
398                                attr,
399                                other
400                            );
401                        }
402                        Some(other_value) => {
403                            if self_value != other_value {
404                                patchset.push(Patch {
405                                    patch_type: PatchType::Attribute,
406                                    subject: attr.clone(),
407                                    previous: self_value.clone(),
408                                    next: other_value.clone(),
409                                });
410                            }
411                        }
412                    }
413
414                }
415
416                Ok(patchset)
417            }
418        }
419    }
420
421    pub fn patch(&mut self, patchset: PatchSet) -> Result<Resource> {
422        let mut res = self.clone();
423        for patch in &patchset.patches {
424            res.attributes.insert(
425                patch.subject.clone(),
426                patch.next.clone(),
427            );
428        }
429        Ok(res)
430    }
431}
432
433impl FromStr for Resource {
434    type Err = Error;
435
436    /// Instantiate from string
437    ///
438    /// ```
439    /// use jsonapi_rs::api::Resource;
440    /// use std::str::FromStr;
441    ///
442    /// let serialized = r#"{
443    ///   "id":"1",
444    ///   "type":"post",
445    ///   "attributes":{
446    ///     "title": "Rails is Omakase",
447    ///     "likes": 250
448    ///   },
449    ///   "relationships":{},
450    ///   "links" :{}
451    /// }"#;
452    ///
453    /// let data = Resource::from_str(&serialized);
454    /// assert_eq!(data.is_ok(), true);
455    /// ```
456    fn from_str(s: &str) -> Result<Self> {
457        serde_json::from_str(s).chain_err(|| "Error parsing resource")
458    }
459}
460
461
462impl Relationship {
463    pub fn as_id(&self) -> std::result::Result<Option<&JsonApiId>, RelationshipAssumptionError> {
464        match self.data {
465            Some(IdentifierData::None) => Ok(None),
466            Some(IdentifierData::Multiple(_)) => Err(RelationshipAssumptionError::RelationshipIsAList),
467            Some(IdentifierData::Single(ref data)) => Ok(Some(&data.id)),
468            None => Ok(None),
469        }
470    }
471
472    pub fn as_ids(&self) -> std::result::Result<Option<JsonApiIds>, RelationshipAssumptionError> {
473        match self.data {
474            Some(IdentifierData::None) => Ok(None),
475            Some(IdentifierData::Single(_)) => Err(RelationshipAssumptionError::RelationshipIsNotAList),
476            Some(IdentifierData::Multiple(ref data)) => Ok(Some(data.iter().map(|x| &x.id).collect())),
477            None => Ok(None),
478        }
479    }
480}
481
482/// Enum to describe top-level JSON:API specification violations
483#[derive(Debug, Clone, PartialEq, Copy)]
484pub enum DocumentValidationError {
485    IncludedWithoutData,
486    MissingContent,
487}
488
489#[derive(Debug, Clone, PartialEq, Copy)]
490pub enum JsonApiDataError {
491    AttributeNotFound,
492    IncompatibleAttributeType,
493}
494
495#[derive(Debug, Clone, PartialEq, Copy)]
496pub enum RelationshipAssumptionError {
497    RelationshipIsAList,
498    RelationshipIsNotAList,
499}
500
501#[derive(Debug, Clone, PartialEq)]
502pub enum DiffPatchError {
503    IncompatibleTypes(String, String),
504    DifferentAttributeKeys,
505    NonExistentProperty(String),
506    IncorrectPropertyValue(String),
507}
508
509#[derive(Debug, Clone, PartialEq, Copy)]
510pub enum PatchType {
511    Relationship,
512    Attribute,
513}