diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c8060f9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test (${{ matrix.rust }}) + runs-on: ubuntu-latest + strategy: + matrix: + rust: [stable, beta, nightly] + fail-fast: false + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - name: Run tests + run: cargo test --verbose + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Run clippy + run: cargo clippy -- -D warnings + + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Check formatting + run: cargo fmt --check diff --git a/.gitignore b/.gitignore index b781aa5..33e08d4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ src/*.bk tests/*.bk *.swp *.swo +/.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fabae9d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,43 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +jsonapi-rust is a Rust implementation of the JSON:API v1 specification (https://jsonapi.org/). It provides serialization, deserialization, and validation of JSON:API requests and responses. + +## Build & Test Commands + +```bash +cargo test # Run all tests +RUST_BACKTRACE=1 cargo test -- --nocapture # Verbose test output with backtraces +cargo test # Run a single test +cargo clippy # Lint (run before fmt) +cargo fmt # Format code +``` + +## Architecture + +### Module Structure + +- **src/api.rs** - Core JSON:API types: `Resource`, `JsonApiDocument`, `Relationship`, validation logic, and diff/patch operations +- **src/model.rs** - `JsonApiModel` trait for converting Rust structs to/from JSON:API format, plus the `jsonapi_model!` macro +- **src/query.rs** - Query parameter parsing for include, fields, filter, sort, and pagination +- **src/array.rs** - `JsonApiArray` trait for optional "has many" relationships +- **src/errors.rs** - Error handling via thiserror + +### Key Abstractions + +1. **JsonApiDocument** - Enum that enforces JSON:API spec (document contains either data OR errors, never both) +2. **JsonApiModel trait** - Implement on structs with `id: String` field to enable JSON:API conversion +3. **jsonapi_model! macro** - Code generator: `jsonapi_model!(MyStruct; "resource-type")` +4. **Resource** - Core type with attributes (`HashMap`), relationships, links, and meta + +### Testing + +Tests in `tests/` use JSON fixtures loaded via `helper::read_json_file()`. The test modules mirror the source structure: `api_test.rs`, `model_test.rs`, `query_test.rs`. + +## Conventions + +- Use conventional commits: `feat:`, `bug:`, `test:`, `doc:`, `refactor:` (changelog is auto-generated via clog-tool) +- Breaking changes are allowed until v1.0.0 diff --git a/Cargo.toml b/Cargo.toml index 57628d1..901539a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "jsonapi" -version = "0.7.0" +version = "0.8.0" +edition = "2021" authors = ["Michiel Kalkman "] description = "JSONAPI implementation" documentation = "https://docs.rs/jsonapi" @@ -12,15 +13,14 @@ categories = [] license = "MIT" [dependencies] -serde = "^1.0.21" -serde_json = "^1.0.6" -serde_derive = "^1.0.21" +serde = { version = "1", features = ["derive"] } +serde_json = "1" queryst = "3" log = "0.4" -error-chain = "^0.12.0" +thiserror = "2" [dev-dependencies] -env_logger = "0.9" +env_logger = "0.11" [badges] travis-ci = { repository = "michiel/jsonapi-rust", branch = "master" } diff --git a/src/api.rs b/src/api.rs index 707e709..94bb0f4 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,10 +1,10 @@ //! Defines custom types and structs primarily that composite the JSON:API //! document -use serde_json; -use std::collections::HashMap; use crate::errors::*; +use log::error; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::str::FromStr; -use std; /// Permitted JSON-API values (all JSON Values) pub type JsonApiValue = serde_json::Value; @@ -163,7 +163,6 @@ pub struct Pagination { pub last: Option, } - #[derive(Debug)] pub struct Patch { pub patch_type: PatchType, @@ -300,7 +299,7 @@ impl FromStr for JsonApiDocument { /// assert_eq!(doc.is_ok(), true); /// ``` fn from_str(s: &str) -> Result { - serde_json::from_str(s).chain_err(|| "Error parsing Document") + Ok(serde_json::from_str(s)?) } } @@ -308,12 +307,10 @@ impl Resource { pub fn get_relationship(&self, name: &str) -> Option<&Relationship> { match self.relationships { None => None, - Some(ref relationships) => { - match relationships.get(name) { - None => None, - Some(rel) => Some(rel), - } - } + Some(ref relationships) => match relationships.get(name) { + None => None, + Some(rel) => Some(rel), + }, } } @@ -364,17 +361,11 @@ impl Resource { other._type.clone(), )) } else { - - let mut self_keys: Vec = - self.attributes.iter().map(|(key, _)| key.clone()).collect(); + let mut self_keys: Vec = self.attributes.keys().cloned().collect(); self_keys.sort(); - let mut other_keys: Vec = other - .attributes - .iter() - .map(|(key, _)| key.clone()) - .collect(); + let mut other_keys: Vec = other.attributes.keys().cloned().collect(); other_keys.sort(); @@ -394,8 +385,7 @@ impl Resource { None => { error!( "Resource::diff unable to find attribute {:?} in {:?}", - attr, - other + attr, other ); } Some(other_value) => { @@ -409,7 +399,6 @@ impl Resource { } } } - } Ok(patchset) @@ -420,10 +409,8 @@ impl Resource { pub fn patch(&mut self, patchset: PatchSet) -> Result { let mut res = self.clone(); for patch in &patchset.patches { - res.attributes.insert( - patch.subject.clone(), - patch.next.clone(), - ); + res.attributes + .insert(patch.subject.clone(), patch.next.clone()); } Ok(res) } @@ -453,26 +440,33 @@ impl FromStr for Resource { /// assert_eq!(data.is_ok(), true); /// ``` fn from_str(s: &str) -> Result { - serde_json::from_str(s).chain_err(|| "Error parsing resource") + Ok(serde_json::from_str(s)?) } } - impl Relationship { pub fn as_id(&self) -> std::result::Result, RelationshipAssumptionError> { match self.data { Some(IdentifierData::None) => Ok(None), - Some(IdentifierData::Multiple(_)) => Err(RelationshipAssumptionError::RelationshipIsAList), + Some(IdentifierData::Multiple(_)) => { + Err(RelationshipAssumptionError::RelationshipIsAList) + } Some(IdentifierData::Single(ref data)) => Ok(Some(&data.id)), None => Ok(None), } } - pub fn as_ids(&self) -> std::result::Result, RelationshipAssumptionError> { + pub fn as_ids( + &self, + ) -> std::result::Result>, RelationshipAssumptionError> { match self.data { Some(IdentifierData::None) => Ok(None), - Some(IdentifierData::Single(_)) => Err(RelationshipAssumptionError::RelationshipIsNotAList), - Some(IdentifierData::Multiple(ref data)) => Ok(Some(data.iter().map(|x| &x.id).collect())), + Some(IdentifierData::Single(_)) => { + Err(RelationshipAssumptionError::RelationshipIsNotAList) + } + Some(IdentifierData::Multiple(ref data)) => { + Ok(Some(data.iter().map(|x| &x.id).collect())) + } None => Ok(None), } } diff --git a/src/array.rs b/src/array.rs index dd506e2..6fd1ceb 100644 --- a/src/array.rs +++ b/src/array.rs @@ -8,15 +8,17 @@ pub trait JsonApiArray { } impl JsonApiArray for Vec { - fn get_models(&self) -> &[M] { self } - fn get_models_mut(&mut self) -> &mut [M] { self } + fn get_models(&self) -> &[M] { + self + } + fn get_models_mut(&mut self) -> &mut [M] { + self + } } impl JsonApiArray for Option> { fn get_models(&self) -> &[M] { - self.as_ref() - .map(|v| v.as_slice()) - .unwrap_or(&[][..]) + self.as_ref().map(|v| v.as_slice()).unwrap_or(&[][..]) } fn get_models_mut(&mut self) -> &mut [M] { diff --git a/src/errors.rs b/src/errors.rs index 24a0a1e..aff694a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,11 +1,17 @@ -error_chain!{ - foreign_links { - SerdeJson(serde_json::Error); - } - errors { - ResourceToModelError(t: String) { - description("Error converting Resource to Model") - display("Error converting Resource to Model: '{}'", t) - } - } +use thiserror::Error; + +/// Error type for jsonapi operations +#[derive(Error, Debug)] +pub enum Error { + #[error("JSON parsing error: {0}")] + SerdeJson(#[from] serde_json::Error), + + #[error("Error converting Resource to Model: '{0}'")] + ResourceToModel(String), + + #[error("{0}")] + Other(String), } + +/// Result type alias for jsonapi operations +pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 875d03b..e08a5cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,13 @@ -#![deny(missing_debug_implementations, - missing_copy_implementations, - trivial_casts, - trivial_numeric_casts, - unsafe_code, - unstable_features, - unused_import_braces, - unused_qualifications - )] - +#![deny( + missing_debug_implementations, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unsafe_code, + unstable_features, + unused_import_braces, + unused_qualifications +)] #![doc(html_root_url = "https://docs.rs/jsonapi/")] //! This is documentation for the `jsonapi` crate. @@ -31,16 +31,16 @@ //! `type` member as required by the [JSON:API] specification //! //! ```rust -//! #[macro_use] extern crate serde_derive; //! #[macro_use] extern crate jsonapi; //! use jsonapi::api::*; //! use jsonapi::model::*; +//! use serde::{Deserialize, Serialize}; //! //! #[derive(Debug, PartialEq, Serialize, Deserialize)] //! struct Flea { //! id: String, //! name: String, -//! }; +//! } //! //! jsonapi_model!(Flea; "flea"); //! @@ -63,6 +63,8 @@ //! variable type in `Result` //! //! ```rust +//! use jsonapi::api::JsonApiDocument; +//! //! let serialized = r#" //! { //! "data": [{ @@ -88,7 +90,7 @@ //! } //! ] //! }"#; -//! let data: Result = serde_json::from_str(&serialized); +//! let data: Result = serde_json::from_str(&serialized); //! assert_eq!(data.is_ok(), true); //! ``` //! @@ -96,6 +98,10 @@ //! [Resource::from_str](api/struct.Resource.html) trait implementation //! //! ```rust +//! use jsonapi::api::Resource; +//! use std::str::FromStr; +//! +//! let serialized = r#"{"type":"post", "id":"1", "attributes":{}}"#; //! let data = Resource::from_str(&serialized); //! assert_eq!(data.is_ok(), true); //! ``` @@ -126,21 +132,8 @@ //! ``` //! -extern crate serde; -extern crate serde_json; -#[macro_use] -extern crate serde_derive; - -extern crate queryst; - -#[macro_use] -extern crate log; - -#[macro_use] -extern crate error_chain; - pub mod api; pub mod array; -pub mod query; -pub mod model; pub mod errors; +pub mod model; +pub mod query; diff --git a/src/model.rs b/src/model.rs index 7870922..57052f8 100644 --- a/src/model.rs +++ b/src/model.rs @@ -3,11 +3,11 @@ //! structs which implement `Deserialize` to be converted to/from a //! [`JsonApiDocument`](../api/struct.JsonApiDocument.html) or //! [`Resource`](../api/struct.Resource.html) -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}; +use serde_json::{from_value, to_value, Map, Value}; +pub use std::collections::HashMap; /// A trait for any struct that can be converted from/into a /// [`Resource`](api/struct.Resource.tml). The only requirement is that your @@ -29,38 +29,37 @@ where #[doc(hidden)] fn build_included(&self) -> Option; - fn from_jsonapi_resource(resource: &Resource, included: &Option) - -> Result - { - + fn from_jsonapi_resource(resource: &Resource, included: &Option) -> Result { let visited_relationships: Vec<&str> = Vec::new(); - Self::from_serializable(Self::resource_to_attrs(resource, included, &visited_relationships)) + Self::from_serializable(Self::resource_to_attrs( + resource, + included, + &visited_relationships, + )) } /// Create a single resource object or collection of resource - /// objects directly from + /// objects directly from /// [`DocumentData`](../api/struct.DocumentData.html). This method /// will parse the document (the `data` and `included` resources) in an /// attempt to instantiate the calling struct. fn from_jsonapi_document(doc: &DocumentData) -> Result { match doc.data.as_ref() { - Some(primary_data) => { - match *primary_data { - PrimaryData::None => bail!("Document had no data"), - PrimaryData::Single(ref resource) => { - Self::from_jsonapi_resource(resource, &doc.included) - } - PrimaryData::Multiple(ref resources) => { - let visited_relationships: Vec<&str> = Vec::new(); - let all: Vec = resources - .iter() - .map(|r| Self::resource_to_attrs(r, &doc.included, &visited_relationships)) - .collect(); - Self::from_serializable(all) - } + Some(primary_data) => match *primary_data { + PrimaryData::None => Err(Error::Other("Document had no data".into())), + PrimaryData::Single(ref resource) => { + Self::from_jsonapi_resource(resource, &doc.included) } - } - None => bail!("Document had no data"), + PrimaryData::Multiple(ref resources) => { + let visited_relationships: Vec<&str> = Vec::new(); + let all: Vec = resources + .iter() + .map(|r| Self::resource_to_attrs(r, &doc.included, &visited_relationships)) + .collect(); + Self::from_serializable(all) + } + }, + None => Err(Error::Other("Document had no data".into())), } } @@ -79,30 +78,26 @@ where (resource, self.build_included()) } else { - panic!(format!("{} is not a Value::Object", self.jsonapi_type())) + panic!("{} is not a Value::Object", self.jsonapi_type()) } } - /// Converts the struct into a complete /// [`JsonApiDocument`](../api/struct.JsonApiDocument.html) fn to_jsonapi_document(&self) -> JsonApiDocument { let (resource, included) = self.to_jsonapi_resource(); - JsonApiDocument::Data ( - DocumentData { - data: Some(PrimaryData::Single(Box::new(resource))), - included, - ..Default::default() - } - ) + JsonApiDocument::Data(DocumentData { + data: Some(PrimaryData::Single(Box::new(resource))), + included, + ..Default::default() + }) } - #[doc(hidden)] fn build_has_one(model: &M) -> Relationship { Relationship { data: Some(IdentifierData::Single(model.as_resource_identifier())), - links: None + links: None, } } @@ -110,9 +105,9 @@ where fn build_has_many(models: &[M]) -> Relationship { Relationship { data: Some(IdentifierData::Multiple( - models.iter().map(|m| m.as_resource_identifier()).collect() + models.iter().map(|m| m.as_resource_identifier()).collect(), )), - links: None + links: None, } } @@ -159,15 +154,10 @@ where /// attempt to find and return the `Resource` whose `type` and `id` /// attributes match #[doc(hidden)] - fn lookup<'a>(needle: &ResourceIdentifier, haystack: &'a [Resource]) - -> Option<&'a Resource> - { - for resource in haystack { - if resource._type == needle._type && resource.id == needle.id { - return Some(resource); - } - } - None + fn lookup<'a>(needle: &ResourceIdentifier, haystack: &'a [Resource]) -> Option<&'a Resource> { + haystack + .iter() + .find(|resource| resource._type == needle._type && resource.id == needle.id) } /// Return a [`ResourceAttributes`](../api/struct.ResourceAttributes.html) @@ -189,9 +179,11 @@ where /// Furthermore the current implementation of this crate does not establish an object graph /// that could be used to traverse these relationships effectively. #[doc(hidden)] - fn resource_to_attrs(resource: &Resource, included: &Option, visited_relationships: &Vec<&str>) - -> ResourceAttributes - { + fn resource_to_attrs( + resource: &Resource, + included: &Option, + visited_relationships: &Vec<&str>, + ) -> ResourceAttributes { let mut new_attrs = HashMap::new(); new_attrs.clone_from(&resource.attributes); new_attrs.insert("id".into(), resource.id.clone().into()); @@ -219,20 +211,20 @@ where Some(IdentifierData::None) => Value::Null, Some(IdentifierData::Single(ref identifier)) => { let found = Self::lookup(identifier, inc) - .map(|r| Self::resource_to_attrs(r, included, &this_visited) ); - to_value(found) - .expect("Casting Single relation to value") - }, + .map(|r| Self::resource_to_attrs(r, included, &this_visited)); + to_value(found).expect("Casting Single relation to value") + } Some(IdentifierData::Multiple(ref identifiers)) => { - let found: Vec> = - identifiers.iter().map(|identifier|{ - Self::lookup(identifier, inc).map(|r|{ + let found: Vec> = identifiers + .iter() + .map(|identifier| { + Self::lookup(identifier, inc).map(|r| { Self::resource_to_attrs(r, included, &this_visited) }) - }).collect(); - to_value(found) - .expect("Casting Multiple relation to value") - }, + }) + .collect(); + to_value(found).expect("Casting Multiple relation to value") + } None => Value::Null, }; new_attrs.insert(name.to_string(), value); @@ -244,7 +236,7 @@ where #[doc(hidden)] fn from_serializable(s: S) -> Result { - from_value(to_value(s)?).map_err(Error::from) + Ok(from_value(to_value(s)?)?) } } @@ -277,10 +269,10 @@ pub fn vec_to_jsonapi_resources( /// [`JsonApiDocument`](../api/struct.JsonApiDocument.html) /// /// ```rust -/// #[macro_use] extern crate serde_derive; /// #[macro_use] extern crate jsonapi; /// use jsonapi::api::*; /// use jsonapi::model::*; +/// use serde::{Deserialize, Serialize}; /// /// #[derive(Debug, PartialEq, Serialize, Deserialize)] /// struct Flea { @@ -305,13 +297,11 @@ pub fn vec_to_jsonapi_resources( /// ``` pub fn vec_to_jsonapi_document(objects: Vec) -> JsonApiDocument { let (resources, included) = vec_to_jsonapi_resources(objects); - JsonApiDocument::Data ( - DocumentData { - data: Some(PrimaryData::Multiple(resources)), - included, - ..Default::default() - } - ) + JsonApiDocument::Data(DocumentData { + data: Some(PrimaryData::Multiple(resources)), + included, + ..Default::default() + }) } impl JsonApiModel for Box { diff --git a/src/query.rs b/src/query.rs index 8948abb..097b372 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,6 +1,7 @@ +use log::warn; use queryst::parse; -use std::collections::HashMap; use serde_json::value::Value; +use std::collections::HashMap; #[derive(Debug, PartialEq, Clone, Copy)] pub struct PageParams { @@ -16,30 +17,27 @@ pub struct Query { pub fields: Option>>, pub page: Option, pub sort: Option>, - pub filter: Option>> + pub filter: Option>>, } // // Helper functions to break down the cyclomatic complexity of parameter parsing // -fn ok_params_include(o:&Value) -> Option> { +fn ok_params_include(o: &Value) -> Option> { match o.pointer("/include") { None => None, - Some(inc) => { - match inc.as_str() { - None => None, - Some(include_str) => { - let arr: Vec = - include_str.split(',').map(|s| s.to_string()).collect(); - Some(arr) - } + Some(inc) => match inc.as_str() { + None => None, + Some(include_str) => { + let arr: Vec = include_str.split(',').map(|s| s.to_string()).collect(); + Some(arr) } - } + }, } } -fn ok_params_fields(o:&Value) -> HashMap> { +fn ok_params_fields(o: &Value) -> HashMap> { let mut fields = HashMap::>::new(); if let Some(x) = o.pointer("/fields") { @@ -47,13 +45,10 @@ fn ok_params_fields(o:&Value) -> HashMap> { if let Some(obj) = x.as_object() { for (key, value) in obj.iter() { let arr: Vec = match value.as_str() { - Some(string) => { - string.split(',').map(|s| s.to_string()).collect() - } + Some(string) => string.split(',').map(|s| s.to_string()).collect(), None => Vec::::new(), }; fields.insert(key.to_string(), arr); - } } } else { @@ -64,23 +59,20 @@ fn ok_params_fields(o:&Value) -> HashMap> { fields } -fn ok_params_sort(o:&Value) -> Option> { +fn ok_params_sort(o: &Value) -> Option> { match o.pointer("/sort") { None => None, - Some(sort) => { - match sort.as_str() { - None => None, - Some(sort_str) => { - let arr: Vec = - sort_str.split(',').map(|s| s.to_string()).collect(); - Some(arr) - } + Some(sort) => match sort.as_str() { + None => None, + Some(sort_str) => { + let arr: Vec = sort_str.split(',').map(|s| s.to_string()).collect(); + Some(arr) } - } + }, } } -fn ok_params_filter(o:&Value) -> Option>> { +fn ok_params_filter(o: &Value) -> Option>> { match o.pointer("/filter") { None => None, Some(x) => { @@ -89,9 +81,7 @@ fn ok_params_filter(o:&Value) -> Option>> { if let Some(obj) = x.as_object() { for (key, value) in obj.iter() { let arr: Vec = match value.as_str() { - Some(string) => { - string.split(',').map(|s| s.to_string()).collect() - } + Some(string) => string.split(',').map(|s| s.to_string()).collect(), None => Vec::::new(), }; tmp_filter.insert(key.to_string(), arr); @@ -106,14 +96,14 @@ fn ok_params_filter(o:&Value) -> Option>> { } } -fn ok_params_page(o:&Value) -> PageParams { +fn ok_params_page(o: &Value) -> PageParams { PageParams { number: match o.pointer("/page/number") { None => { warn!( "Query::from_params : No page/number found in {:?}, setting \ default 0", - o + o ); 0 } @@ -125,7 +115,7 @@ fn ok_params_page(o:&Value) -> PageParams { warn!( "Query::from_params : page/number found in {:?}, \ not able not able to parse it - setting default 0", - o + o ); 0 } @@ -134,7 +124,7 @@ fn ok_params_page(o:&Value) -> PageParams { warn!( "Query::from_params : page/number found in {:?}, but it is \ not an expected type - setting default 0", - o + o ); 0 } @@ -145,7 +135,7 @@ fn ok_params_page(o:&Value) -> PageParams { warn!( "Query::from_params : No page/size found in {:?}, setting \ default 0", - o + o ); 0 } @@ -157,7 +147,7 @@ fn ok_params_page(o:&Value) -> PageParams { warn!( "Query::from_params : page/size found in {:?}, \ not able not able to parse it - setting default 0", - o + o ); 0 } @@ -166,7 +156,7 @@ fn ok_params_page(o:&Value) -> PageParams { warn!( "Query::from_params : page/size found in {:?}, but it is \ not an expected type - setting default 0", - o + o ); 0 } @@ -175,10 +165,10 @@ fn ok_params_page(o:&Value) -> PageParams { } } -fn ok_params(o:Value) -> Query { +fn ok_params(o: Value) -> Query { Query { _type: "none".into(), - include : ok_params_include(&o), + include: ok_params_include(&o), fields: Some(ok_params_fields(&o)), page: Some(ok_params_page(&o)), sort: ok_params_sort(&o), @@ -205,11 +195,8 @@ impl Query { /// /// ``` pub fn from_params(params: &str) -> Self { - match parse(params) { - Ok(o) => { - ok_params(o) - } + Ok(o) => ok_params(o), Err(err) => { warn!("Query::from_params : Can't parse : {:?}", err); Query { diff --git a/tests/api_test.rs b/tests/api_test.rs index cbec64d..1c26881 100644 --- a/tests/api_test.rs +++ b/tests/api_test.rs @@ -1,8 +1,8 @@ //! The purpose of these tests is to validate compliance with the JSONAPI //! specification and to ensure that this crate reads documents properly +extern crate env_logger; extern crate jsonapi; extern crate serde_json; -extern crate env_logger; use jsonapi::api::*; @@ -28,15 +28,12 @@ fn it_works() { assert_eq!(deserialized.id, resource.id); - let jsonapidocument = JsonApiDocument::Data ( - DocumentData { - data: Some(PrimaryData::None), - ..Default::default() - } - ); + let jsonapidocument = JsonApiDocument::Data(DocumentData { + data: Some(PrimaryData::None), + ..Default::default() + }); assert_eq!(jsonapidocument.is_valid(), true); - } #[test] @@ -51,12 +48,10 @@ fn jsonapi_document_can_be_valid() { meta: Some(Meta::new()), }; - let jsonapi_document_with_data = JsonApiDocument::Data ( - DocumentData { - data: Some(PrimaryData::Single(Box::new(resource))), - ..Default::default() - } - ); + let jsonapi_document_with_data = JsonApiDocument::Data(DocumentData { + data: Some(PrimaryData::Single(Box::new(resource))), + ..Default::default() + }); assert_eq!(jsonapi_document_with_data.is_valid(), true); } @@ -74,12 +69,10 @@ fn jsonapi_document_invalid_errors() { meta: Some(Meta::new()), }; - let no_content_document = JsonApiDocument::Data ( - DocumentData { - data: None, - ..Default::default() - } - ); + let no_content_document = JsonApiDocument::Data(DocumentData { + data: None, + ..Default::default() + }); match no_content_document.validate() { None => assert!(false), @@ -88,31 +81,25 @@ fn jsonapi_document_invalid_errors() { } } - let null_data_content_document = JsonApiDocument::Data ( - DocumentData { - data: Some(PrimaryData::None), - ..Default::default() - } - ); + let null_data_content_document = JsonApiDocument::Data(DocumentData { + data: Some(PrimaryData::None), + ..Default::default() + }); match null_data_content_document.validate() { None => assert!(true), Some(_) => assert!(false), } - let included_without_data_document = JsonApiDocument::Data ( - DocumentData { - included: Some(vec![included_resource]), - ..Default::default() - } - ); + let included_without_data_document = JsonApiDocument::Data(DocumentData { + included: Some(vec![included_resource]), + ..Default::default() + }); match included_without_data_document.validate() { None => assert!(false), Some(errors) => { - assert!(errors.contains( - &DocumentValidationError::IncludedWithoutData, - )); + assert!(errors.contains(&DocumentValidationError::IncludedWithoutData,)); } } } @@ -127,12 +114,10 @@ fn error_from_json_string() { let error: Result = serde_json::from_str(serialized); assert_eq!(error.is_ok(), true); match error { - Ok(jsonapierror) => { - match jsonapierror.id { - Some(id) => assert_eq!(id, "1"), - None => assert!(false), - } - } + Ok(jsonapierror) => match jsonapierror.id { + Some(id) => assert_eq!(id, "1"), + None => assert!(false), + }, Err(_) => assert!(false), } } @@ -202,30 +187,26 @@ fn api_document_from_json_file() { let data: Result = serde_json::from_str(&s); match data { - Ok(res) => { - match res { - JsonApiDocument::Error(_x) => assert!(false), - JsonApiDocument::Data(x) => { - match x.data { - Some(PrimaryData::Multiple(arr)) => { - assert_eq!(arr.len(), 1); - } - Some(PrimaryData::Single(_)) => { - println!( - "api_document_from_json_file : Expected one Resource in a vector, \ + Ok(res) => match res { + JsonApiDocument::Error(_x) => assert!(false), + JsonApiDocument::Data(x) => match x.data { + Some(PrimaryData::Multiple(arr)) => { + assert_eq!(arr.len(), 1); + } + Some(PrimaryData::Single(_)) => { + println!( + "api_document_from_json_file : Expected one Resource in a vector, \ not a direct Resource" - ); - assert!(false); - } - Some(PrimaryData::None) => { - println!("api_document_from_json_file : Expected one Resource in a vector"); - assert!(false); - } - None => assert!(false), - } + ); + assert!(false); } - } - } + Some(PrimaryData::None) => { + println!("api_document_from_json_file : Expected one Resource in a vector"); + assert!(false); + } + None => assert!(false), + }, + }, Err(err) => { println!("api_document_from_json_file : Error: {:?}", err); assert!(false); @@ -241,59 +222,57 @@ fn api_document_collection_from_json_file() { let data: Result = serde_json::from_str(&s); match data { - Ok(x) => { - match x { - JsonApiDocument::Error(_) => assert!(false), - JsonApiDocument::Data(res) => { - match res.data { - Some(PrimaryData::Multiple(arr)) => { - assert_eq!(arr.len(), 1); - } - Some(PrimaryData::Single(_)) => { - println!( - "api_document_collection_from_json_file : Expected one Resource in \ + Ok(x) => match x { + JsonApiDocument::Error(_) => assert!(false), + JsonApiDocument::Data(res) => { + match res.data { + Some(PrimaryData::Multiple(arr)) => { + assert_eq!(arr.len(), 1); + } + Some(PrimaryData::Single(_)) => { + println!( + "api_document_collection_from_json_file : Expected one Resource in \ a vector, not a direct Resource" - ); - assert!(false); - } - Some(PrimaryData::None) => { - println!( - "api_document_collection_from_json_file : Expected one Resource in \ + ); + assert!(false); + } + Some(PrimaryData::None) => { + println!( + "api_document_collection_from_json_file : Expected one Resource in \ a vector" - ); - assert!(false); - } - None => assert!(false), + ); + assert!(false); } + None => assert!(false), + } - match res.included { - Some(arr) => { - assert_eq!(arr.len(), 3); - assert_eq!(arr[0].id, "9"); - assert_eq!(arr[1].id, "5"); - assert_eq!(arr[2].id, "12"); - } - None => { - println!( - "api_document_collection_from_json_file : Expected three Resources \ + match res.included { + Some(arr) => { + assert_eq!(arr.len(), 3); + assert_eq!(arr[0].id, "9"); + assert_eq!(arr[1].id, "5"); + assert_eq!(arr[2].id, "12"); + } + None => { + println!( + "api_document_collection_from_json_file : Expected three Resources \ in 'included' in a vector" - ); - assert!(false); - } + ); + assert!(false); } + } - match res.links { - Some(links) => { - assert_eq!(links.len(), 3); - } - None => { - println!("api_document_collection_from_json_file : expected links"); - assert!(false); - } + match res.links { + Some(links) => { + assert_eq!(links.len(), 3); + } + None => { + println!("api_document_collection_from_json_file : expected links"); + assert!(false); } } } - } + }, Err(err) => { println!("api_document_collection_from_json_file : Error: {:?}", err); assert!(false); @@ -332,7 +311,7 @@ fn can_deserialize_jsonapi_example_resource_003() { #[test] fn can_deserialize_jsonapi_example_resource_004() { let _ = env_logger::try_init(); - let s = ::read_json_file("data/resource_004.json"); + let s = crate::read_json_file("data/resource_004.json"); let data: Result = serde_json::from_str(&s); assert!(data.is_ok()); } @@ -383,54 +362,45 @@ fn can_get_attribute() { Ok(res) => { match res.get_attribute("likes") { None => assert!(false), - Some(val) => { - match val.as_i64() { - None => assert!(false), - Some(num) => { - let x: i64 = 250; - assert_eq!(num, x); - } + Some(val) => match val.as_i64() { + None => assert!(false), + Some(num) => { + let x: i64 = 250; + assert_eq!(num, x); } - } + }, } match res.get_attribute("title") { None => assert!(false), - Some(val) => { - match val.as_str() { - None => assert!(false), - Some(s) => { - assert_eq!(s, "Rails is Omakase"); - } + Some(val) => match val.as_str() { + None => assert!(false), + Some(s) => { + assert_eq!(s, "Rails is Omakase"); } - } + }, } match res.get_attribute("published") { None => assert!(false), - Some(val) => { - match val.as_bool() { - None => assert!(false), - Some(b) => { - assert_eq!(b, true); - } + Some(val) => match val.as_bool() { + None => assert!(false), + Some(b) => { + assert_eq!(b, true); } - } + }, } match res.get_attribute("tags") { None => assert!(false), - Some(val) => { - match val.as_array() { - None => assert!(false), - Some(arr) => { - assert_eq!(arr[0], "rails"); - assert_eq!(arr[1], "news"); - } + Some(val) => match val.as_array() { + None => assert!(false), + Some(arr) => { + assert_eq!(arr[0], "rails"); + assert_eq!(arr[1], "news"); } - } + }, } - } } } @@ -450,17 +420,15 @@ fn can_diff_resource() { // So far so good match data2 { Err(_) => assert!(false), - Ok(res2) => { - match res1.diff(res2) { - Err(_) => { - assert!(false); - } - Ok(patchset) => { - println!("can_diff_resource: PatchSet is {:?}", patchset); - assert_eq!(patchset.patches.len(), 5); - } + Ok(res2) => match res1.diff(res2) { + Err(_) => { + assert!(false); } - } + Ok(patchset) => { + println!("can_diff_resource: PatchSet is {:?}", patchset); + assert_eq!(patchset.patches.len(), 5); + } + }, } } } @@ -475,12 +443,10 @@ fn it_omits_empty_document_and_primary_data_keys() { attributes: ResourceAttributes::new(), ..Default::default() }; - let doc = JsonApiDocument::Data ( - DocumentData { - data: Some(PrimaryData::Single(Box::new(resource))), - ..Default::default() - } - ); + let doc = JsonApiDocument::Data(DocumentData { + data: Some(PrimaryData::Single(Box::new(resource))), + ..Default::default() + }); assert_eq!( serde_json::to_string(&doc).unwrap(), @@ -490,12 +456,10 @@ fn it_omits_empty_document_and_primary_data_keys() { #[test] fn it_does_not_omit_an_empty_primary_data() { - let doc = JsonApiDocument::Data ( - DocumentData { - data: Some(PrimaryData::None), - ..Default::default() - } - ); + let doc = JsonApiDocument::Data(DocumentData { + data: Some(PrimaryData::None), + ..Default::default() + }); assert_eq!(serde_json::to_string(&doc).unwrap(), r#"{"data":null}"#); } @@ -506,12 +470,10 @@ fn it_omits_empty_error_keys() { id: Some("error_id".to_string()), ..Default::default() }; - let doc = JsonApiDocument::Error ( - DocumentError { - errors: vec![error], - ..Default::default() - } - ); + let doc = JsonApiDocument::Error(DocumentError { + errors: vec![error], + ..Default::default() + }); assert_eq!( serde_json::to_string(&doc).unwrap(), r#"{"errors":[{"id":"error_id"}]}"# diff --git a/tests/helper.rs b/tests/helper.rs index d17cfe2..7e989eb 100644 --- a/tests/helper.rs +++ b/tests/helper.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::fs::File; use std::io::prelude::*; use std::path::Path; @@ -7,15 +6,15 @@ pub fn read_json_file(filename: &str) -> String { let path = Path::new(filename); let display = path.display(); - let mut file = match File::open(&path) { - Err(why) => panic!("couldn't open {}: {}", display, Error::description(&why)), + let mut file = match File::open(path) { + Err(why) => panic!("couldn't open {}: {}", display, why), 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); }; s diff --git a/tests/model_test.rs b/tests/model_test.rs index 1f6518a..6d112fc 100644 --- a/tests/model_test.rs +++ b/tests/model_test.rs @@ -1,10 +1,9 @@ #[macro_use] extern crate jsonapi; -#[macro_use] -extern crate serde_derive; -extern crate serde_json; + use jsonapi::array::JsonApiArray; use jsonapi::model::*; +use serde::{Deserialize, Serialize}; mod helper; use helper::read_json_file; @@ -22,7 +21,7 @@ struct Book { id: String, title: String, first_chapter: Chapter, - chapters: Vec + chapters: Vec, } jsonapi_model!(Book; "books"; has one first_chapter; has many chapters); @@ -39,11 +38,27 @@ fn to_jsonapi_document_and_back() { let book = Book { id: "1".into(), title: "The Fellowship of the Ring".into(), - first_chapter: Chapter { id: "1".into(), title: "A Long-expected Party".into(), ordering: 1 }, + 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 } + 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, + }, ], }; @@ -51,8 +66,8 @@ fn to_jsonapi_document_and_back() { 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"); + let book_again = + Book::from_jsonapi_document(&book_doc).expect("Book should be generated from the book_doc"); assert_eq!(book, book_again); } @@ -109,7 +124,7 @@ fn test_vec_to_jsonapi_document() { #[test] fn from_jsonapi_document() { - let json = ::read_json_file("data/author_tolkien.json"); + let json = read_json_file("data/author_tolkien.json"); // TODO - is this the right thing that we want to test? Shold this cast into a JsonApiDocument // and detect if this was a data or an error? diff --git a/tests/query_test.rs b/tests/query_test.rs index b981f7c..8b47b60 100644 --- a/tests/query_test.rs +++ b/tests/query_test.rs @@ -1,5 +1,5 @@ -extern crate jsonapi; extern crate env_logger; +extern crate jsonapi; use jsonapi::query::*; @@ -254,7 +254,7 @@ fn can_parse_and_use_defaults_for_invalid_values() { match query.sort { None => assert!(true), - Some(_) => assert!(false) + Some(_) => assert!(false), } match query.filter { @@ -370,7 +370,7 @@ fn can_generate_string_sort_multiple() { include: None, fields: None, page: None, - sort: Some(vec!["-name".into(),"created".into()]), + sort: Some(vec!["-name".into(), "created".into()]), filter: None, }; @@ -447,12 +447,8 @@ fn can_generate_string_fields_multiple_key_and_values() { // assert!( - query_string.eq( - "fields[item]=title,description&fields[user]=name,dateofbirth", - ) || - query_string.eq( - "fields[user]=name,dateofbirth&fields[item]=title,description", - ) + query_string.eq("fields[item]=title,description&fields[user]=name,dateofbirth",) + || query_string.eq("fields[user]=name,dateofbirth&fields[item]=title,description",) ); } @@ -524,12 +520,8 @@ fn can_generate_string_filter_multiple_key_and_values() { // assert!( - query_string.eq( - "filter[posts]=1,2&filter[authors]=3,4", - ) || - query_string.eq( - "filter[authors]=3,4&filter[posts]=1,2", - ) + query_string.eq("filter[posts]=1,2&filter[authors]=3,4",) + || query_string.eq("filter[authors]=3,4&filter[posts]=1,2",) ); }