Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add json-schema->prismatic transformation support #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions resources/complex.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"name": "Order",
"required": ["order_id", "customer_id", "total", "order_details"],
"additionalProperties": false,
"properties": {
"order_id": { "type": "integer" },
"customer_id": { "type": "integer" },
"total": { "type": "number" },
"order_details": {
"type": "array",
"items": {
"type": "object",
"required": ["quantity", "total", "product_detail"],
"additionalProperties": false,
"properties": {
"quantity": { "type": "integer" },
"total": { "type": "number" },
"product_detail": {
"type": "object",
"required": ["product_id", "product_name", "product_status", "product_tags", "price", "product_properties"],
"additionalProperties": false,
"properties": {
"product_id": { "type": "integer" },
"product_name": { "type": "string" },
"product_description": {
"type": ["null", "string"],
"default": ""
},
"product_status": {
"type": "string",
"enum": ["AVAILABLE", "OUT_OF_STOCK"],
"default": "AVAILABLE"
},
"product_tags": {
"type": "array",
"items": { "type": "string" }
},
"price": { "type": "number" },
"product_properties": {
"type": "object",
"additionalProperties": { "type": "string" }
}
}
}
}
}
}
}
}
12 changes: 12 additions & 0 deletions resources/simple.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "Order",
"required": ["order_id", "customer_id", "total"],
"properties": {
"order_id": { "type": "integer" },
"customer_id": { "type": "integer" },
"total": { "type": "number" }
},
"additionalProperties": false
}
164 changes: 164 additions & 0 deletions src/com/intentmedia/schema_transform/json_transform.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
(ns com.intentmedia.schema-transform.json-transform
(:require [cheshire.core :refer [parse-string]]
[schema.core :as s]))

(declare json-type-transformer)


(def json-primitive->prismatic-primitive
{"boolean" s/Bool
"integer" s/Int
"number" s/Num
"string" s/Str
"null" nil})


