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..64b2943 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.13" 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/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 9492e22..8096e40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,9 @@ extern crate serde; extern crate serde_json; extern crate derive_builder; -use serde::{Serialize, Deserialize}; use derive_builder::Builder; +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; pub type Id = String; pub type Schema = String; pub type Ref = String; @@ -22,10 +23,10 @@ pub type NonNegativeInteger = i64; pub type NonNegativeIntegerDefaultZero = i64; pub type Pattern = String; pub type SchemaArray = Vec; -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(untagged)] pub enum Items { - JSONSchema(JSONSchema), + JSONSchema(Box), SchemaArray(SchemaArray), } pub type UniqueItems = bool; @@ -43,54 +44,46 @@ pub type StringArray = Vec; /// /// {} /// -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Builder, Default)] -#[builder(setter(strip_option), default)] -#[serde(default)] -pub struct Definitions { - #[serde(flatten)] - pub additional_properties: Option -} +pub type Definitions = HashMap; /// Properties /// /// # Default /// /// {} /// -#[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 Properties = HashMap; /// PatternProperties /// /// # Default /// /// {} /// -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Builder, Default)] -#[builder(setter(strip_option), default)] -#[serde(default)] -pub struct PatternProperties { - #[serde(flatten)] - pub additional_properties: Option -} -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -#[serde(untagged)] +pub type PatternProperties = HashMap; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub enum DependenciesSet { - JSONSchema(JSONSchema), + JSONSchema(Box), StringArray(StringArray), } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Builder, Default)] -#[builder(setter(strip_option), default)] -#[serde(default)] -pub struct Dependencies { - #[serde(flatten)] - pub additional_properties: Option -} +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, Clone, Debug, Eq, PartialEq)] #[serde(untagged)] @@ -98,112 +91,114 @@ pub enum Type { SimpleTypes(SimpleTypes), ArrayOfSimpleTypes(ArrayOfSimpleTypes), } + pub type Format = String; pub type ContentMediaType = String; pub type ContentEncoding = String; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Builder, Default)] #[builder(setter(strip_option), default)] #[serde(default)] pub struct JSONSchemaObject { - #[serde(rename="$id", skip_serializing_if("Option::is_none"))] + #[serde(rename="$id", skip_serializing_if = "Option::is_none")] pub id: Option, - #[serde(rename="$schema", skip_serializing_if("Option::is_none"))] + #[serde(rename="$schema", skip_serializing_if = "Option::is_none")] pub schema: Option, - #[serde(rename="$ref", skip_serializing_if("Option::is_none"))] + #[serde(rename="$ref", skip_serializing_if = "Option::is_none")] pub _ref: Option, - #[serde(rename="$comment", skip_serializing_if("Option::is_none"))] + #[serde(rename="$comment", skip_serializing_if = "Option::is_none")] pub comment: Option, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option<Description>, - #[serde(rename="default", skip_serializing_if("Option::is_none"))] + #[serde(rename="default", skip_serializing_if = "Option::is_none")] pub _default: Option<AlwaysTrue>, - #[serde(rename="readOnly", skip_serializing_if("Option::is_none"))] + #[serde(rename="readOnly", skip_serializing_if = "Option::is_none")] pub read_only: Option<ReadOnly>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub examples: Option<Examples>, - #[serde(rename="multipleOf", skip_serializing_if("Option::is_none"))] + #[serde(rename="multipleOf", skip_serializing_if = "Option::is_none")] pub multiple_of: Option<MultipleOf>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub maximum: Option<Maximum>, - #[serde(rename="exclusiveMaximum", skip_serializing_if("Option::is_none"))] + #[serde(rename="exclusiveMaximum", skip_serializing_if = "Option::is_none")] pub exclusive_maximum: Option<ExclusiveMaximum>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub minimum: Option<Minimum>, - #[serde(rename="exclusiveMinimum", skip_serializing_if("Option::is_none"))] + #[serde(rename="exclusiveMinimum", skip_serializing_if = "Option::is_none")] pub exclusive_minimum: Option<ExclusiveMinimum>, - #[serde(rename="maxLength", skip_serializing_if("Option::is_none"))] + #[serde(rename="maxLength", skip_serializing_if = "Option::is_none")] pub max_length: Option<NonNegativeInteger>, - #[serde(rename="minLength", skip_serializing_if("Option::is_none"))] + #[serde(rename="minLength", skip_serializing_if = "Option::is_none")] pub min_length: Option<NonNegativeIntegerDefaultZero>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub pattern: Option<Pattern>, - #[serde(rename="additionalItems", skip_serializing_if("Option::is_none"))] - pub additional_items: Option<JSONSchema>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(rename="additionalItems", skip_serializing_if = "Option::is_none")] + pub additional_items: Option<Box<JSONSchema>>, + #[serde(skip_serializing_if = "Option::is_none")] pub items: Option<Items>, - #[serde(rename="maxItems", skip_serializing_if("Option::is_none"))] + #[serde(rename="maxItems", skip_serializing_if = "Option::is_none")] pub max_items: Option<NonNegativeInteger>, - #[serde(rename="minItems", skip_serializing_if("Option::is_none"))] + #[serde(rename="minItems", skip_serializing_if = "Option::is_none")] pub min_items: Option<NonNegativeIntegerDefaultZero>, - #[serde(rename="uniqueItems", skip_serializing_if("Option::is_none"))] + #[serde(rename="uniqueItems", skip_serializing_if = "Option::is_none")] pub unique_items: Option<UniqueItems>, - #[serde(skip_serializing_if("Option::is_none"))] - pub contains: Option<JSONSchema>, - #[serde(rename="maxProperties", skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub contains: Option<Box<JSONSchema>>, + #[serde(rename="maxProperties", skip_serializing_if = "Option::is_none")] pub max_properties: Option<NonNegativeInteger>, - #[serde(rename="minProperties", skip_serializing_if("Option::is_none"))] + #[serde(rename="minProperties", skip_serializing_if = "Option::is_none")] pub min_properties: Option<NonNegativeIntegerDefaultZero>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub required: Option<StringArray>, - #[serde(rename="additionalProperties", skip_serializing_if("Option::is_none"))] - pub additional_properties: Option<JSONSchema>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(rename="additionalProperties", skip_serializing_if = "Option::is_none")] + pub additional_properties: Option<Box<JSONSchema>>, + #[serde(skip_serializing_if = "Option::is_none")] pub definitions: Option<Definitions>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub properties: Option<Properties>, - #[serde(rename="patternProperties", skip_serializing_if("Option::is_none"))] + #[serde(rename="patternProperties", skip_serializing_if = "Option::is_none")] pub pattern_properties: Option<PatternProperties>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub dependencies: Option<Dependencies>, - #[serde(rename="propertyNames", skip_serializing_if("Option::is_none"))] - pub property_names: Option<JSONSchema>, - #[serde(rename="const", skip_serializing_if("Option::is_none"))] + #[serde(rename="propertyNames", skip_serializing_if = "Option::is_none")] + pub property_names: Option<Box<JSONSchema>>, + #[serde(rename="const", skip_serializing_if = "Option::is_none")] pub _const: Option<AlwaysTrue>, - #[serde(rename="enum", skip_serializing_if("Option::is_none"))] + #[serde(rename="enum", skip_serializing_if = "Option::is_none")] pub _enum: Option<Enum>, - #[serde(rename="type", skip_serializing_if("Option::is_none"))] + #[serde(rename="type", skip_serializing_if = "Option::is_none")] pub _type: Option<Type>, - #[serde(skip_serializing_if("Option::is_none"))] + #[serde(skip_serializing_if = "Option::is_none")] pub format: Option<Format>, - #[serde(rename="contentMediaType", skip_serializing_if("Option::is_none"))] + #[serde(rename="contentMediaType", skip_serializing_if = "Option::is_none")] pub content_media_type: Option<ContentMediaType>, - #[serde(rename="contentEncoding", skip_serializing_if("Option::is_none"))] + #[serde(rename="contentEncoding", skip_serializing_if = "Option::is_none")] pub content_encoding: Option<ContentEncoding>, - #[serde(rename="if", skip_serializing_if("Option::is_none"))] - pub _if: Option<JSONSchema>, - #[serde(skip_serializing_if("Option::is_none"))] - pub then: Option<JSONSchema>, - #[serde(rename="else", skip_serializing_if("Option::is_none"))] - pub _else: Option<JSONSchema>, - #[serde(rename="allOf", skip_serializing_if("Option::is_none"))] + #[serde(rename="if", skip_serializing_if = "Option::is_none")] + pub _if: Option<Box<JSONSchema>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub then: Option<Box<JSONSchema>>, + #[serde(rename="else", skip_serializing_if = "Option::is_none")] + pub _else: Option<Box<JSONSchema>>, + #[serde(rename="allOf", skip_serializing_if = "Option::is_none")] pub all_of: Option<SchemaArray>, - #[serde(rename="anyOf", skip_serializing_if("Option::is_none"))] + #[serde(rename="anyOf", skip_serializing_if = "Option::is_none")] pub any_of: Option<SchemaArray>, - #[serde(rename="oneOf", skip_serializing_if("Option::is_none"))] + #[serde(rename="oneOf", skip_serializing_if = "Option::is_none")] pub one_of: Option<SchemaArray>, - #[serde(skip_serializing_if("Option::is_none"))] - pub not: Option<JSONSchema>, + #[serde(skip_serializing_if = "Option::is_none")] + pub not: Option<Box<JSONSchema>>, } /// JSONSchemaBoolean /// /// Always valid if true. Never valid if false. Is constant. /// pub type JSONSchemaBoolean = bool; -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(untagged)] pub enum JSONSchema { JSONSchemaObject(JSONSchemaObject), JSONSchemaBoolean(JSONSchemaBoolean), -} \ No newline at end of file +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..cdc5227 --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,107 @@ +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(); + 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); + } + } +}