diff --git a/.gitignore b/.gitignore index e1d17e7..616ee0b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build docs .DS_Store dist +target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5db1b1a..61b4d40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,83 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "itoa" version = "0.4.7" @@ -8,8 +86,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "json_schema" -version = "1.6.9" +version = "1.6.10" dependencies = [ + "derive_builder", "serde", "serde_json", ] @@ -69,6 +148,12 @@ dependencies = [ "serde", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.72" diff --git a/Cargo.toml b/Cargo.toml index 3f6d721..5d2424c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,5 @@ license = "Apache-2.0" [dependencies] serde_json = "1.0" - - [dependencies.serde] - version = "1.0" - features = [ "derive" ] +serde = { version = "1.0", features = [ "derive" ] } +derive_builder = "0.10.2" \ No newline at end of file diff --git a/README.md b/README.md index ec78f19..237e0ab 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,11 @@ This repo contains the json schema meta schema and code to package it on npm, ge `go get github.com/json-schema-tools/meta-schema` + +### Rust + +`cargo install json_schema` + ## Using ### Typescript @@ -19,7 +24,28 @@ This repo contains the json schema meta schema and code to package it on npm, ge import JSONSchema, { JSONSchemaObject, Properties, Items } from "@json-schema-tools/meta-schema" ``` -### +### Rust + +#### From a string +```rust +let foo = r#"{ + "title": "helloworld", + "type": "string" +}"#; + +let as_json_schema: JSONSchemaObject = serde_json::from_str(foo).unwrap(); +``` + +#### Using builder pattern +```rust +let schema = JSONSchemaObjectBuilder::default() + .title("foobar".to_string()) + ._type(Type::SimpleTypes(SimpleTypes::String)) + .build() + .unwrap(); + +let as_str = serde_json::to_string(&schema).unwrap(); +``` ### Contributing diff --git a/src/lib.rs b/src/lib.rs index 977aed8..1acde98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,11 @@ extern crate serde; extern crate serde_json; +extern crate derive_builder; +use derive_builder::Builder; use serde::{Serialize, Deserialize}; use std::collections::HashMap; + pub type Id = String; pub type Schema = String; pub type Ref = String; @@ -21,7 +24,8 @@ pub type NonNegativeInteger = i64; pub type NonNegativeIntegerDefaultZero = i64; pub type Pattern = String; pub type SchemaArray = Vec; -#[derive(Serialize, Deserialize)] + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] pub enum Items { JSONSchema, SchemaArray @@ -48,7 +52,15 @@ pub type Definitions = HashMap>; /// /// {} /// -pub type Properties = HashMap>; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Builder, Default)] +#[builder(setter(strip_option), default)] +#[serde(default)] +pub struct Properties { + #[serde(flatten)] + pub additional_properties: Option +} +pub type PropertiesAdditional = HashMap>; /// PatternProperties /// /// # Default @@ -56,36 +68,64 @@ pub type Properties = HashMap>; /// {} /// pub type PatternProperties = HashMap>; -#[derive(Serialize, Deserialize)] + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] pub enum DependenciesSet { JSONSchema, StringArray } pub type Dependencies = HashMap>; pub type Enum = Vec; -pub type SimpleTypes = serde_json::Value; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum SimpleTypes { + #[serde(rename = "string")] + String, + #[serde(rename = "array")] + Array, + #[serde(rename = "object")] + Object, + #[serde(rename = "number")] + Number, + #[serde(rename = "boolean")] + Boolean, + #[serde(rename = "integer")] + Integer +} + pub type ArrayOfSimpleTypes = Vec; -#[derive(Serialize, Deserialize)] + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(untagged)] pub enum Type { - SimpleTypes, - ArrayOfSimpleTypes + SimpleTypes(SimpleTypes), + ArrayOfSimpleTypes(ArrayOfSimpleTypes) } + pub type Format = String; pub type ContentMediaType = String; pub type ContentEncoding = String; -#[derive(Serialize, Deserialize)] + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Builder, Default)] +#[builder(setter(strip_option), default)] +#[serde(default)] pub struct JSONSchemaObject { - #[serde(rename="$id")] - pub(crate) id: Option, - #[serde(rename="$schema")] - pub(crate) schema: Option, - #[serde(rename="$ref")] - pub(crate) _ref: Option, - #[serde(rename="$comment")] - pub(crate) comment: Option, - pub(crate) title: Option, + #[serde(rename="$id", skip_serializing_if = "Option::is_none")] + pub id: Option<Id>, + #[serde(rename="$schema", skip_serializing_if = "Option::is_none")] + pub schema: Option<Schema>, + #[serde(rename="$ref", skip_serializing_if = "Option::is_none")] + pub _ref: Option<Ref>, + #[serde(rename="$comment", skip_serializing_if = "Option::is_none")] + pub comment: Option<Comment>, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option<Title>, + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) description: Option<Description>, - pub(crate) default: Option<AlwaysTrue>, + + #[serde(rename="default", skip_serializing_if = "Option::is_none")] + pub(crate) _default: Option<AlwaysTrue>, + #[serde(rename="readOnly")] pub(crate) read_only: Option<ReadOnly>, pub(crate) examples: Option<Examples>, @@ -103,7 +143,7 @@ pub struct JSONSchemaObject { pub(crate) min_length: Option<NonNegativeIntegerDefaultZero>, pub(crate) pattern: Option<Pattern>, #[serde(rename="additionalItems")] - pub(crate) additional_items: Option<JSONSchema>, + pub(crate) additional_items: Option<Box<JSONSchema>>, pub(crate) items: Option<Items>, #[serde(rename="maxItems")] pub(crate) max_items: Option<NonNegativeInteger>, @@ -111,52 +151,54 @@ pub struct JSONSchemaObject { pub(crate) min_items: Option<NonNegativeIntegerDefaultZero>, #[serde(rename="uniqueItems")] pub(crate) unique_items: Option<UniqueItems>, - pub(crate) contains: Option<JSONSchema>, + pub contains: Option<Box<JSONSchema>>, #[serde(rename="maxProperties")] pub(crate) max_properties: Option<NonNegativeInteger>, #[serde(rename="minProperties")] pub(crate) min_properties: Option<NonNegativeIntegerDefaultZero>, pub(crate) required: Option<StringArray>, #[serde(rename="additionalProperties")] - pub(crate) additional_properties: Option<JSONSchema>, + pub(crate) additional_properties: Option<Box<JSONSchema>>, pub(crate) definitions: Option<Definitions>, - pub(crate) properties: Option<Properties>, + pub properties: Option<Properties>, #[serde(rename="patternProperties")] pub(crate) pattern_properties: Option<PatternProperties>, pub(crate) dependencies: Option<Dependencies>, #[serde(rename="propertyNames")] - pub(crate) property_names: Option<JSONSchema>, + pub(crate) property_names: Option<Box<JSONSchema>>, #[serde(rename="const")] pub(crate) _const: Option<AlwaysTrue>, #[serde(rename="enum")] pub(crate) _enum: Option<Enum>, #[serde(rename="type")] - pub(crate) _type: Option<Type>, + pub _type: Option<Type>, pub(crate) format: Option<Format>, #[serde(rename="contentMediaType")] pub(crate) content_media_type: Option<ContentMediaType>, #[serde(rename="contentEncoding")] pub(crate) content_encoding: Option<ContentEncoding>, #[serde(rename="if")] - pub(crate) _if: Option<JSONSchema>, - pub(crate) then: Option<JSONSchema>, + pub(crate) _if: Option<Box<JSONSchema>>, + pub(crate) then: Option<Box<JSONSchema>>, #[serde(rename="else")] - pub(crate) _else: Option<JSONSchema>, + pub(crate) _else: Option<Box<JSONSchema>>, #[serde(rename="allOf")] pub(crate) all_of: Option<SchemaArray>, #[serde(rename="anyOf")] - pub(crate) any_of: Option<SchemaArray>, + pub(crate) any_of: Option<Box<JSONSchema>>, #[serde(rename="oneOf")] pub(crate) one_of: Option<SchemaArray>, - pub(crate) not: Option<JSONSchema>, + pub(crate) not: Option<Box<JSONSchema>>, } /// JSONSchemaBoolean /// /// Always valid if true. Never valid if false. Is constant. /// pub type JSONSchemaBoolean = bool; -#[derive(Serialize, Deserialize)] + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(untagged)] pub enum JSONSchema { - JSONSchemaObject, - JSONSchemaBoolean -} \ No newline at end of file + JSONSchemaObject(JSONSchemaObject), + JSONSchemaBoolean(JSONSchemaBoolean) +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..2907f09 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,113 @@ +extern crate json_schema; + +use json_schema::*; + +#[test] +fn can_build_and_serialize() { + let schema = JSONSchemaObjectBuilder::default() + .title("foobar".to_string()) + ._type(Type::SimpleTypes(SimpleTypes::String)) + .build() + .unwrap(); + + let as_str = serde_json::to_string(&schema).unwrap(); + let expected = "{\"title\": \"foobar\", \"type\": \"string\"}".to_string(); + assert_eq!(as_str, expected); +} + +#[test] +fn can_build_and_serialize_array_types() { + + let schema = JSONSchemaObjectBuilder::default() + ._type(Type::ArrayOfSimpleTypes(vec!(SimpleTypes::String, SimpleTypes::Array))) + .build() + .unwrap(); + + let as_str = serde_json::to_string(&schema).unwrap(); + println!("{}", as_str); + + let expected = "{\"type\": [\"string\", \"array\"]}".to_string(); + assert_eq!(as_str, expected); +} + +#[test] +fn can_deserialize() { + let foo = r#"{ + "title": "helloworld", + "type": "string" + }"#; + + let as_json_schema: JSONSchemaObject = serde_json::from_str(foo).unwrap(); + assert_eq!(as_json_schema.title.unwrap(), "helloworld"); + assert_eq!(as_json_schema._type.unwrap(), Type::SimpleTypes(SimpleTypes::String)); +} + +#[test] +fn can_deserialize_with_array_type() { + let foo = r#"{ + "title": "helloworld", + "type": ["string", "array"] + }"#; + + let as_json_schema: JSONSchemaObject = serde_json::from_str(&foo).unwrap(); + + let title = as_json_schema.title.as_ref(); + assert_eq!(title.unwrap(), "helloworld"); + let types_vec: ArrayOfSimpleTypes = vec!(SimpleTypes::String, SimpleTypes::Array); + let t = as_json_schema._type.as_ref(); + assert_eq!(t.unwrap(), &Type::ArrayOfSimpleTypes(types_vec)); + + let back_to_str = serde_json::to_string(&as_json_schema); + assert_eq!(foo.replace(" ", "").replace("\n", ""), back_to_str.unwrap()); +} + + +#[test] +fn can_deserialize_boolean_schema() { + let foo = "true"; + + let as_json_schema: JSONSchema = serde_json::from_str(&foo).unwrap(); + + assert_eq!(as_json_schema, JSONSchema::JSONSchemaBoolean(true)); +} + + +#[test] +fn can_deserialize_nested_schema() { + let foo = r#"{ + "title": "helloworld", + "type": "object", + "properties": { + "foo": { "type": "string", "title": "nestedfoo" } + } + }"#; + + let as_json_schema: JSONSchema = serde_json::from_str(&foo).unwrap(); + + match as_json_schema { + JSONSchema::JSONSchemaObject(as_json_schema) => { + let title = as_json_schema.title.as_ref(); + assert_eq!(title.unwrap(), "helloworld"); + + let subschema_props = as_json_schema + .properties + .unwrap() + .additional_properties + .unwrap(); + let subschema = subschema_props.get("foo").unwrap(); + + match &**subschema { + JSONSchema::JSONSchemaObject(subschema) => { + let sub_title = subschema.title.as_ref(); + assert_eq!(sub_title.unwrap(), "nestedfoo"); + } + JSONSchema::JSONSchemaBoolean(_subschema) => { + assert_eq!(0,1); + } + } + }, + JSONSchema::JSONSchemaBoolean(_as_json_schema) => { + assert_eq!(0,1); + } + } +}