(defn predicates
([min max] (predicates min max nil))
([min max unique]
(cond-> []
(and min max)
(conj (s/pred #(<= min (count %) max) (format "(<= %d size %d)" min max)))

min
(conj (s/pred #(<= min (count %)) (format "(<= %d size)" min)))

max
(conj (s/pred #(<= (count %) max) (format "(<= size %d)" max)))

unique
(conj (s/pred #(= % (distinct %)) "unique")))))


(defn add-preds [schema preds]
(if (empty? preds)
schema
(apply s/both (cons schema preds))))


(defn json-object-props-transformer [json-object-type]
(let [properties (:properties json-object-type)
required (->> json-object-type :required (map keyword) (into #{}))
required? (partial contains? required)]
(->> properties
(map
(fn [[name schema]]
(let [key-modifier (if (required? name)
identity
s/optional-key)]
[(key-modifier name)
(json-type-transformer schema)])))
(reduce
(fn [combiner [k v]]
(assoc combiner k v))
{}))))


(defn json-object-additional-props-transformer [transformed add-props]
(cond
(false? add-props)
transformed

(map? add-props)
(assoc transformed s/Str (json-type-transformer add-props))

:else
(assoc transformed s/Str s/Any)))


(defn json-object-transformer [json-object-type]
(let [add-props (:additionalProperties json-object-type)
preds (predicates (:minProperties json-object-type) (:maxProperties json-object-type))]
(-> (json-object-props-transformer json-object-type)
(json-object-additional-props-transformer add-props)
(add-preds preds))))


(defn json-tuple-transformer [json-array-type]
(let [schema (->> (:items json-array-type)
(map-indexed #(s/optional (json-type-transformer %2) (str (inc %1))))
(into []))]
(if (false? (:additionalItems json-array-type))
schema
(conj schema s/Any))))


(defn json-list-transformer [json-array-type]
[(json-type-transformer (:items json-array-type))])


(defn json-array-transformer [json-array-type]
(let [preds (predicates
(:minItems json-array-type)
(:maxItems json-array-type)
(:uniqueItems json-array-type))]
(-> (if (map? (:items json-array-type))
(json-list-transformer json-array-type)
(json-tuple-transformer json-array-type))
(add-preds preds))))


(defn json-nil? [type]
(= "null" type))

(defn enum? [json-type]
(vector? (:enum json-type)))

(defn nilable? [types]
(some json-nil? types))

(defn union? [types]
(and (vector? types)
(> (count types) 0)))


(defn json-enum-transformer [json-enum-type]
(apply s/enum (:enum json-enum-type)))


(defn json-nilable-transformer [json-nilable-type]
(let [types (into [] (remove json-nil? (:type json-nilable-type)))]
(s/maybe (json-type-transformer
(assoc json-nilable-type :type types)))))


(defn json-union-type-transformer [json-union-type]
(let [types (:type json-union-type)]
(if (= 1 (count types))
(json-primitive->prismatic-primitive (first types))
(apply s/cond-pre (map json-primitive->prismatic-primitive types)))))


(def json-type->transformer
{"object" json-object-transformer
"array" json-array-transformer})


(defn json-type-transformer [json-type]
(let [type (:type json-type)]
(cond
(nilable? type)
(json-nilable-transformer json-type)

(enum? json-type)
(json-enum-transformer json-type)

(union? type)
(json-union-type-transformer json-type)

(contains? json-primitive->prismatic-primitive type)
(json-primitive->prismatic-primitive type)

:else
(let [transformer (json-type->transformer type)]
(if transformer
(transformer json-type)
(throw (ex-info (str "No transformer for type " type) {:json-type json-type})))))))


(defn json-parsed->prismatic [json]
(json-type-transformer json))


(defn json->prismatic [json]
(json-parsed->prismatic (parse-string json true)))
79 changes: 79 additions & 0 deletions test/com/intentmedia/schema_transform/json_transform_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
(ns com.intentmedia.schema-transform.json-transform-test
(:require [clojure.test :refer :all]
[clojure.java.io :as io]

[schema.core :as s]
[cheshire.core :refer [generate-string]]
[com.intentmedia.schema-transform.json-transform :refer :all]))


(defn- read-schema [filename]
(slurp (io/file (io/resource filename))))


(deftest test-primitive-types
(are [json prismatic] (= prismatic (json-primitive->prismatic-primitive json))
"boolean" s/Bool
"integer" s/Int
"number" s/Num
"string" s/Str
"null" nil))


(deftest test-object-additional-properties
(are [json prismatic] (= prismatic (json->prismatic (generate-string json)))
{:type "object"}
{s/Str s/Any}

{:type "object"
:additionalProperties false}
{}

{:type "object"
:additionalProperties {:type "boolean"}}
{s/Str s/Bool}))


(deftest test-array-schemas
(testing "test uniform list"
(are [json prismatic] (= prismatic (json->prismatic (generate-string json)))
{:type "array"}
[s/Any]

{:type "array"
:items {:type "number"}}
[s/Num]))

(testing "test tuple"
(are [json prismatic] (= prismatic (json->prismatic (generate-string json)))
{:type "array"
:items [{:type "integer"} {:type "string"}]}
[(s/optional s/Int "1") (s/optional s/Str "2") s/Any]

{:type "array"
:additionalItems false
:items [{:type "integer"} {:type "string" :enum ["one" "two" "zero"]}]}
[(s/optional s/Int "1") (s/optional (s/enum "one" "two" "zero") "2")])))


(deftest test-json-transform
(testing "Converts a simple object type"
(is (= {:order_id s/Int
:customer_id s/Int
:total s/Num}
(json->prismatic (read-schema "simple.json")))))

(testing "Converts a complex object type"
(is (= {:order_id s/Int
:customer_id s/Int
:total s/Num
:order_details [{:quantity s/Int
:total s/Num
:product_detail {:product_id s/Int
:product_name s/Str
(s/optional-key :product_description) (s/maybe s/Str)
:product_status (s/enum "AVAILABLE" "OUT_OF_STOCK")
:product_tags [s/Str]
:price s/Num
:product_properties {s/Str s/Str}}}]}
(json->prismatic (read-schema "complex.json"))))))