diff --git a/src/json_schema/core.clj b/src/json_schema/core.clj index 1aa98b8..76d72ed 100644 --- a/src/json_schema/core.clj +++ b/src/json_schema/core.clj @@ -5,7 +5,9 @@ [org.everit.json.schema.loader SchemaLoader SchemaClient SchemaLoader$SchemaLoaderBuilder] [org.everit.json.schema Schema] - [org.everit.json.schema ValidationException])) + [org.everit.json.schema ValidationException] + [org.everit.json.schema FormatValidator] + [java.util Optional])) (defn- ^JSONTokener prepare-tokener "Prepares a JSONTokener instance for further processing" @@ -37,10 +39,26 @@ (when-not is-input-valid? (throw (ex-info "Unsupported Schema input" {:input input}))))) +(defn- format-validator [key validation-fn] + (reify FormatValidator + (formatName [_] key) + (validate [_ value] + (if-let [validation-error-message (validation-fn value)] + (Optional/of validation-error-message) + (Optional/empty))))) + (defn ^JSONObject prepare-schema* "Prepares JSON Schema based on input string or map. An optional parameter map can be supplied to refer to relative file schemas via classpath in $ref fields. + Setting :format-validators to map of format name and validation function adds + them as custom format validators. + Validator takes value and returns validation error message or nil if success + (prepare-schema* input { + :format-validators {\"uuid\" (fn [value] + (when-not (uuid? (parse-uuid value)) + (format \"[%s] is not a valid UUID value\" value)))}}) + Setting :classpath-aware? to true enables absolute classpath resolution. (prepare-schema* input {:classpath-aware? true}) @@ -54,20 +72,27 @@ (SchemaLoader/load (JSONObject. (prepare-tokener input)))) ([input params] (assert-schema-input-valid! input) - (if-not (:classpath-aware? params) - (prepare-schema* input) - (let [resolution-scope (:default-resolution-scope params) - set-resolution-scope (fn [^SchemaLoader$SchemaLoaderBuilder builder] - (if resolution-scope - (.resolutionScope builder ^String resolution-scope) - builder)) - schema-loader (-> (SchemaLoader/builder) - (.schemaClient (SchemaClient/classPathAwareClient)) - ^SchemaLoader$SchemaLoaderBuilder (set-resolution-scope) - (.schemaJson - ^JSONObject (JSONObject. (prepare-tokener input))) - (.build))] - (.build (.load schema-loader)))))) + (let [set-format-validators (fn [^SchemaLoader$SchemaLoaderBuilder builder] + (doseq [[k validation-fn] (:format-validators params)] + (.addFormatValidator builder (format-validator k validation-fn))) + builder)] + (if-not (:classpath-aware? params) + (.build (.load (-> (SchemaLoader/builder) + ^SchemaLoader$SchemaLoaderBuilder (set-format-validators) + (.schemaJson ^JSONObject (JSONObject. (prepare-tokener input))) + (.build)))) + (let [resolution-scope (:default-resolution-scope params) + set-resolution-scope (fn [^SchemaLoader$SchemaLoaderBuilder builder] + (if resolution-scope + (.resolutionScope builder ^String resolution-scope) + builder)) + schema-loader (-> (SchemaLoader/builder) + (.schemaClient (SchemaClient/classPathAwareClient)) + ^SchemaLoader$SchemaLoaderBuilder (set-resolution-scope) + ^SchemaLoader$SchemaLoaderBuilder (set-format-validators) + (.schemaJson ^JSONObject (JSONObject. (prepare-tokener input))) + (.build))] + (.build (.load schema-loader))))))) (def ^JSONObject prepare-schema (memoize prepare-schema*)) diff --git a/test/json_schema/core_test.clj b/test/json_schema/core_test.clj index ab084c7..a6dc922 100644 --- a/test/json_schema/core_test.clj +++ b/test/json_schema/core_test.clj @@ -172,3 +172,58 @@ (testing "Valid input as EDN list" (is (= '("a" "b") (json/validate schema '("a" "b")))))))) + +(deftest validate-uuid + (testing "Validate EDN where data is uuid" + (let [uuid-validator (fn [value] + (when-not (uuid? (parse-uuid value)) + (format "[%s] is not a valid UUID value" value))) + json-str-valid "{\"id\" : \"7c0fcade-fdcc-4d48-86a7-da55ebb82f04\"}" + json-str-invalid "{\"id\" : \"Tc0fcade-fdcc-4d48-86a7-da55ebb82f04\"}" + json-str-empty "{\"id\" : \"\"}"] + + (testing "Class path unaware" + (let [schema (json/prepare-schema {:$schema "http://json-schema.org/draft-04/schema" + :id "https://luposlip.com/some-other-schema.json" + :type "object" + :properties {:id {:type "string" + :format "uuid"}}} + {:format-validators {"uuid" uuid-validator}})] + (testing "Valid uuid" + (is (= json-str-valid (json/validate schema json-str-valid)))) + + (testing "Invalid uuid" + (is (thrown-with-msg? + Exception + #"JSON Validation error" + (json/validate schema json-str-invalid)))) + + (testing "Empty instead of uuid" + (is (thrown-with-msg? + Exception + #"JSON Validation error" + (json/validate schema json-str-empty)))))) + + (testing "Class path aware" + (let [schema (json/prepare-schema {:$schema "http://json-schema.org/draft-04/schema" + :id "https://luposlip.com/some-other-schema.json" + :type "object" + :properties {:id {:type "string" + :format "uuid"}}} + {:classpath-aware? true + :default-resolution-scope "classpath://ref/relative/" + :format-validators {"uuid" uuid-validator}})] + (testing "Valid uuid" + (is (= json-str-valid (json/validate schema json-str-valid)))) + + (testing "Invalid uuid" + (is (thrown-with-msg? + Exception + #"JSON Validation error" + (json/validate schema json-str-invalid)))) + + (testing "Empty instead of uuid" + (is (thrown-with-msg? + Exception + #"JSON Validation error" + (json/validate schema json-str-empty)))))))))