├── test ├── juxt │ └── jinx │ │ ├── test.json │ │ ├── demo.clj │ │ ├── clj_transform_test.cljc │ │ ├── regex_test.cljc │ │ ├── coercion_test.cljc │ │ ├── petstore.edn │ │ ├── annotation_test.cljc │ │ ├── resolve_test.cljc │ │ ├── official_test.cljc │ │ ├── validate_test.cljc │ │ └── schema_test.cljc └── cljs-test-opts.edn ├── .gitignore ├── .dir-locals.el ├── jsonschema.cljs.edn ├── .gitmodules ├── src └── juxt │ └── jinx │ ├── alpha │ ├── core.cljc │ ├── clj_transform.cljc │ ├── regex.cljc │ ├── jsonpointer.cljc │ ├── resolve.cljc │ ├── patterns.cljs │ ├── schema.cljc │ └── patterns.clj │ └── alpha.clj ├── LICENSE ├── Makefile ├── .circleci └── config.yml ├── deps.edn ├── pom.xml ├── todo.org ├── README.adoc ├── resources └── schemas │ └── json-schema.org │ └── draft-07 │ └── schema └── spec ├── draft-handrews-relative-json-pointer-01 ├── rfc2673 ├── rfc6901 ├── rfc6532 └── rfc2234 /test/juxt/jinx/test.json: -------------------------------------------------------------------------------- 1 | {"foo": "bar"} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.nrepl-port 2 | /.cpcache 3 | /cljs-test-runner-out 4 | /target 5 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil 2 | (cider-clojure-cli-global-options . "-R:dev:dev-nrepl:test -C:dev:dev-nrepl:test"))) 3 | -------------------------------------------------------------------------------- /jsonschema.cljs.edn: -------------------------------------------------------------------------------- 1 | ^{:auto-testing true 2 | :open-url false 3 | :watch-dirs ["src" "test"]} 4 | {:main juxt.jinx.validate} 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "official-test-suite"] 2 | path = official-test-suite 3 | url = https://github.com/json-schema-org/JSON-Schema-Test-Suite 4 | -------------------------------------------------------------------------------- /test/cljs-test-opts.edn: -------------------------------------------------------------------------------- 1 | {:optimizations :none 2 | :cache-analysis true 3 | :pseudo-names true 4 | :infer-externs false 5 | :pretty-print true} 6 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha/core.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha.core 4 | (:refer-clojure :exclude [number? integer? array? object?])) 5 | 6 | (defn number? [x] 7 | (clojure.core/number? x)) 8 | 9 | (defn integer? [x] 10 | (or 11 | (clojure.core/integer? x) 12 | (when (number? x) 13 | (zero? (mod x 1))))) 14 | 15 | (defn array? [x] 16 | (sequential? x)) 17 | 18 | (defn object? [x] 19 | (map? x)) 20 | 21 | (defn schema? [x] 22 | (or (object? x) (boolean? x))) 23 | 24 | 25 | (defn regex? [x] 26 | (let [valid? (atom true)] 27 | (try 28 | #?(:clj (java.util.regex.Pattern/compile x) :cljs (new js/RegExp. x)) 29 | (catch #?(:clj Exception :cljs js/Error) e 30 | (reset! valid? false))) 31 | @valid?)) 32 | -------------------------------------------------------------------------------- /test/juxt/jinx/demo.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.demo 4 | (:require 5 | [juxt.jinx.alpha :as jinx])) 6 | 7 | (comment 8 | (jinx/validate 9 | {"firstName" "John" 10 | "lastName" "Doe" 11 | "age" 21} 12 | (jinx/schema 13 | {"$id" "https://example.com/person.schema.json" 14 | "$schema" "http://json-schema.org/draft-07/schema#" 15 | "title" "Person" 16 | "type" "object" 17 | "properties" 18 | {"firstName" 19 | {"type" "string" 20 | "description" "The person's first name."} 21 | "lastName" 22 | {"type" "string" 23 | "description" "The person's last name."} 24 | "age" {"description" "Age in years which must be equal to or greater than zero." 25 | "type" "integer" 26 | "minimum" 0}}}))) 27 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha 4 | (:require 5 | [juxt.jinx.alpha.schema :as schema] 6 | [juxt.jinx.alpha.validate :as validate] 7 | [juxt.jinx.alpha.clj-transform :as transform])) 8 | 9 | (defn schema 10 | "Build a JSON Schema from a map (or boolean). Must conform to 11 | rules. Returns a map that can be used in validation." 12 | ([s] (schema/schema s)) 13 | ([s options] (schema/schema s options))) 14 | 15 | (defn validate 16 | "Validate a map (or boolean) according to the given schema." 17 | ([schema instance] (validate/validate schema instance)) 18 | ([schema instance options] (validate/validate schema instance options))) 19 | 20 | (defn ^:jinx/experimental clj->jsch 21 | "Transform a Clojure syntax shorthand into JSON Schema and build it." 22 | [clj] 23 | (schema/schema (transform/clj->jsch clj))) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2019 JUXT LTD. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := test 2 | 3 | .PHONY: test watch 4 | 5 | STYLESDIR = ../asciidoctor-stylesheet-factory/stylesheets 6 | STYLESHEET = juxt.css 7 | 8 | .PHONY: watch default deploy test 9 | 10 | official-test: 11 | clj -Atest -i :official 12 | 13 | test-clj: 14 | clojure -Atest -e deprecated 15 | 16 | test-cljs: 17 | rm -rf cljs-test-runner-out && mkdir -p cljs-test-runner-out/gen && clojure -Sverbose -Atest-cljs 18 | 19 | test: 20 | make test-clj && make test-cljs 21 | 22 | lint: 23 | clj-kondo --lint src/juxt --lint test/juxt 24 | 25 | watch: 26 | find . -regex ".*\\.clj[cs]?" | entr make test 27 | 28 | pom: 29 | rm pom.xml; clojure -Spom; echo "Now use git diff to add back in the non-generated bits of pom" 30 | # Dev pom is used to created development project with intellij 31 | dev-pom: 32 | rm pom.xml && clojure -R:dev:dev-rebel:dev-nrepl:test-cljs -C:dev:dev-rebel:dev-nrepl:test-cljs -Spom 33 | 34 | deploy: 35 | pom 36 | mvn deploy 37 | 38 | figwheel: 39 | clojure -R:dev:dev-nrepl:dev-rebel -C:dev:dev-nrepl:dev-rebel:test -m figwheel.main --build jsonschema --repl 40 | 41 | # hooray for stackoverflow 42 | .PHONY: list 43 | list: 44 | @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | xargs 45 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Clojure CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-clojure/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/clojure:tools-deps-1.10.0.442-node 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/postgres:9.4 16 | 17 | working_directory: ~/repo 18 | 19 | environment: 20 | # Customize the JVM maximum heap limit 21 | JVM_OPTS: -Xmx3200m 22 | 23 | steps: 24 | - checkout 25 | 26 | # Download and cache dependencies 27 | - restore_cache: 28 | keys: 29 | - v1-dependencies-{{ checksum "deps.edn" }} 30 | # fallback to using the latest cache if no exact match is found 31 | - v1-dependencies- 32 | 33 | # need a few bits for cljs test - there must be a better way of including? 34 | - run: sudo npm install karma-cli -g 35 | - run: npm install karma --save-dev 36 | - run: npm install karma-cljs-test 37 | 38 | - run: sudo apt-get install -y make 39 | - run: make test 40 | 41 | - save_cache: 42 | paths: 43 | - ~/.m2 44 | key: v1-dependencies-{{ checksum "deps.edn" }} 45 | -------------------------------------------------------------------------------- /test/juxt/jinx/clj_transform_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.clj-transform-test 4 | (:require 5 | [juxt.jinx.alpha.clj-transform :refer [clj->jsch]] 6 | [clojure.test :refer [deftest is]] 7 | #?(:clj 8 | [clojure.test :refer [deftest is testing]] 9 | :cljs 10 | [cljs.test :refer-macros [deftest is testing run-tests]]))) 11 | 12 | (deftest clj->jsch-test 13 | (is (= {"type" "string"} (clj->jsch 'string))) 14 | (is (= {"type" "integer"} (clj->jsch 'integer))) 15 | (is (= {"type" "object"} (clj->jsch 'object))) 16 | (is (= {"type" "array" "items" {"type" "string"}} (clj->jsch '[string]))) 17 | (is (= {"type" "array" "items" [{"type" "string"}{"type" "integer"}]} (clj->jsch '(string integer)))) 18 | (is (= {"type" "null"} (clj->jsch nil))) 19 | (is (= {"allOf" [{"type" "string"}{"type" "integer"}]} (clj->jsch '(all-of string integer)))) 20 | (is (= {"oneOf" [{"type" "string"}{"type" "integer"}]} (clj->jsch '(one-of string integer)))) 21 | (is (= {"anyOf" [{"type" "string"}{"type" "integer"}]} (clj->jsch '(any-of string integer)))) 22 | (is (= {"properties" 23 | {"a" {"type" "array", "items" {"type" "string"}}, 24 | "b" {"type" "string", "constant" "20"}}, 25 | "required" ["a"]} 26 | (clj->jsch {:properties {"a" ['string] 27 | "b" "20"} 28 | :required ["a"]}))) 29 | #?(:clj 30 | (is (= {"pattern" "\\w+"} (clj->jsch #"\w+"))))) 31 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha/clj_transform.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha.clj-transform) 4 | 5 | (defn clj->jsch [x] 6 | (cond 7 | (vector? x) 8 | (if (= 1 (count x)) 9 | {"type" "array" "items" (clj->jsch (first x))} 10 | (throw (ex-info "Vector can only contain one item, the type of the array items" {}))) 11 | 12 | (boolean? x) 13 | {"type" "boolean" "constant" x} 14 | 15 | (integer? x) 16 | {"type" "integer" "constant" x} 17 | 18 | (number? x) 19 | {"type" "number" "constant" x} 20 | 21 | (string? x) 22 | {"type" "string" "constant" x} 23 | 24 | (list? x) 25 | (cond 26 | (= (first x) 'all-of) {"allOf" (mapv clj->jsch (rest x))} 27 | (= (first x) 'one-of) {"oneOf" (mapv clj->jsch (rest x))} 28 | (= (first x) 'any-of) {"anyOf" (mapv clj->jsch (rest x))} 29 | (= (first x) 'not) {"not" (clj->jsch (second x))} 30 | :else 31 | {"type" "array" "items" (mapv clj->jsch x)}) 32 | 33 | (nil? x) {"type" "null"} 34 | 35 | (symbol? x) 36 | (cond 37 | (#{'string 'integer 'boolean 'number 'object} x) 38 | {"type" (name x)} 39 | :else (throw (ex-info "Unexpected symbol" {:symbol x}))) 40 | 41 | (map? x) 42 | (reduce-kv 43 | (fn [acc k v] 44 | (assoc acc 45 | (if (and (keyword? k) (nil? (namespace k))) 46 | (name k) k) 47 | (case k 48 | :properties (reduce-kv 49 | (fn [acc k v] 50 | (assoc acc 51 | k (clj->jsch v) 52 | )) 53 | {} v) 54 | v))) 55 | {} x) 56 | 57 | #?@(:clj 58 | [(instance? java.util.regex.Pattern x) 59 | {"pattern" (str x)}]))) 60 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha/regex.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha.regex 4 | (:require 5 | [clojure.set :as set] 6 | [clojure.string :as str] 7 | [juxt.jinx.alpha.patterns :as patterns])) 8 | 9 | (def addr-spec patterns/addr-spec) 10 | (comment 11 | (re-matches addr-spec "mal@juxt.pro")) 12 | 13 | (def iaddr-spec patterns/iaddr-spec) 14 | 15 | (defn hostname? [s] 16 | (and 17 | (re-matches patterns/subdomain s) 18 | ;; "Labels must be 63 characters or less." -- RFC 1034, Section 3.5 19 | (<= (apply max (map count (str/split s #"\."))) 63) 20 | ;; "To simplify implementations, the total number of octets that 21 | ;; represent a domain name (i.e., the sum of all label octets and 22 | ;; label lengths) is limited to 255." -- RFC 1034, Section 3.1 23 | (<= (count s) 255))) 24 | 25 | 26 | (defn idn-hostname? [s] 27 | (when-let [ace #?(:clj (try 28 | (java.net.IDN/toASCII s) 29 | (catch IllegalArgumentException e 30 | ;; Catch an error indicating this is not valid 31 | ;; idn-hostname 32 | )) 33 | :cljs s)] 34 | (and 35 | ;; Ensure no illegal chars 36 | (empty? (set/intersection (set (seq s)) 37 | #{\u302E ; Hangul single dot tone mark 38 | })) 39 | ;; Ensure ASCII version is a valid hostname 40 | (hostname? ace)))) 41 | 42 | (def IPv4address patterns/IPv4address) 43 | (def IPv6address patterns/IPv6address) 44 | 45 | (def URI patterns/URI) 46 | (def relative-ref patterns/relative-ref) 47 | 48 | (def IRI patterns/IRI) 49 | (def irelative-ref patterns/irelative-ref) 50 | 51 | (def json-pointer patterns/json-pointer) 52 | (def relative-json-pointer patterns/relative-json-pointer) 53 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | {:paths ["src" "test" "resources"] 4 | :deps 5 | {cheshire/cheshire {:mvn/version "5.8.1"} 6 | cljs-node-io/cljs-node-io {:mvn/version "1.1.2"} 7 | lambdaisland/uri {:mvn/version "1.1.0"}} 8 | :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] 9 | :aliases 10 | {:dev 11 | {:extra-deps 12 | {com.bhauman/figwheel-main {:mvn/version "0.2.0"} 13 | com.bhauman/cljs-test-display {:mvn/version "0.1.1"} 14 | org.clojure/clojurescript {:mvn/version "1.10.520"} 15 | } 16 | 17 | :extra-paths ["dev/src" "test"] 18 | :jvm-opts ["-Dclojure.spec.compile-asserts=true"]} 19 | :test-cljs {:extra-paths ["test" "cljs-test-runner-out/gen"] 20 | :extra-deps {org.clojure/clojurescript {:mvn/version "1.10.520"} 21 | olical/cljs-test-runner {:mvn/version "3.5.0"}} 22 | :main-opts ["-m" "cljs-test-runner.main" "-c" "test/cljs-test-opts.edn"]} 23 | :test {:extra-paths ["test"] 24 | :extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" 25 | :sha "028a6d41ac9ac5d5c405dfc38e4da6b4cc1255d5"}} 26 | :main-opts ["-m" "cognitect.test-runner"]} 27 | 28 | :dev-nrepl {:jvm-opts ["-Dnrepl.load=true"] 29 | :extra-paths ["aliases/nrepl"] 30 | :extra-deps 31 | {com.cemerick/piggieback {:mvn/version "0.2.2"} 32 | org.clojure/tools.nrepl {:mvn/version "0.2.12"} 33 | org.clojure/tools.trace {:mvn/version "0.7.10"}}} 34 | 35 | :dev-rebel {:extra-paths ["aliases/rebel"] 36 | :extra-deps {com.bhauman/rebel-readline {:mvn/version "0.1.1"} 37 | com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"} 38 | io.aviso/pretty {:mvn/version "0.1.34"}} 39 | :main-opts ["-m" "jsonschema.rebel.main"]}}} 40 | -------------------------------------------------------------------------------- /test/juxt/jinx/regex_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.regex-test 4 | #?@(:clj [(:require 5 | [juxt.jinx.alpha.regex :as regex] 6 | [juxt.jinx.alpha.patterns :as patterns] 7 | [clojure.test :refer [deftest is are testing]])] 8 | :cljs [(:require 9 | [juxt.jinx.alpha.regex :as regex] 10 | [juxt.jinx.alpha.patterns :as patterns] 11 | [cljs.test :refer-macros [deftest is are testing run-tests ]])])) 12 | 13 | #?(:clj 14 | (do 15 | (deftest iri-test 16 | (let [m (patterns/matched regex/IRI "https://jon:password@juxt.pro:8080/site/index.html?debug=true#bar")] 17 | (are [group expected] (= expected (patterns/re-group-by-name m group)) 18 | "scheme" "https" 19 | "authority" "jon:password@juxt.pro:8080" 20 | "userinfo" "jon:password" 21 | "host" "juxt.pro" 22 | "port" "8080" 23 | "path" "/site/index.html" 24 | "query" "debug=true" 25 | "fragment" "bar"))) 26 | 27 | (deftest addr-spec-test 28 | (let [m (patterns/matched regex/addr-spec "mal@juxt.pro")] 29 | (are [group expected] (= expected (patterns/re-group-by-name m group)) 30 | "localpart" "mal" 31 | "domain" "juxt.pro")))) 32 | 33 | 34 | ;; TODO: ipv6 tests from rfc2234 35 | 36 | :cljs 37 | (do 38 | (deftest addr-spec-test 39 | (are [x y] (= y (patterns/parse "addr-spec-test" x)) 40 | "mal@juxt.pro" 41 | ["mal" "juxt.pro"])) 42 | 43 | (deftest iri-test 44 | (are [x y] (= y (patterns/parse "iri-test" x)) 45 | "https://jon:password@juxt.pro:8080/site/index.html?debug=true#bar" 46 | ["https" "jon:password" "juxt.pro" "8080" "/site/index.html" "debug=true" "bar" ] 47 | "http://user:password@example.com:8080/path?query=value#fragment" 48 | ["http" "user:password" "example.com" "8080" "/path" "query=value" "fragment"] 49 | "http://example.com" 50 | ["http" nil "example.com" nil "" nil nil] 51 | )))) 52 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jinx 5 | jinx 6 | 0.1.5 7 | jinx 8 | jinx is not xml-schema (it's json-schema!) 9 | https://github.com/juxt/jinx 10 | 11 | https://github.com/juxt/jinx 12 | 13 | 14 | UTF-8 15 | 16 | 17 | 18 | org.clojure 19 | clojure 20 | 1.10.1 21 | 22 | 23 | cheshire 24 | cheshire 25 | 5.8.1 26 | 27 | 28 | cljs-node-io 29 | cljs-node-io 30 | 1.1.2 31 | 32 | 33 | lambdaisland 34 | uri 35 | 1.1.0 36 | 37 | 38 | 39 | 40 | 41 | src 42 | 43 | 44 | resources 45 | 46 | 47 | src 48 | 49 | 50 | 51 | clojars 52 | Clojars repository 53 | https://clojars.org/repo 54 | 55 | 56 | 57 | 58 | clojars 59 | https://repo.clojars.org/ 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /test/juxt/jinx/coercion_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.coercion-test 4 | #?@(:clj [(:require 5 | [juxt.jinx.alpha.validate :as validate] 6 | [clojure.test :refer [deftest is are testing]])] 7 | :cljs [(:require 8 | [juxt.jinx.alpha.validate :as validate] 9 | [cljs.test :refer-macros [deftest is are testing run-tests]])])) 10 | 11 | (deftest coercion-test 12 | (testing "coerce string to integer" 13 | (is (= 14 | 123 15 | (:instance 16 | (validate/validate 17 | {"type" "integer"} 18 | "123" 19 | {:coercions {#?(:clj String :cljs "string") 20 | {"integer" (fn [x] (#?(:clj Integer/parseInt :cljs js/parseInt) x))}}})))) 21 | 22 | 23 | (is (= {"foo" 123} 24 | (:instance 25 | (validate/validate 26 | {"properties" {"foo" {"type" "integer"}}} 27 | {"foo" "123"} 28 | {:coercions {#?(:clj String :cljs "string") 29 | {"integer" (fn [x] 30 | (#?(:clj Integer/parseInt :cljs js/parseInt) x))}}}))))) 31 | 32 | (testing "coerce single string to integer array" 33 | (is 34 | (= {"foo" [123]} 35 | (:instance 36 | (validate/validate 37 | {"properties" {"foo" {"type" "array" 38 | "items" {"type" "integer"}}}} 39 | {"foo" "123"} 40 | {:coercions {#?(:clj String :cljs "string") 41 | {"array" vector 42 | "integer" (fn [x] 43 | (#?(:clj Integer/parseInt :cljs js/parseInt) x))}}}))))) 44 | 45 | (testing "coerce single array to integer array" 46 | (is 47 | (= {"foo" [123 456]} 48 | (:instance 49 | (validate/validate 50 | {"properties" {"foo" {"type" "array" 51 | "items" {"type" "integer"}}}} 52 | {"foo" ["123" "456"]} 53 | {:coercions {#?(:clj String :cljs "string") 54 | {"integer" (fn [x] 55 | (#?(:clj Integer/parseInt :cljs js/parseInt) x))}}})))))) 56 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha/jsonpointer.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019-2021, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha.jsonpointer 4 | (:require 5 | [clojure.string :as str])) 6 | 7 | (def reference-token-pattern #"/((?:[^/~]|~0|~1)*)") 8 | 9 | (defn decode [token] 10 | (-> token 11 | (str/replace "~1" "/") 12 | (str/replace "~0" "~"))) 13 | 14 | (defn reference-tokens [s] 15 | (map decode (map second (re-seq reference-token-pattern s)))) 16 | 17 | (defn json-pointer [doc pointer] 18 | (loop [tokens (reference-tokens (or pointer "")) 19 | subdoc doc] 20 | (if (seq tokens) 21 | (recur 22 | (next tokens) 23 | (cond 24 | (map? subdoc) 25 | (let [subsubdoc (get subdoc (first tokens))] 26 | (if (some? subsubdoc) subsubdoc 27 | (throw (ex-info "Failed to locate" {:json-pointer pointer 28 | :subsubdoc subsubdoc 29 | :subdoc subdoc 30 | :tokens tokens 31 | :first-token (first tokens) 32 | :type-subdoc (type subdoc) 33 | :doc doc 34 | :debug (get subdoc (first tokens)) 35 | })))) 36 | (sequential? subdoc) 37 | (if (re-matches #"[0-9]+" (first tokens)) 38 | (let [subsubdoc 39 | (get subdoc #?(:clj (Integer/parseInt (first tokens)) 40 | :cljs (js/Number (first tokens))))] 41 | (if (some? subsubdoc) 42 | subsubdoc 43 | (throw (ex-info "Failed to locate" {:json-pointer pointer 44 | :subdoc subdoc 45 | :doc doc})))) 46 | (throw (ex-info "Failed to locate, must be a number" {:json-pointer pointer 47 | :subdoc subdoc 48 | :doc doc}))))) 49 | subdoc))) 50 | 51 | (comment 52 | (json-pointer 53 | {"a" [{"b" "alpha"} {"b" [{"c" {"greek" "delta"}}]}]} 54 | "/a/1/b/0/c/greek")) 55 | 56 | (comment 57 | (json-pointer 58 | {"a" [{"b" "alpha"} {"b" [{"c" {"greek" "delta"}}]}]} 59 | nil)) 60 | -------------------------------------------------------------------------------- /test/juxt/jinx/petstore.edn: -------------------------------------------------------------------------------- 1 | {"openapi" "3.0.0", 2 | "info" 3 | {"version" "1.0.0", 4 | "title" "Swagger Petstore", 5 | "license" {"name" "MIT"}}, 6 | "servers" ({"url" "http://petstore.swagger.io/v1"}), 7 | "paths" 8 | {"/pets" 9 | {"get" 10 | {"summary" "List all pets", 11 | "operationId" "listPets", 12 | "tags" ("pets"), 13 | "parameters" 14 | ({"name" "limit", 15 | "in" "query", 16 | "description" "How many items to return at one time (max 100)", 17 | "required" false, 18 | "schema" {"type" "integer", "format" "int32"}}), 19 | "responses" 20 | {"200" 21 | {"description" "A paged array of pets", 22 | "headers" 23 | {"x-next" 24 | {"description" "A link to the next page of responses", 25 | "schema" {"type" "string"}}}, 26 | "content" 27 | {"application/json" 28 | {"schema" {"$ref" "#/components/schemas/Pets"}}}}, 29 | "default" 30 | {"description" "unexpected error", 31 | "content" 32 | {"application/json" 33 | {"schema" {"$ref" "#/components/schemas/Error"}}}}}}, 34 | "post" 35 | {"summary" "Create a pet", 36 | "operationId" "createPets", 37 | "tags" ("pets"), 38 | "responses" 39 | {"201" {"description" "Null response"}, 40 | "default" 41 | {"description" "unexpected error", 42 | "content" 43 | {"application/json" 44 | {"schema" {"$ref" "#/components/schemas/Error"}}}}}}}, 45 | "/pets/{petId}" 46 | {"get" 47 | {"summary" "Info for a specific pet", 48 | "operationId" "showPetById", 49 | "tags" ("pets"), 50 | "parameters" 51 | ({"name" "petId", 52 | "in" "path", 53 | "required" true, 54 | "description" "The id of the pet to retrieve", 55 | "schema" {"type" "string"}}), 56 | "responses" 57 | {"200" 58 | {"description" "Expected response to a valid request", 59 | "content" 60 | {"application/json" 61 | {"schema" {"$ref" "#/components/schemas/Pet"}}}}, 62 | "default" 63 | {"description" "unexpected error", 64 | "content" 65 | {"application/json" 66 | {"schema" {"$ref" "#/components/schemas/Error"}}}}}}}}, 67 | "components" 68 | {"schemas" 69 | {"Pet" 70 | {"type" "object", 71 | "required" ("id" "name"), 72 | "properties" 73 | {"id" {"type" "integer", "format" "int64"}, 74 | "name" {"type" "string"}, 75 | "tag" {"type" "string"}}}, 76 | "Pets" 77 | {"type" "array", "items" {"$ref" "#/components/schemas/Pet"}}, 78 | "Error" 79 | {"type" "object", 80 | "required" ("code" "message"), 81 | "properties" 82 | {"code" {"type" "integer", "format" "int32"}, 83 | "message" {"type" "string"}}}}}} 84 | -------------------------------------------------------------------------------- /test/juxt/jinx/annotation_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.annotation-test 4 | (:require 5 | [juxt.jinx.alpha.validate :as v] 6 | [juxt.jinx.alpha.schema :refer [schema]] 7 | #?(:clj 8 | [clojure.test :refer [deftest is testing]] 9 | :cljs 10 | [cljs.test :refer-macros [deftest is testing run-tests]] 11 | [cljs.core :refer [ExceptionInfo]]))) 12 | 13 | (deftest simple-annotation-test 14 | (is 15 | (= 16 | {:instance "Malcolm" 17 | :annotations {"default" "Bob"} 18 | :type "string" 19 | :valid? true} 20 | (v/validate 21 | {"type" "string" 22 | "default" "Bob"} 23 | "Malcolm" ))) 24 | (is 25 | (= 26 | {:instance {"surname" "Sparks" 27 | "firstname" "Bob"} 28 | :annotations 29 | {"title" "person" 30 | "description" "A person, user or employee" 31 | :properties 32 | {"surname" {"title" "Surname" 33 | "description" "Family name"} 34 | "firstname" {"default" "Bob"}}} 35 | :type "object" 36 | :valid? true} 37 | 38 | (v/validate 39 | (schema 40 | {"type" "object" 41 | "title" "person" 42 | "description" "A person, user or employee" 43 | "properties" 44 | {"firstname" 45 | {"type" "string" 46 | "default" "Bob"} 47 | "surname" 48 | {"type" "string" 49 | "title" "Surname" 50 | "description" "Family name" 51 | "examples" ["Smith" "Johnson" "Jones" "Williams"] 52 | }} 53 | "required" ["firstname" "surname"]}) 54 | {"surname" "Sparks"} 55 | {:journal? false})))) 56 | 57 | 58 | #_(v/validate 59 | (schema 60 | {"type" "object" 61 | "required" ["firstname"] 62 | "properties" {"firstname" {"type" "string" "default" "Dominic"} 63 | "surname" 64 | {"anyOf" 65 | [{"type" "string" 66 | "default" "foo" 67 | "title" "Surname" 68 | } 69 | {"type" "number" 70 | "default" "foo" 71 | "title" "Family name" 72 | }]}}}) 73 | {"surname" "Sparks"} 74 | 75 | {:journal? false}) 76 | 77 | 78 | #_(v/validate 79 | (schema 80 | {"type" "object" 81 | "required" ["firstname"] 82 | "properties" {"firstname" {"type" "string" "default" "Dominic"} 83 | "surname" 84 | {"allOf" 85 | [{"type" "string" 86 | "default" "foo" 87 | "title" "Surname" 88 | } 89 | {"type" "string" 90 | "default" "food" 91 | "title" "Family name" 92 | }]}}}) 93 | {"surname" "Sparks"} 94 | {:journal? false}) 95 | -------------------------------------------------------------------------------- /todo.org: -------------------------------------------------------------------------------- 1 | * DONE Support regex patterns in clj->jsch 2 | * DONE Schema expansion 3 | react-jsonschema-form (and potentially other libraries) are not able 4 | to follow $refs and require pre-expanded schemas. Therefore, add a 5 | feature to allow expansion. 6 | * TODO Change order of validation in api-2 so we can use partials 7 | Also, make schema a record 8 | * TODO Coercions 9 | Support a map of maps (from, to) lookup 10 | * TODO Update documention in README.adoc 11 | * TODO Coercions 12 | as per https://ajv.js.org/coercion.html 13 | * DONE 6. Validation Keywords 14 | ** DONE [#A] 6.1 Validation Keywords for Any Instance Type 15 | *** DONE 6.1.1 type 16 | *** DONE 6.1.2 enum 17 | *** DONE 6.1.3 const 18 | ** DONE [#C] 6.2 Validation Keywords for Numeric Instances (number and integer) 19 | *** DONE 6.2.1 multipleOf 20 | *** DONE 6.2.2 maximum 21 | *** DONE 6.2.3 exclusiveMaximum 22 | *** DONE 6.2.4 minimum 23 | *** DONE 6.2.5 exclusiveMinimum 24 | ** DONE [#B] 6.3 Validation Keywords for Strings 25 | *** DONE 6.3.1 maxLength 26 | *** DONE 6.3.2 minLength 27 | *** DONE 6.3.3 pattern 28 | ** DONE [#A] 6.4 Validation Keywords for Arrays 29 | *** DONE 6.4.1 items 30 | *** DONE 6.4.2 additionalItems 31 | *** DONE 6.4.3 maxItems 32 | *** DONE 6.4.4 minItems 33 | *** DONE 6.4.5 uniqueItems 34 | *** DONE 6.4.6 contains 35 | ** DONE [#A] 6.5 Validation Keywords for Objects 36 | *** DONE 6.5.1 maxProperties 37 | *** DONE 6.5.2 minProperties 38 | *** DONE 6.5.3 required 39 | *** DONE 6.5.4 properties 40 | *** DONE 6.5.5 patternProperties 41 | *** DONE 6.5.6 additionalProperties 42 | *** DONE 6.5.7 dependenices 43 | *** DONE 6.5.8 propertyNames 44 | ** DONE [#C] 6.6 Keywords for Applying Subschemas Conditionally 45 | *** DONE 6.6.1 if 46 | *** DONE 6.6.2 then 47 | *** DONE 6.6.3 else 48 | ** DONE [#A] 6.7 Keywords for Applying Subschemas With Boolean Logic 49 | *** DONE 6.7.1 allOf 50 | *** DONE 6.7.2 anyOf 51 | *** DONE 6.7.3 oneOf 52 | *** DONE 6.7.4 not 53 | * DONE Fix refs tests errors/failures 54 | ** DONE Load schemas, validate them according to themselves, find internal $ids 55 | 56 | * DONE Download specs 57 | - [X] RFC 1035 58 | - [X] RFC 1123 59 | - [X] RFC 5321 60 | - [X] RFC 1034 61 | - [X] RFC 2673 62 | - [X] RFC 3986 63 | - [X] RFC 3987 64 | - [X] RFC 4291 65 | - [X] RFC 5322 66 | - [X] RFC 5890 67 | - [X] RFC 6531 68 | * DONE [#B] 7. Semantic Validation With "format" 69 | * TODO Fix remaining format tests (uri-template and idn-email), currently ignored 70 | * DONE Finish schema validation (if, then, else) 71 | * DONE Finish schema validation (allOf, anyOf, oneOf, not) 72 | * DONE Finish schema validation (format) 73 | * TODO [#A] Annotations 74 | ** TODO oneOf 75 | * TODO [#C] Recursion protection (use schema-path visited hash-set) 76 | * TODO [#C] 8. String-Encoding Non-JSON Data 77 | * TODO [#C] 9. Schema Re-Use With "definitions" 78 | * TODO [#C] 10. Schema Annotations 79 | * TODO Default value annotations factored into oneOf in dependencies 80 | * TODO Relative jsonpointer 81 | * TODO Improve jsonpointer "Failed to locate" error messages 82 | * TODO Improved error messages and locators 83 | * TODO Download Ajv to compare 84 | * TODO Compare with luposlip/json-schema/, particularly errors 85 | * DONE Download release zip of JSON-Schema-Test-Suite or try with tools.deps tech to download git repo 86 | * TODO [#C] Validation of schema regex value must conform to regex 87 | ** TODO Implement regex grammar to detect bad regex patterns https://www.ecma-international.org/ecma-262/9.0/index.html#sec-literals-regular-expression-literals 88 | *** TODO This needs to be implemented for both 89 | - [ ] schema validation (pattern) and 90 | - [ ] instance format validation. 91 | *** TODO Use CircleCI (or TravisCI) to automatically run tests 92 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = jinx 2 | 3 | jinx is a recursive acronym: jinx is not xml-schema 4 | 5 | jinx is json-schema! 6 | 7 | image:https://img.shields.io/clojars/v/jinx.svg["Clojars",link="https://clojars.org/jinx"] 8 | image:https://circleci.com/gh/juxt/jinx.svg?style=shield["CircleCI", link="https://circleci.com/gh/juxt/jinx"] 9 | 10 | == Introduction 11 | 12 | Almost all Clojure implementations of https://json-schema.org/[json 13 | schema validators] wrap Java libraries. This is generally a good idea. 14 | 15 | However, there are some reasons why a _native_ Clojure implementation 16 | can be useful: 17 | 18 | * Java libraries compile jsonschema to object graphs, making them 19 | inaccessible to many of the data functions in the Clojure core 20 | library. 21 | 22 | * On the front-end, it can be painful to have to convert Clojure data 23 | to JavaScript objects simply for the purposes of calling a 24 | jsonschema validation such as 25 | https://github.com/epoberezkin/ajv[Ajv]. 26 | 27 | * Extensibility: JSON Schema is designed to be extended with additional 28 | vocabularies. Clojure has some nice open-for-extension mechanisms. 29 | 30 | * Size: Implementing JSON Schema is not that scary in a language as 31 | nice as Clojure. There's not so much code to read, understand and 32 | possibly extend. 33 | 34 | == Scope 35 | 36 | This library implements JSON Schema 'draft7' 37 | (draft-handrews-json-schema-validation-01). 38 | 39 | == Status 40 | 41 | CAUTION: This is a new project, of alpha status. There may be future 42 | incompatible changes ahead. 43 | 44 | Most core features are working but there is more work yet to do: 45 | 46 | * Improved Error messages 47 | * Relative json-pointer 48 | * Patterns for uri-template and idn-email 49 | 50 | This library is tested with the official 51 | https://github.com/json-schema-org/JSON-Schema-Test-Suite[JSON-Schema-Test-Suite]. 52 | 53 | JSON Schema provides an official test suite, of which jinx passes all 54 | the non-optional tests, and all but two of the optional tests. 55 | 56 | == Usage 57 | 58 | === Require 59 | 60 | [source,clojure] 61 | ---- 62 | (require '[juxt.jinx-alpha-2 :as jinx]) 63 | ---- 64 | 65 | === Create a schema 66 | 67 | [source,clojure] 68 | ---- 69 | (jinx/schema {"type" "array" "items" {"type" "string"}}) 70 | ---- 71 | 72 | === Create a schema (short-hand) 73 | 74 | [source,clojure] 75 | ---- 76 | (jinx/clj->jsch ['string]) 77 | ---- 78 | 79 | === Validate a document 80 | 81 | [source,clojure] 82 | ---- 83 | (jinx/validate 84 | {} 85 | (jinx/schema {"type" "object"})) 86 | ---- 87 | 88 | == Schemas 89 | 90 | A schema is a Clojure map (or boolean) that should be augmented with 91 | metadata by calling `juxt.jinx.schema/schema` on the schema data: 92 | 93 | [source,clojure] 94 | ---- 95 | (juxt.jinx.schema/schema {"type" "object"}) 96 | ---- 97 | 98 | == Resolvers 99 | 100 | Validation can take an optional options map. 101 | 102 | The `:resolvers` entry should be a collection of resolvers. 103 | 104 | * `:juxt.jinx.resolve/built-in` is the built-in resolver which will resolve schemas contained in the library, such as the draft7 meta-schema. 105 | 106 | * `:juxt.jinx.resolve/default-resolver` is a resolver which takes an argument of a map of URIs (or regexes) to values. 107 | + 108 | A value can be a schema (which should be pre-processed with schema metadata by calling `juxt.jinx.schema/schema`). 109 | + 110 | A value may also be a function (called with the URI or, in the case of a regex, the result of the regex match): 111 | + 112 | [source,clojure] 113 | ---- 114 | {#"http://example.com/schemas/(.*)" (fn [match] {:type "object" 115 | :path (second match)})} 116 | ---- 117 | 118 | == Developing 119 | 120 | When you clone this repository, use the `--recursive` option to ensure 121 | that the official json schema repo is also cloned (as a submodule). 122 | 123 | ---- 124 | git clone --recursive https://github.com/juxt/jinx 125 | ---- 126 | 127 | == Alternative implementations 128 | 129 | * https://github.com/niquola/json-schema.clj 130 | -------------------------------------------------------------------------------- /test/juxt/jinx/resolve_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.resolve-test 4 | #?@(:clj 5 | [(:require 6 | [juxt.jinx.alpha.resolve :refer [resolve-uri expand-document]] 7 | [clojure.string :as str] 8 | [clojure.edn :as edn] 9 | [cheshire.core :as cheshire] 10 | [clojure.java.io :as io] 11 | [lambdaisland.uri :as uri] 12 | [clojure.test :refer [deftest is are testing]])] 13 | :cljs 14 | [(:require 15 | [juxt.jinx.alpha.resolve :refer [resolve-uri]] 16 | [clojure.string :as str] 17 | [cljs-node-io.file :refer [File]] 18 | [lambdaisland.uri :as uri] 19 | [cljs.test :refer-macros [deftest is are testing run-tests]]) 20 | (:import goog.Uri)])) 21 | 22 | (comment 23 | :resolvers [[:juxt.jinx.alpha.resolve/default-resolver {"http://example.com/foo" (io/resource "schemas/json-schema.org/draft-07/schema")}] 24 | :juxt.jinx.alpha.resolve/built-in]) 25 | 26 | (deftest built-in-resolver-test 27 | (is 28 | (resolve-uri :juxt.jinx.alpha.resolve/built-in "http://json-schema.org/draft-07/schema"))) 29 | 30 | 31 | (def example-map 32 | #?(:clj 33 | {"http://example.com/test" (io/resource "juxt/jinx/test.json") 34 | "http://example.com/literal-boolean-schema" false 35 | "http://example.com/literal-object-schema" {:type "string"} 36 | "http://example.com/literal-function-schema" 37 | (fn [_] {:type "string" 38 | :uri "http://example.com/literal-function-schema"}) 39 | #"http://example.com/static/(.*)" {:type "object"} 40 | #"http://example.com/schemas/(.*)" (fn [match] {:type "object" 41 | :path (second match)})} 42 | :cljs 43 | {"http://example.com/test" (File. "test/juxt/jinx/test.json") 44 | "http://example.com/literal-boolean-schema" false 45 | "http://example.com/literal-object-schema" {:type "string"} 46 | "http://example.com/literal-function-schema" 47 | (fn [_] {:type "string" 48 | :uri "http://example.com/literal-function-schema"}) 49 | #"http://example.com/static/(.*)" {:type "object"} 50 | #"http://example.com/schemas/(.*)" (fn [match] {:type "object" 51 | :path (second match)})})) 52 | 53 | 54 | (deftest default-resolver-test 55 | (let [m example-map] 56 | (testing "literal-to-resource" 57 | (is 58 | (= 59 | {"foo" "bar"} 60 | (resolve-uri 61 | [:juxt.jinx.alpha.resolve/default-resolver m] 62 | "http://example.com/test")))) 63 | 64 | (testing "literal-to-schema" 65 | (is 66 | (= 67 | false 68 | (resolve-uri 69 | [:juxt.jinx.alpha.resolve/default-resolver m] 70 | "http://example.com/literal-boolean-schema")))) 71 | 72 | (testing "literal-to-object" 73 | (is 74 | (= 75 | {:type "string"} 76 | (resolve-uri 77 | [:juxt.jinx.alpha.resolve/default-resolver m] 78 | "http://example.com/literal-object-schema")))) 79 | 80 | (testing "literal-to-function" 81 | (is 82 | (= 83 | {:type "string" 84 | :uri "http://example.com/literal-function-schema"} 85 | (resolve-uri 86 | [:juxt.jinx.alpha.resolve/default-resolver m] 87 | "http://example.com/literal-function-schema")))) 88 | 89 | (testing "regex-to-constant" 90 | (is 91 | (= 92 | {:type "object"} 93 | (resolve-uri 94 | [:juxt.jinx.alpha.resolve/default-resolver example-map] 95 | "http://example.com/static/schema.json")))) 96 | 97 | (testing "regex-to-function" 98 | (is 99 | (= 100 | {:type "object", :path "schema1.json"} 101 | (resolve-uri 102 | [:juxt.jinx.alpha.resolve/default-resolver example-map] 103 | "http://example.com/schemas/schema1.json")))))) 104 | 105 | 106 | #?(:clj 107 | (deftest document-expansion-test 108 | (let [expansion 109 | (expand-document 110 | (edn/read-string (slurp (io/resource "juxt/jinx/petstore.edn"))) 111 | {})] 112 | (is 113 | (= 114 | "string" 115 | (get-in expansion ["paths" "/pets" "get" "responses" 116 | "200" "content" "application/json" 117 | "schema" "items" "properties" "name" "type"])))))) 118 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha/resolve.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha.resolve 4 | #?@ 5 | (:clj 6 | [(:require 7 | [cheshire.core :as cheshire] 8 | [clojure.java.io :as io] 9 | [clojure.string :as str] 10 | [clojure.walk :refer [postwalk]] 11 | [juxt.jinx.alpha.jsonpointer :as jsonpointer] 12 | [lambdaisland.uri :as uri])] 13 | :cljs 14 | [(:require 15 | [cljs-node-io.core :as io :refer [slurp]] 16 | [cljs-node-io.file :refer [File]] 17 | [clojure.string :as str] 18 | [clojure.walk :refer [postwalk]] 19 | [juxt.jinx.alpha.jsonpointer :as jsonpointer] 20 | [lambdaisland.uri :as uri]) 21 | (:require-macros [juxt.jinx.alpha.resolve :refer [slurp-resource]]) 22 | (:import goog.Uri)])) 23 | 24 | #?(:clj 25 | (defmacro slurp-resource [resource] 26 | (clojure.core/slurp (io/resource resource)))) 27 | 28 | (defn read-json-string [json-str] 29 | #?(:clj (cheshire/parse-string json-str) 30 | :cljs (js->clj (js/JSON.parse json-str)))) 31 | 32 | (defn read-json-stream [json-str] 33 | #?(:clj (cheshire/parse-stream (io/reader json-str)) 34 | :cljs (js->clj (js/JSON.parse (slurp json-str))))) 35 | 36 | (defmulti resolve-uri 37 | (fn [k uri] 38 | (cond 39 | (keyword? k) k 40 | (coll? k) (first k)))) 41 | 42 | (def built-in-schemas 43 | {"http://json-schema.org/draft-07/schema" (slurp-resource "schemas/json-schema.org/draft-07/schema")}) 44 | 45 | (defmethod resolve-uri ::built-in [_ uri] 46 | (when-let [res (built-in-schemas uri)] 47 | (read-json-string res))) 48 | 49 | (defprotocol DefaultResolverDereferencer 50 | (deref-val [_ k] "Dereference")) 51 | 52 | (extend-protocol DefaultResolverDereferencer 53 | #?(:clj java.net.URL :cljs goog.Uri) 54 | (deref-val [res k] (read-json-stream res)) 55 | 56 | #?(:clj Boolean :cljs boolean) 57 | (deref-val [res k] res) 58 | 59 | #?(:clj clojure.lang.IPersistentMap :cljs cljs.core/PersistentArrayMap) 60 | (deref-val [res k] res) 61 | 62 | #?(:clj clojure.lang.Fn :cljs function) 63 | (deref-val [f k] (deref-val (f k) k)) 64 | 65 | #?(:clj java.io.File :cljs cljs-node-io.file/File) 66 | (deref-val [file k] 67 | #?(:clj (read-json-stream file) 68 | :cljs (js->clj (read-json-stream file))))) 69 | 70 | (defmethod resolve-uri ::default-resolver [[xx m] ^String uri] 71 | (when-let 72 | [[k val] 73 | (or 74 | ;; First strategy: lookup the url directly 75 | (find m uri) 76 | 77 | ;; Second, find a matching regex 78 | (some (fn [[pattern v]] 79 | (when #?(:clj (instance? java.util.regex.Pattern pattern) 80 | :cljs (regexp? pattern)) 81 | (when-let [match (re-matches pattern uri)] 82 | [match v]))) 83 | m))] 84 | 85 | (deref-val val k))) 86 | 87 | (defmethod resolve-uri ::function [[_ f] ^String uri] 88 | (f uri)) 89 | 90 | (defn- resolv [uri doc resolvers] 91 | "Return a vector of [schema new-doc & [new-base-uri]]." 92 | ;; TODO: Return a map rather than vector 93 | (let [[docref fragment] (str/split uri #"#")] 94 | 95 | (if (empty? docref) 96 | [(jsonpointer/json-pointer doc fragment) doc] 97 | 98 | (if-let [embedded-schema (-> doc meta :uri->schema (get docref))] 99 | [(jsonpointer/json-pointer embedded-schema fragment) 100 | doc 101 | docref] 102 | 103 | (if-let [doc (some 104 | (fn [resolver] (resolve-uri resolver docref)) 105 | resolvers)] 106 | [(jsonpointer/json-pointer doc fragment) 107 | doc 108 | docref] 109 | 110 | (throw (ex-info (str "Failed to resolve uri: " docref) {:uri docref}))))))) 111 | 112 | 113 | (defn resolve-ref [ref-object doc ctx] 114 | (assert ref-object) 115 | 116 | (let [;; "The value of the "$ref" property MUST be a URI Reference." 117 | ;; -- [CORE Section 8.3] 118 | base-uri (get (meta ref-object) :base-uri) 119 | ref #?(:clj (some-> (get ref-object "$ref") java.net.URLDecoder/decode) 120 | :cljs (some-> (get ref-object "$ref") js/decodeURIComponent)) 121 | uri (str (uri/join (or base-uri (:base-uri ctx)) ref))] 122 | 123 | (let [options 124 | (if false #_(contains? (:visited-memory ctx) uri) 125 | (throw (ex-info "Infinite cycle detected" {:uri uri})) 126 | (update ctx :visited-memory (fnil conj #{}) uri))] 127 | 128 | (let [[new-schema doc base-uri] (resolv uri doc (get-in ctx [:options :resolvers]))] 129 | [new-schema (cond-> ctx 130 | base-uri (assoc :base-uri base-uri) 131 | doc (assoc :doc doc))])))) 132 | 133 | 134 | (defn expand-document 135 | ([doc ctx] 136 | (expand-document doc doc ctx)) 137 | ([doc parent ctx] 138 | (postwalk 139 | (fn [m] 140 | (if (and (map? m) (contains? m "$ref")) 141 | (let [[new-doc new-ctx] (resolve-ref m parent ctx)] 142 | (expand-document new-doc parent new-ctx)) 143 | m)) 144 | doc))) 145 | -------------------------------------------------------------------------------- /resources/schemas/json-schema.org/draft-07/schema: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "http://json-schema.org/draft-07/schema#", 4 | "title": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "nonNegativeInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "nonNegativeIntegerDefault0": { 16 | "allOf": [ 17 | { "$ref": "#/definitions/nonNegativeInteger" }, 18 | { "default": 0 } 19 | ] 20 | }, 21 | "simpleTypes": { 22 | "enum": [ 23 | "array", 24 | "boolean", 25 | "integer", 26 | "null", 27 | "number", 28 | "object", 29 | "string" 30 | ] 31 | }, 32 | "stringArray": { 33 | "type": "array", 34 | "items": { "type": "string" }, 35 | "uniqueItems": true, 36 | "default": [] 37 | } 38 | }, 39 | "type": ["object", "boolean"], 40 | "properties": { 41 | "$id": { 42 | "type": "string", 43 | "format": "uri-reference" 44 | }, 45 | "$schema": { 46 | "type": "string", 47 | "format": "uri" 48 | }, 49 | "$ref": { 50 | "type": "string", 51 | "format": "uri-reference" 52 | }, 53 | "$comment": { 54 | "type": "string" 55 | }, 56 | "title": { 57 | "type": "string" 58 | }, 59 | "description": { 60 | "type": "string" 61 | }, 62 | "default": true, 63 | "readOnly": { 64 | "type": "boolean", 65 | "default": false 66 | }, 67 | "examples": { 68 | "type": "array", 69 | "items": true 70 | }, 71 | "multipleOf": { 72 | "type": "number", 73 | "exclusiveMinimum": 0 74 | }, 75 | "maximum": { 76 | "type": "number" 77 | }, 78 | "exclusiveMaximum": { 79 | "type": "number" 80 | }, 81 | "minimum": { 82 | "type": "number" 83 | }, 84 | "exclusiveMinimum": { 85 | "type": "number" 86 | }, 87 | "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, 88 | "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 89 | "pattern": { 90 | "type": "string", 91 | "format": "regex" 92 | }, 93 | "additionalItems": { "$ref": "#" }, 94 | "items": { 95 | "anyOf": [ 96 | { "$ref": "#" }, 97 | { "$ref": "#/definitions/schemaArray" } 98 | ], 99 | "default": true 100 | }, 101 | "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, 102 | "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 103 | "uniqueItems": { 104 | "type": "boolean", 105 | "default": false 106 | }, 107 | "contains": { "$ref": "#" }, 108 | "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, 109 | "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 110 | "required": { "$ref": "#/definitions/stringArray" }, 111 | "additionalProperties": { "$ref": "#" }, 112 | "definitions": { 113 | "type": "object", 114 | "additionalProperties": { "$ref": "#" }, 115 | "default": {} 116 | }, 117 | "properties": { 118 | "type": "object", 119 | "additionalProperties": { "$ref": "#" }, 120 | "default": {} 121 | }, 122 | "patternProperties": { 123 | "type": "object", 124 | "additionalProperties": { "$ref": "#" }, 125 | "propertyNames": { "format": "regex" }, 126 | "default": {} 127 | }, 128 | "dependencies": { 129 | "type": "object", 130 | "additionalProperties": { 131 | "anyOf": [ 132 | { "$ref": "#" }, 133 | { "$ref": "#/definitions/stringArray" } 134 | ] 135 | } 136 | }, 137 | "propertyNames": { "$ref": "#" }, 138 | "const": true, 139 | "enum": { 140 | "type": "array", 141 | "items": true, 142 | "minItems": 1, 143 | "uniqueItems": true 144 | }, 145 | "type": { 146 | "anyOf": [ 147 | { "$ref": "#/definitions/simpleTypes" }, 148 | { 149 | "type": "array", 150 | "items": { "$ref": "#/definitions/simpleTypes" }, 151 | "minItems": 1, 152 | "uniqueItems": true 153 | } 154 | ] 155 | }, 156 | "format": { "type": "string" }, 157 | "contentMediaType": { "type": "string" }, 158 | "contentEncoding": { "type": "string" }, 159 | "if": { "$ref": "#" }, 160 | "then": { "$ref": "#" }, 161 | "else": { "$ref": "#" }, 162 | "allOf": { "$ref": "#/definitions/schemaArray" }, 163 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 164 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 165 | "not": { "$ref": "#" } 166 | }, 167 | "default": true 168 | } 169 | -------------------------------------------------------------------------------- /test/juxt/jinx/official_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.official-test 4 | #?@(:clj [(:require [clojure.java.io :as io] 5 | [cheshire.core :as json] 6 | [clojure.test :refer :all] 7 | [clojure.test :as test] 8 | [juxt.jinx.alpha.validate :refer [validate]] 9 | [juxt.jinx.alpha.schema :refer [schema]] 10 | [juxt.jinx.alpha.resolve :as resolv] 11 | [juxt.jinx.alpha.schema :as schema])] 12 | :cljs [(:require [cljs-node-io.core :as io :refer [slurp]] 13 | [cljs-node-io.fs :as fs] 14 | [cljs-node-io.file :refer [File]] 15 | [cljs.test :refer-macros [deftest is testing run-tests]] 16 | [juxt.jinx.alpha.validate :refer [validate]] 17 | [juxt.jinx.alpha.schema :as schema :refer [schema]] 18 | [juxt.jinx.alpha.resolve :as resolv] 19 | [cljs.nodejs :as nodejs])])) 20 | 21 | (defn- env [s] 22 | #?(:clj (System/getenv (str s))) 23 | #?(:cljs (aget js/process.env s))) 24 | 25 | #?(:cljs 26 | (def Throwable js/Error)) 27 | 28 | (def TESTS-ROOT 29 | #?(:clj (io/file "official-test-suite") 30 | :cljs (str "official-test-suite"))) 31 | 32 | (def TESTS-DIR 33 | #?(:clj (io/file TESTS-ROOT "tests/draft7") 34 | :cljs (str TESTS-ROOT "/tests/draft7"))) 35 | 36 | 37 | #?(:cljs 38 | (do 39 | (def fs (cljs.nodejs/require "fs")) 40 | 41 | (defn file-exists? [f] 42 | (fs.existsSync f)) 43 | (defn dir? [f] 44 | (and 45 | (file-exists? f) 46 | (.. fs (lstatSync f) (isDirectory)))) 47 | (defn file? [f] 48 | (.. fs (lstatSync f) (isFile))) 49 | (defn file-seq [dir] 50 | (if (fs.existsSync dir) 51 | (tree-seq 52 | dir? 53 | (fn [d] (map (partial str d "/") (seq (fs.readdirSync d)))) 54 | dir) 55 | [])) 56 | (defn read-file-cljs [fname] 57 | (js->clj (js/JSON.parse (.readFileSync fs fname)))))) 58 | 59 | (defn test-jsonschema [{:keys [schema data valid] :as test}] 60 | (try 61 | (let [schema (schema/schema schema) 62 | result (validate 63 | schema data 64 | {:resolvers 65 | [::resolv/built-in 66 | [::resolv/default-resolver 67 | {#"http://localhost:1234/(.*)" 68 | (fn [match] 69 | #?(:clj (io/file (io/file TESTS-ROOT "remotes") (second match)) 70 | :cljs (File. (str TESTS-ROOT "/remotes/" (second match)))))}]]}) 71 | success? (if valid (:valid? result) 72 | (not (:valid? result)))] 73 | (cond-> test 74 | success? (assoc :result :success) 75 | (and (not success?) valid) (assoc :failures (:error result)) 76 | (and (empty? result) (not valid)) (assoc :failures [{:message "Incorrectly judged valid"}]))) 77 | (catch Throwable e (merge test {:result :error 78 | :error e})))) 79 | 80 | (defn success? [x] (= (:result x) :success)) 81 | 82 | ;; Test suite 83 | 84 | (defn tests 85 | [tests-dir] 86 | (for [testfile #?(:clj (file-seq TESTS-DIR) 87 | :cljs (filter file? (file-seq TESTS-DIR))) 88 | ;; filename (-> tests-dir .list sort) 89 | ;; Test filenames implemented so far or being worked on currently 90 | ;; :when ((or filename-pred some?) filename) 91 | ;; :let [testfile (io/file tests-dir filename)] 92 | 93 | :when #?(:clj (.isFile testfile) 94 | :cljs (file? testfile)) 95 | :let [objects #?(:clj (json/parse-stream (io/reader testfile)) 96 | :cljs (read-file-cljs testfile))] 97 | {:strs [schema tests description]} objects 98 | ;; Any required parsing of the schema, do it now for performance 99 | :let [test-group-description description] 100 | test tests 101 | :let [{:strs [description data valid]} test]] 102 | {:filename (str testfile) 103 | :test-group-description test-group-description 104 | :test-description description 105 | :schema schema 106 | :data data 107 | :valid valid})) 108 | 109 | ;; TODO: Pull out defaults and refs from validation keywords - this is 110 | ;; premature abstraction 111 | 112 | (defn exclude-test? [test] 113 | (contains? 114 | #{"format: uri-template" 115 | "validation of an internationalized e-mail addresses"} 116 | (:test-group-description test))) 117 | 118 | (defn cljs-exclude-test? [test] 119 | (or (contains? 120 | #{"format: uri-template" 121 | ;; Not sure why these tests are failing 122 | "validation of URI References" 123 | } 124 | (:test-group-description test)) 125 | (contains? 126 | #{"an invalid IRI based on IPv6"} 127 | (:test-description test)))) 128 | 129 | #?(:clj 130 | (do 131 | (defn make-tests [] 132 | (doseq [test (remove exclude-test? (tests TESTS-DIR))] 133 | (let [testname (symbol (str (gensym "test") "-test"))] 134 | (eval `(test/deftest ~(vary-meta testname assoc :official true) ~testname 135 | (test/testing ~(:test-description test) 136 | (test/is (success? (test-jsonschema ~test))))))))) 137 | (make-tests))) 138 | 139 | #?(:cljs 140 | (deftest cljs-tests 141 | (testing "Testing JSON-Schema-Test-Suite - cljs" 142 | (doseq [test (remove cljs-exclude-test? (tests TESTS-DIR))] 143 | (let [testname (symbol (str (gensym "test") "-test"))] 144 | (do 145 | (is (success? (test-jsonschema test))))))))) 146 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha/patterns.cljs: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha.patterns) 4 | 5 | (def addr-spec 6 | #"(?i)((?^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+)@(?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*))") 7 | 8 | (def iaddr-spec 9 | #"(?i)((?^[a-z0-9\x21\x23-\x27\x2A-\x2B\x2D\x2F\x3D\x3F\x5E-\x60\x7B-\x7E\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]+)@(?[a-z0-9\x21\x23-\x27\x2A-\x2B\x2D\x2F\x3D\x3F\x5E-\x60\x7B-\x7E\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef](?:[a-z0-9\x21\x23-\x27\x2A-\x2B\x2D\x2F\x3D\x3F\x5E-\x60\x7B-\x7E\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]{0,61}[a-z0-9\x21\x23-\x27\x2A-\x2B\x2D\x2F\x3D\x3F\x5E-\x60\x7B-\x7E\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef])?(?:\.[a-z0-9\x21\x23-\x27\x2A-\x2B\x2D\x2F\x3D\x3F\x5E-\x60\x7B-\x7E\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef](?:[a-z0-9\x21\x23-\x27\x2A-\x2B\x2D\x2F\x3D\x3F\x5E-\x60\x7B-\x7E\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]{0,61}[a-z0-9\x21\x23-\x27\x2A-\x2B\x2D\x2F\x3D\x3F\x5E-\x60\x7B-\x7E\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef])?)*))") 10 | 11 | (def subdomain 12 | #"(?i)^[a-z\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef](?:[a-z0-9-\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]{0,61}[a-z0-9\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef])?(?:\.[a-z0-9\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef](?:[a-z0-9-\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]{0,61}[a-z0-9\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef])?)*$") 13 | 14 | 15 | (def IPv4address 16 | #"^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$") 17 | 18 | (def IPv6address 19 | #"(?i)^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$") 20 | 21 | 22 | (def URI 23 | #"(?i)^(?[a-z][a-z0-9+\-.]*):(?://(?(?:(?(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*)@)?(?:(?\x5B(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\x5D|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*))?(?::(?(?:\d*)))?)?)?(?(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*))?(?:#(?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*))?$") 24 | 25 | 26 | (def relative-ref 27 | #"(?://(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-fA-F]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=@]|%[0-9a-f]{2})+(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)") 28 | 29 | 30 | 31 | (def IRI 32 | #"(?i)^(?[a-z][a-z0-9+\-.]*):(?://(?(?:(?(?:[a-z0-9\-._~!$&'()*+,;=:\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})*)@)?(?:(?\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})*))?(?::(?(?:\d*)))?)?)?(?:(?(?:/(?:[a-z0-9\x2D-\x2E\x5F\x7E\xA0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\x25[0-9a-f][0-9a-f]|[\x21\x24\x26-\x2C\x3B\x3D]|\x3A|\x40)*)*)|/(?:[a-z0-9\x2D-\x2E\x5F\x7E\xA0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\x25[0-9a-f][0-9a-f]|[\x21\x24[\x26-\x2C]\x3B\x3D]|\x3A|\x40)+(?:/(?:[a-z0-9\x2D-\x2E\x5F\x7E\xA0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\x25\d\d|[\x21\x24\x26-\x2C\x3B\x3D]|\x3A|\x40)*)*|(?:[a-z0-9\x2D-\x2E\x5F\x7E\xA0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\x25[0-9a-f][0-9a-f]|[\x21\x24\x26-\x2C\x3B\x3D]|\x3A|\x40)+(?:/(?:[a-z0-9\x2D-\x2E\x5F\x7E\xA0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|\x25[0-9a-f][0-9a-f]|[\x21\x24[\x26-\x2C]\x3B\x3D]|\x3A|\x40)*)?)(?:\?(?(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef/?]|%[0-9a-f]{2})*))?(?:#(?(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef/?]|%[0-9a-f]{2})*))?") 33 | 34 | (def irelative-ref 35 | #"(?i)(?://(?(?:(?(?:[a-z0-9\-._~!$&'()*+,;=:\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})*)@)?(?:(?\x5B(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]+)\x5D|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})*))?(?::(?(?:\d*)))?)?)?(?(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})*)*|/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})+(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})+(?:/(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef]|%[0-9a-f]{2})*)*)(?:\?(?(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef/?]|%[0-9a-f]{2})*))?(?:#(?(?:[a-z0-9\-._~!$&'()*+,;=:@\x2d-\x2e\x5f\x7e\xa0-\ud7ff\uf900-\ufdcf\ufdf0-\uffef/?]|%[0-9a-f]{2})*))?$") 36 | 37 | (def json-pointer 38 | #"(?i)^(?:/(?:[a-z0-9+\u0000-\u001f\u007f\x20-\x2E\x3A-\x40\x5B-\x60\x7B-\x7D\x80-\uFFFF]|~0|~1)*)*$") 39 | 40 | (def relative-json-pointer 41 | #"(?:0|[1-9][0-9]*)(?:#|(?:/(?:([^/~])|(~[01]))*)*)$") 42 | 43 | (def iso-date-time 44 | #"(?i)^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d:\d\d)$") 45 | 46 | (def iso-local-date 47 | #"^(\d\d\d\d)-(\d\d)-(\d\d)$") 48 | 49 | (def iso-time 50 | #"(?i)^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d:\d\d)?$") 51 | 52 | (defn parse 53 | "Parse a test string" 54 | [te instance] 55 | (case te 56 | "addr-spec-test" 57 | (if-let [[_ _ w n] (re-matches addr-spec instance)] [w n] "Not found") 58 | "iri-test" 59 | ;["http://user:password@example.com:8080/path/ererwer?query=value#fragment" "http" "user:password@example.com:8080" "user:password" "example.com" "8080" "/path/ererwer" "query=value" "fragment"] 60 | (if-let [[_ scheme _ user host port path query fragment] (re-matches IRI instance)] [scheme user host port path query fragment] "Not found"))) 61 | -------------------------------------------------------------------------------- /test/juxt/jinx/validate_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.validate-test 4 | #?@(:clj [(:require 5 | [juxt.jinx.alpha.validate :as validate] 6 | [clojure.test :refer [deftest is are testing]])] 7 | :cljs [(:require 8 | [juxt.jinx.alpha.validate :as validate] 9 | [cljs.test :refer-macros [deftest is are testing run-tests]])])) 10 | 11 | (defn run-validate [schema instance] 12 | (let [result (validate/validate schema instance)] 13 | [(:valid? result) (:instance result)])) 14 | 15 | (deftest boolean-schema-test [] 16 | (testing "true schema always true" 17 | (is (= [true {"foo" "bar"}] 18 | (run-validate true {"foo" "bar"})))) 19 | 20 | (testing "false schema always false" 21 | (is (= [false {"foo" "bar"}] 22 | (run-validate false {"foo" "bar"}))))) 23 | 24 | (deftest boolean-test [] 25 | (is (= [true false] 26 | (run-validate 27 | {"type" "boolean"} 28 | false))) 29 | 30 | (is (= [true true] 31 | (run-validate 32 | {"type" "boolean"} 33 | true))) 34 | 35 | (testing "object is not boolean" 36 | (is (= [false {}] 37 | (run-validate 38 | {"type" "boolean"} 39 | {}))))) 40 | 41 | (deftest number-test [] 42 | (is (= [true 10] 43 | (run-validate 44 | {"type" "number"} 45 | 10))) 46 | 47 | (is (= [true 21] 48 | (run-validate 49 | {"type" "number" 50 | "multipleOf" 7} 51 | 21))) 52 | 53 | (is (= [false 20] 54 | (run-validate 55 | {"type" "number" 56 | "multipleOf" 7} 57 | 20))) 58 | 59 | (is (= [true 100] 60 | (run-validate 61 | {"type" "number" 62 | "maximum" 100} 63 | 100))) 64 | 65 | (is (= [false 100] 66 | (run-validate 67 | {"type" "number" 68 | "exclusiveMaximum" 100} 69 | 100))) 70 | 71 | (is (= [true 10] 72 | (run-validate 73 | {"type" "number" 74 | "minimum" 10} 75 | 10))) 76 | 77 | (is (= [false 10] 78 | (run-validate 79 | {"type" "number" 80 | "exclusiveMinimum" 10} 81 | 10)))) 82 | 83 | (deftest string-test [] 84 | ;; A string is a valid string. 85 | (is (= [true "a string"] 86 | (run-validate 87 | {"type" "string"} 88 | "a string"))) 89 | 90 | ;; A number is not a valid string. 91 | (is (= [false 123] 92 | (run-validate 93 | {"type" "string"} 94 | 123))) 95 | 96 | ;; Nil is not a valid string. 97 | (is (= [false nil] 98 | (run-validate 99 | {"type" "string"} 100 | nil))) 101 | 102 | ;; Even if there is a default string, nil isn't valid. 103 | (is (= [false nil] 104 | (run-validate 105 | {"type" "string" 106 | "default" "default-string"} 107 | nil))) 108 | 109 | ;; Prefer the existing instance to the default. 110 | (is (= [true "a string"] 111 | (run-validate 112 | {"type" "string" 113 | "default" "default-string"} 114 | "a string"))) 115 | 116 | ;; Prefer the existing instance to the default, even if invalid. 117 | ;; (reinstate) 118 | (is (= [false 123] 119 | (run-validate 120 | {"type" "string" 121 | "default" "default-string"} 122 | 123))) 123 | 124 | ;; A nil value is replaced with the default value, even if the 125 | ;; result isn't itself valid. 126 | ;; (reinstate) 127 | #_(is (= [false 123] 128 | (run-validate 129 | {"type" "string" 130 | "default" 123} 131 | nil 132 | ))) 133 | 134 | ;; If string is within the max length, validation succeeds. 135 | (is (= [true "fo"] 136 | (run-validate 137 | {"type" "string" 138 | "maxLength" 3} 139 | "fo"))) 140 | 141 | ;; If string is the same as the max length, validation succeeds. 142 | (is (= [true "foo"] 143 | (run-validate 144 | {"type" "string" 145 | "maxLength" 3} 146 | "foo"))) 147 | 148 | ;; If string is over the max length, validation fails. 149 | (is (= [false "foo"] 150 | (run-validate 151 | {"type" "string" 152 | "maxLength" 2} 153 | "foo"))) 154 | 155 | ;; If string is over the min length, validation succeeds. 156 | (is (= [true "food"] 157 | (run-validate 158 | {"type" "string" 159 | "minLength" 3} 160 | "food"))) 161 | 162 | ;; If string is the same as the min length, validation succeeds. 163 | (is (= [true "foo"] 164 | (run-validate 165 | {"type" "string" 166 | "minLength" 3} 167 | "foo"))) 168 | 169 | ;; If string is under the min length, validation fails. 170 | (is (= [false "fo"] 171 | (run-validate 172 | {"type" "string" 173 | "minLength" 3} 174 | "fo")))) 175 | 176 | (deftest enum-test 177 | (is (= [true "b"] 178 | (run-validate 179 | {"enum" ["a" "b" "c"]} 180 | "b"))) 181 | ;; (reinstate) 182 | #_(is (= [true "b"] 183 | (run-validate nil {"enum" ["a" "b" "c"] 184 | "default" "b"})))) 185 | 186 | (deftest arrays-test 187 | (is (= [true []] 188 | (run-validate 189 | {"type" "array"} 190 | []))) 191 | 192 | (is (= [true [true true false true]] 193 | (run-validate 194 | {"type" "array" 195 | "items" {"type" "boolean"}} 196 | [true true false true] ))) 197 | 198 | (is (= [true [1 2 3]] 199 | (run-validate 200 | {"type" "array" 201 | "items" {"type" "number"}} 202 | [1 2 3] ))) 203 | 204 | 205 | (is (= [false [1 2 "foo"]] 206 | (run-validate 207 | {"type" "array" 208 | "items" {"type" "number"}} 209 | [1 2 "foo"] ))) 210 | 211 | (is (= [true [1 2 "foo"]] 212 | (run-validate 213 | {"type" "array" 214 | "items" [{"type" "number"} 215 | {"type" "number"} 216 | {"type" "string"}]} 217 | [1 2 "foo"] ))) 218 | 219 | (is (= [true [1 2 "foo" 10]] 220 | (run-validate 221 | {"type" "array" 222 | "items" [{"type" "number"} 223 | {"type" "number"} 224 | {"type" "string"}]} 225 | [1 2 "foo" 10])))) 226 | 227 | (deftest additional-items-test 228 | (is (= [true [1 2 "foo" 10]] 229 | (run-validate 230 | {"type" "array" 231 | "items" [{"type" "number"} 232 | {"type" "number"} 233 | {"type" "string"}] 234 | "additionalItems" true} 235 | [1 2 "foo" 10] ))) 236 | 237 | (is (= [false [1 2 "foo" 10]] 238 | (run-validate 239 | {"type" "array" 240 | "items" [{"type" "number"} 241 | {"type" "number"} 242 | {"type" "string"}] 243 | "additionalItems" false} 244 | [1 2 "foo" 10])))) 245 | 246 | (deftest min-items-test 247 | (is (= [true [true 10 20 20]] 248 | (run-validate 249 | {"type" "array" 250 | "items" [{"type" "boolean"} 251 | {"type" "number"} 252 | {"type" "number"} 253 | {"type" "number"}] 254 | "additionalItems" {"type" "string"} 255 | "uniqueItems" false} 256 | [true 10 20 20]))) 257 | 258 | (is (= [false [true 10 20 20]] 259 | (run-validate 260 | {"type" "array" 261 | "items" [{"type" "boolean"} 262 | {"type" "number"} 263 | {"type" "number"} 264 | {"type" "number"}] 265 | "additionalItems" {"type" "string"} 266 | "uniqueItems" true} 267 | [true 10 20 20])))) 268 | 269 | (deftest object-test 270 | (is (= [true {}] 271 | (run-validate 272 | {"type" "object"} 273 | {}))) 274 | 275 | (is (= [true {"foo" "bar"}] 276 | (run-validate {"type" "object"} {"foo" "bar"})))) 277 | 278 | (deftest properties-test 279 | (is (= [false {"foo" "bar"}] 280 | (run-validate 281 | {"type" "object" 282 | "properties" {"foo" {"type" "number"}}} 283 | {"foo" "bar"}))) 284 | 285 | (is (= [true {"foo" {"bar" 10}}] 286 | (run-validate 287 | {"type" "object" 288 | "properties" {"foo" {"type" "object" 289 | "properties" {"bar" {"type" "number"}}}}} 290 | {"foo" {"bar" 10}}))) 291 | 292 | (is (= [false {"foo" {"bar" 10}}] 293 | (run-validate 294 | {"type" "object" 295 | "properties" {"foo" {"type" "object" 296 | "properties" {"bar" {"type" "string"}}}}} 297 | {"foo" {"bar" 10}})))) 298 | 299 | (deftest properties-test1 300 | (is (= [true {"foo" "bar"}] 301 | (run-validate 302 | {"type" "object" 303 | "required" ["foo"] 304 | "properties" {"foo" {"type" "string" 305 | "default" "bar"}}} 306 | {})))) 307 | 308 | ;; Do not imply default values for objects and arrays 309 | ;; (Possibly re-instate) 310 | #_(deftest recover-from-type-failure-test 311 | (is (= [false nil] 312 | (run-validate {"type" "object"} nil))) 313 | (is (= [false nil] 314 | (run-validate {"type" "array"} nil)))) 315 | 316 | 317 | #_(validate 318 | {"type" "object" 319 | "required" ["foo"] 320 | "properties" {"foo" {"type" "object" 321 | "required" ["bar"] 322 | "properties" {"bar" {"default" "zip"}} 323 | "default" {"abc" 123}}}} 324 | {} 325 | ) 326 | 327 | (deftest recover-from-required-failure-test 328 | ;; Possibly re-instate 329 | #_(testing "Recover with child default" 330 | (is 331 | (= [true {"foo" {"abc" 123, "bar" "zip"}}] 332 | (run-validate 333 | {"type" "object" 334 | "required" ["foo"] 335 | "properties" {"foo" {"type" "object" 336 | "required" ["bar"] 337 | "properties" {"bar" {"default" "zip"}} 338 | "default" {"abc" 123}}}} 339 | {})))) 340 | 341 | (testing "Do not recover from nil parent, as default values are not implied" 342 | (is 343 | (= [false nil] 344 | (run-validate 345 | {"type" "object" 346 | "required" ["foo"] 347 | "properties" {"foo" {"type" "object" 348 | "required" ["bar"] 349 | "properties" {"bar" {"default" "zip"}} 350 | "default" {"abc" 123}}}} 351 | nil)))) 352 | 353 | (testing "Don't recover, no implied default for an object" 354 | ;; I didn't think it would be a good idea to imply default values 355 | ;; for objects/arrays, etc. However, that causes oneOf and anyOf 356 | ;; branches to start passing when they should definitely not be 357 | ;; (principle of least surprise). So instead, let's test that we 358 | ;; don't start implying defaults. 359 | (is 360 | (= [false {}] 361 | (run-validate 362 | {"type" "object" 363 | "required" ["foo"] 364 | "properties" {"foo" {"type" "object" 365 | "required" ["bar"] 366 | "properties" {"bar" {"default" "zip"}}}}} 367 | {})))) 368 | 369 | ;; Might be able to leave with this failing 370 | (testing "No recovery, as no implied default child with nil parent" 371 | (is 372 | (= [false nil] 373 | (run-validate 374 | {"type" "object" 375 | "required" ["foo"] 376 | "properties" {"foo" {"type" "object" 377 | "required" ["bar"] 378 | "properties" {"bar" {"default" "zip"}}}}} 379 | nil))))) 380 | 381 | (deftest dependencies_test 382 | (testing "Recover with child default" 383 | (is 384 | (= [true {"foo" 1 "bar" 2}] 385 | (run-validate 386 | {"dependencies" 387 | {"bar" 388 | {"properties" {"foo" {"type" "integer"} 389 | "bar" {"type" "integer"}}}}} 390 | {"foo" 1 "bar" 2}))) 391 | 392 | ;; No recovery, possibly re-instate 393 | #_(is 394 | (= 395 | [true {"bar" 1 "foo" 42}] 396 | (run-validate 397 | {"dependencies" 398 | {"bar" 399 | {"required" ["foo"] 400 | "properties" {"foo" {"type" "integer" 401 | "default" 42} 402 | "bar" {"type" "integer"}}}}} 403 | {"bar" 1}))) 404 | 405 | ;; No recovery, possibly re-instate 406 | #_(is 407 | (= 408 | [true {"bar" 2 "foo" 24}] 409 | (run-validate 410 | {"dependencies" 411 | {"bar" 412 | {"oneOf" [{"required" ["foo"] 413 | "properties" {"foo" {"type" "integer" 414 | "default" 42} 415 | "bar" {"type" "integer" 416 | "const" 1}}} 417 | {"required" ["foo"] 418 | "properties" {"foo" {"type" "integer" 419 | "default" 24} 420 | "bar" {"type" "integer" 421 | "const" 2}}}]}}} 422 | {"bar" 2}))))) 423 | -------------------------------------------------------------------------------- /spec/draft-handrews-relative-json-pointer-01: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Internet Engineering Task Force G. Luff 6 | Internet-Draft 7 | Intended status: Informational H. Andrews, Ed. 8 | Expires: July 23, 2018 Cloudflare, Inc. 9 | January 19, 2018 10 | 11 | 12 | Relative JSON Pointers 13 | draft-handrews-relative-json-pointer-01 14 | 15 | Abstract 16 | 17 | JSON Pointer is a syntax for specifying locations in a JSON document, 18 | starting from the document root. This document defines an extension 19 | to the JSON Pointer syntax, allowing relative locations from within 20 | the document. 21 | 22 | Status of This Memo 23 | 24 | This Internet-Draft is submitted in full conformance with the 25 | provisions of BCP 78 and BCP 79. 26 | 27 | Internet-Drafts are working documents of the Internet Engineering 28 | Task Force (IETF). Note that other groups may also distribute 29 | working documents as Internet-Drafts. The list of current Internet- 30 | Drafts is at https://datatracker.ietf.org/drafts/current/. 31 | 32 | Internet-Drafts are draft documents valid for a maximum of six months 33 | and may be updated, replaced, or obsoleted by other documents at any 34 | time. It is inappropriate to use Internet-Drafts as reference 35 | material or to cite them other than as "work in progress." 36 | 37 | This Internet-Draft will expire on July 23, 2018. 38 | 39 | Copyright Notice 40 | 41 | Copyright (c) 2018 IETF Trust and the persons identified as the 42 | document authors. All rights reserved. 43 | 44 | This document is subject to BCP 78 and the IETF Trust's Legal 45 | Provisions Relating to IETF Documents 46 | (https://trustee.ietf.org/license-info) in effect on the date of 47 | publication of this document. Please review these documents 48 | carefully, as they describe your rights and restrictions with respect 49 | to this document. Code Components extracted from this document must 50 | include Simplified BSD License text as described in Section 4.e of 51 | the Trust Legal Provisions and are provided without warranty as 52 | described in the Simplified BSD License. 53 | 54 | 55 | 56 | Luff & Andrews Expires July 23, 2018 [Page 1] 57 | 58 | Internet-Draft Relative JSON Pointers January 2018 59 | 60 | 61 | Table of Contents 62 | 63 | 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2 64 | 2. Conventions and Terminology . . . . . . . . . . . . . . . . . 2 65 | 3. Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 66 | 4. Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . 3 67 | 5. JSON String Representation . . . . . . . . . . . . . . . . . 4 68 | 5.1. Examples . . . . . . . . . . . . . . . . . . . . . . . . 4 69 | 6. Non-use in URI Fragment Identifiers . . . . . . . . . . . . . 5 70 | 7. Error Handling . . . . . . . . . . . . . . . . . . . . . . . 5 71 | 8. Relationship to JSON Pointer . . . . . . . . . . . . . . . . 5 72 | 9. Acknowledgements . . . . . . . . . . . . . . . . . . . . . . 5 73 | 10. Security Considerations . . . . . . . . . . . . . . . . . . . 5 74 | 11. References . . . . . . . . . . . . . . . . . . . . . . . . . 6 75 | 11.1. Normative References . . . . . . . . . . . . . . . . . . 6 76 | 11.2. Informative References . . . . . . . . . . . . . . . . . 6 77 | Appendix A. ChangeLog . . . . . . . . . . . . . . . . . . . . . 7 78 | Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 7 79 | 80 | 1. Introduction 81 | 82 | JSON Pointer (RFC 6901 [RFC6901]) is a syntax for specifying 83 | locations in a JSON document, starting from the document root. This 84 | document defines a related syntax allowing identification of relative 85 | locations from within the document. 86 | 87 | 2. Conventions and Terminology 88 | 89 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 90 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this 91 | document are to be interpreted as described in RFC 2119 [RFC2119]. 92 | 93 | 3. Syntax 94 | 95 | A Relative JSON Pointer is a Unicode string (see RFC 4627, Section 3 96 | [RFC4627]), comprising a non-negative integer, followed by either a 97 | '#' (%x23) character or a JSON Pointer (RFC 6901 [RFC6901]). 98 | 99 | The separation between the integer prefix and the JSON Pointer will 100 | always be unambiguous, because a JSON Pointer must be either zero- 101 | length or start with a '/' (%x2F). Similarly, a JSON Pointer will 102 | never be ambiguous with the '#'. 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | Luff & Andrews Expires July 23, 2018 [Page 2] 113 | 114 | Internet-Draft Relative JSON Pointers January 2018 115 | 116 | 117 | The ABNF syntax of a Relative JSON Pointer is: 118 | 119 | 120 | relative-json-pointer = non-negative-integer <json-pointer> 121 | relative-json-pointer =/ non-negative-integer "#" 122 | non-negative-integer = %x30 / %x31-39 *( %x30-39 ) 123 | ; "0", or digits without a leading "0" 124 | 125 | 126 | where follows the production defined in RFC 6901, 127 | Section 3 [RFC6901] ("Syntax"). 128 | 129 | 4. Evaluation 130 | 131 | Evaluation of a Relative JSON Pointer begins with a reference to a 132 | value within a JSON document, and completes with either a value 133 | within that document, a string corresponding to an object member, or 134 | integer value representing an array index. 135 | 136 | Evaluation begins by processing the non-negative-integer prefix. 137 | This can be found by taking the longest continuous sequence of 138 | decimal digits available, starting from the beginning of the string, 139 | taking the decimal numerical value. If this value is more than zero, 140 | then the following steps are repeated that number of times: 141 | 142 | If the current referenced value is the root of the document, then 143 | evaluation fails (see below). 144 | 145 | If the referenced value is an item within an array, then the new 146 | referenced value is that array. 147 | 148 | If the referenced value is an object member within an object, then 149 | the new referenced value is that object. 150 | 151 | If the remainder of the Relative JSON Pointer is a JSON Pointer, then 152 | evaluation proceeds as per RFC 6901, Section 4 [RFC6901] 153 | ("Evaluation"), with the modification that the initial reference 154 | being used is the reference currently being held (which may not be 155 | root of the document). 156 | 157 | Otherwise (when the remainder of the Relative JSON Pointer is the 158 | character '#'), the final result is determined as follows: 159 | 160 | If the current referenced value is the root of the document, then 161 | evaluation fails (see below). 162 | 163 | If the referenced value is an item within an array, then the final 164 | evaluation result is the value's index position within the array. 165 | 166 | 167 | 168 | Luff & Andrews Expires July 23, 2018 [Page 3] 169 | 170 | Internet-Draft Relative JSON Pointers January 2018 171 | 172 | 173 | If the referenced value is an object member within an object, then 174 | the new referenced value is the corresponding member name. 175 | 176 | 5. JSON String Representation 177 | 178 | The concerns surrounding JSON String representation of a Relative 179 | JSON Pointer are identical to those laid out in RFC 6901, Section 5 180 | [RFC6901]. 181 | 182 | 5.1. Examples 183 | 184 | For example, given the JSON document: 185 | 186 | 187 | { 188 | "foo": ["bar", "baz"], 189 | "highly": { 190 | "nested": { 191 | "objects": true 192 | } 193 | } 194 | } 195 | 196 | 197 | Starting from the value "baz" (inside "foo"), the following JSON 198 | strings evaluate to the accompanying values: 199 | 200 | 201 | "0" "baz" 202 | "1/0" "bar" 203 | "2/highly/nested/objects" true 204 | "0#" 1 205 | "1#" "foo" 206 | 207 | 208 | Starting from the value {"objects":true} (corresponding to the member 209 | key "nested"), the following JSON strings evaluate to the 210 | accompanying values: 211 | 212 | 213 | "0/objects" true 214 | "1/nested/objects" true 215 | "2/foo/0" "bar" 216 | "0#" "nested" 217 | "1#" "highly" 218 | 219 | 220 | 221 | 222 | 223 | 224 | Luff & Andrews Expires July 23, 2018 [Page 4] 225 | 226 | Internet-Draft Relative JSON Pointers January 2018 227 | 228 | 229 | 6. Non-use in URI Fragment Identifiers 230 | 231 | Unlike a JSON Pointer, a Relative JSON Pointer can not be used in a 232 | URI fragment identifier. Such fragments specify exact positions 233 | within a document, and therefore Relative JSON Pointers are not 234 | suitable. 235 | 236 | 7. Error Handling 237 | 238 | In the event of an error condition, evaluation of the JSON Pointer 239 | fails to complete. 240 | 241 | Evaluation may fail due to invalid syntax, or referencing a non- 242 | existent value. This specification does not define how errors are 243 | handled. An application of JSON Relative Pointer SHOULD specify the 244 | impact and handling of each type of error. 245 | 246 | 8. Relationship to JSON Pointer 247 | 248 | Relative JSON Pointers are intended as a companion to JSON Pointers. 249 | Applications MUST specify the use of each syntax separately. 250 | Defining either JSON Pointer or Relative JSON Pointer as an 251 | acceptable syntax does not imply that the other syntax is also 252 | acceptable. 253 | 254 | 9. Acknowledgements 255 | 256 | The language and structure of this specification are based heavily on 257 | [RFC6901], sometimes quoting it outright. 258 | 259 | This draft remains primarily as written and published by Geraint 260 | Luff, with only minor subsequent alterations under new editorship. 261 | 262 | 10. Security Considerations 263 | 264 | Evaluation of a given Relative JSON Pointer is not guaranteed to 265 | reference an actual JSON value. Applications using Relative JSON 266 | Pointer should anticipate this situation by defining how a pointer 267 | that does not resolve ought to be handled. 268 | 269 | As part of processing, a composite data structure may be assembled 270 | from multiple JSON documents (in part or in full). In such cases, 271 | applications SHOULD ensure that a Relative JSON Pointer does not 272 | evaluate to a value outside the document for which is was written. 273 | 274 | Note that JSON pointers can contain the NUL (Unicode U+0000) 275 | character. Care is needed not to misinterpret this character in 276 | programming languages that use NUL to mark the end of a string. 277 | 278 | 279 | 280 | Luff & Andrews Expires July 23, 2018 [Page 5] 281 | 282 | Internet-Draft Relative JSON Pointers January 2018 283 | 284 | 285 | 11. References 286 | 287 | 11.1. Normative References 288 | 289 | [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate 290 | Requirement Levels", BCP 14, RFC 2119, 291 | DOI 10.17487/RFC2119, March 1997, 292 | . 293 | 294 | [RFC6901] Bryan, P., Ed., Zyp, K., and M. Nottingham, Ed., 295 | "JavaScript Object Notation (JSON) Pointer", RFC 6901, 296 | DOI 10.17487/RFC6901, April 2013, 297 | . 298 | 299 | 11.2. Informative References 300 | 301 | [RFC4627] Crockford, D., "The application/json Media Type for 302 | JavaScript Object Notation (JSON)", RFC 4627, 303 | DOI 10.17487/RFC4627, July 2006, 304 | . 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | Luff & Andrews Expires July 23, 2018 [Page 6] 337 | 338 | Internet-Draft Relative JSON Pointers January 2018 339 | 340 | 341 | Appendix A. ChangeLog 342 | 343 | [[CREF1: This section to be removed before leaving Internet-Draft 344 | status.]] 345 | 346 | draft-handrews-relative-json-pointer-01 347 | 348 | * The initial number is "non-negative", not "positive" 349 | 350 | draft-handrews-relative-json-pointer-00 351 | 352 | * Revived draft with identical wording and structure. 353 | 354 | * Clarified how to use alongside JSON Pointer. 355 | 356 | draft-luff-relative-json-pointer-00 357 | 358 | * Initial draft. 359 | 360 | Authors' Addresses 361 | 362 | Geraint Luff 363 | Cambridge 364 | UK 365 | 366 | EMail: luffgd@gmail.com 367 | 368 | 369 | Henry Andrews (editor) 370 | Cloudflare, Inc. 371 | San Francisco, CA 372 | USA 373 | 374 | EMail: henry@cloudflare.com 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | Luff & Andrews Expires July 23, 2018 [Page 7] 393 | -------------------------------------------------------------------------------- /spec/rfc2673: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group M. Crawford 8 | Request for Comments: 2673 Fermilab 9 | Category: Standards Track August 1999 10 | 11 | 12 | Binary Labels in the Domain Name System 13 | 14 | Status of this Memo 15 | 16 | This document specifies an Internet standards track protocol for the 17 | Internet community, and requests discussion and suggestions for 18 | improvements. Please refer to the current edition of the "Internet 19 | Official Protocol Standards" (STD 1) for the standardization state 20 | and status of this protocol. Distribution of this memo is unlimited. 21 | 22 | Copyright Notice 23 | 24 | Copyright (C) The Internet Society (1999). All Rights Reserved. 25 | 26 | 1. Introduction and Terminology 27 | 28 | This document defines a "Bit-String Label" which may appear within 29 | domain names. This new label type compactly represents a sequence of 30 | "One-Bit Labels" and enables resource records to be stored at any 31 | bit-boundary in a binary-named section of the domain name tree. 32 | 33 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 34 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this 35 | document are to be interpreted as described in [KWORD]. 36 | 37 | 2. Motivation 38 | 39 | Binary labels are intended to efficiently solve the problem of 40 | storing data and delegating authority on arbitrary boundaries when 41 | the structure of underlying name space is most naturally represented 42 | in binary. 43 | 44 | 3. Label Format 45 | 46 | Up to 256 One-Bit Labels can be grouped into a single Bit-String 47 | Label. Within a Bit-String Label the most significant or "highest 48 | level" bit appears first. This is unlike the ordering of DNS labels 49 | themselves, which has the least significant or "lowest level" label 50 | first. Nonetheless, this ordering seems to be the most natural and 51 | efficient for representing binary labels. 52 | 53 | 54 | 55 | 56 | 57 | 58 | Crawford Standards Track [Page 1] 59 | 60 | RFC 2673 Binary Labels in the Domain Name System August 1999 61 | 62 | 63 | Among consecutive Bit-String Labels, the bits in the first-appearing 64 | label are less significant or "at a lower level" than the bits in 65 | subsequent Bit-String Labels, just as ASCII labels are ordered. 66 | 67 | 3.1. Encoding 68 | 69 | 0 1 2 70 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 . . . 71 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-//+-+-+-+-+-+-+ 72 | |0 1| ELT | Count | Label ... | 73 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+//-+-+-+-+-+-+-+ 74 | 75 | (Each tic mark represents one bit.) 76 | 77 | 78 | ELT 000001 binary, the six-bit extended label type [EDNS0] 79 | assigned to the Bit-String Label. 80 | 81 | Count The number of significant bits in the Label field. A Count 82 | value of zero indicates that 256 bits are significant. 83 | (Thus the null label representing the DNS root cannot be 84 | represented as a Bit String Label.) 85 | 86 | Label The bit string representing a sequence of One-Bit Labels, 87 | with the most significant bit first. That is, the One-Bit 88 | Label in position 17 in the diagram above represents a 89 | subdomain of the domain represented by the One-Bit Label in 90 | position 16, and so on. 91 | 92 | The Label field is padded on the right with zero to seven 93 | pad bits to make the entire field occupy an integral number 94 | of octets. These pad bits MUST be zero on transmission and 95 | ignored on reception. 96 | 97 | A sequence of bits may be split into two or more Bit-String Labels, 98 | but the division points have no significance and need not be 99 | preserved. An excessively clever server implementation might split 100 | Bit-String Labels so as to maximize the effectiveness of message 101 | compression [DNSIS]. A simpler server might divide Bit-String Labels 102 | at zone boundaries, if any zone boundaries happen to fall between 103 | One-Bit Labels. 104 | 105 | 3.2. Textual Representation 106 | 107 | A Bit-String Label is represented in text -- in a zone file, for 108 | example -- as a surrounded by the delimiters "\[" and "]". 109 | The is either a dotted quad or a base indicator and a 110 | sequence of digits appropriate to that base, optionally followed by a 111 | 112 | 113 | 114 | Crawford Standards Track [Page 2] 115 | 116 | RFC 2673 Binary Labels in the Domain Name System August 1999 117 | 118 | 119 | slash and a length. The base indicators are "b", "o" and "x", 120 | denoting base 2, 8 and 16 respectively. The length counts the 121 | significant bits and MUST be between 1 and 32, inclusive, after a 122 | dotted quad, or between 1 and 256, inclusive, after one of the other 123 | forms. If the length is omitted, the implicit length is 32 for a 124 | dotted quad or 1, 3 or 4 times the number of binary, octal or 125 | hexadecimal digits supplied, respectively, for the other forms. 126 | 127 | In augmented Backus-Naur form [ABNF], 128 | 129 | bit-string-label = "\[" bit-spec "]" 130 | 131 | bit-spec = bit-data [ "/" length ] 132 | / dotted-quad [ "/" slength ] 133 | 134 | bit-data = "x" 1*64HEXDIG 135 | / "o" 1*86OCTDIG 136 | / "b" 1*256BIT 137 | 138 | dotted-quad = decbyte "." decbyte "." decbyte "." decbyte 139 | 140 | decbyte = 1*3DIGIT 141 | 142 | length = NZDIGIT *2DIGIT 143 | 144 | slength = NZDIGIT [ DIGIT ] 145 | 146 | OCTDIG = %x30-37 147 | 148 | NZDIGIT = %x31-39 149 | 150 | If a is present, the number of digits in the MUST 151 | be just sufficient to contain the number of bits specified by the 152 | . If there are insignificant bits in a final hexadecimal or 153 | octal digit, they MUST be zero. A always has all four 154 | parts even if the associated is less than 24, but, like the 155 | other forms, insignificant bits MUST be zero. 156 | 157 | Each number represented by a must be between 0 and 255, 158 | inclusive. 159 | 160 | The number represented by must be between 1 and 256 161 | inclusive. 162 | 163 | The number represented by must be between 1 and 32 164 | inclusive. 165 | 166 | 167 | 168 | 169 | 170 | Crawford Standards Track [Page 3] 171 | 172 | RFC 2673 Binary Labels in the Domain Name System August 1999 173 | 174 | 175 | When the textual form of a Bit-String Label is generated by machine, 176 | the length SHOULD be explicit, not implicit. 177 | 178 | 3.2.1. Examples 179 | 180 | The following four textual forms represent the same Bit-String Label. 181 | 182 | \[b11010000011101] 183 | \[o64072/14] 184 | \[xd074/14] 185 | \[208.116.0.0/14] 186 | 187 | The following represents two consecutive Bit-String Labels which 188 | denote the same relative point in the DNS tree as any of the above 189 | single Bit-String Labels. 190 | 191 | \[b11101].\[o640] 192 | 193 | 3.3. Canonical Representation and Sort Order 194 | 195 | Both the wire form and the text form of binary labels have a degree 196 | of flexibility in their grouping into multiple consecutive Bit-String 197 | Labels. For generating and checking DNS signature records [DNSSEC] 198 | binary labels must be in a predictable form. This canonical form is 199 | defined as the form which has the fewest possible Bit-String Labels 200 | and in which all except possibly the first (least significant) label 201 | in any sequence of consecutive Bit-String Labels is of maximum 202 | length. 203 | 204 | For example, the canonical form of any sequence of up to 256 One-Bit 205 | Labels has a single Bit-String Label, and the canonical form of a 206 | sequence of 513 to 768 One-Bit Labels has three Bit-String Labels of 207 | which the second and third contain 256 label bits. 208 | 209 | The canonical sort order of domain names [DNSSEC] is extended to 210 | encompass binary labels as follows. Sorting is still label-by-label, 211 | from most to least significant, where a label may now be a One-Bit 212 | Label or a standard (code 00) label. Any One-Bit Label sorts before 213 | any standard label, and a 0 bit sorts before a 1 bit. The absence of 214 | a label sorts before any label, as specified in [DNSSEC]. 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Crawford Standards Track [Page 4] 227 | 228 | RFC 2673 Binary Labels in the Domain Name System August 1999 229 | 230 | 231 | For example, the following domain names are correctly sorted. 232 | 233 | foo.example 234 | \[b1].foo.example 235 | \[b100].foo.example 236 | \[b101].foo.example 237 | bravo.\[b10].foo.example 238 | alpha.foo.example 239 | 240 | 4. Processing Rules 241 | 242 | A One-Bit Label never matches any other kind of label. In 243 | particular, the DNS labels represented by the single ASCII characters 244 | "0" and "1" do not match One-Bit Labels represented by the bit values 245 | 0 and 1. 246 | 247 | 5. Discussion 248 | 249 | A Count of zero in the wire-form represents a 256-bit sequence, not 250 | to optimize that particular case, but to make it completely 251 | impossible to have a zero-bit label. 252 | 253 | 6. IANA Considerations 254 | 255 | This document defines one Extended Label Type, termed the Bit-String 256 | Label, and requests registration of the code point 000001 binary in 257 | the space defined by [EDNS0]. 258 | 259 | 7. Security Considerations 260 | 261 | All security considerations which apply to traditional ASCII DNS 262 | labels apply equally to binary labels. he canonicalization and 263 | sorting rules of section 3.3 allow these to be addressed by DNS 264 | Security [DNSSEC]. 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | Crawford Standards Track [Page 5] 283 | 284 | RFC 2673 Binary Labels in the Domain Name System August 1999 285 | 286 | 287 | 8. References 288 | 289 | [ABNF] Crocker, D. and P. Overell, "Augmented BNF for Syntax 290 | Specifications: ABNF", RFC 2234, November 1997. 291 | 292 | [DNSIS] Mockapetris, P., "Domain names - implementation and 293 | specification", STD 13, RFC 1035, November 1987. 294 | 295 | [DNSSEC] Eastlake, D., 3rd, C. Kaufman, "Domain Name System Security 296 | Extensions", RFC 2065, January 1997 297 | 298 | [EDNS0] Vixie, P., "Extension mechanisms for DNS (EDNS0)", RFC 2671, 299 | August 1999. 300 | 301 | [KWORD] Bradner, S., "Key words for use in RFCs to Indicate 302 | Requirement Levels," BCP 14, RFC 2119, March 1997. 303 | 304 | 9. Author's Address 305 | 306 | Matt Crawford 307 | Fermilab MS 368 308 | PO Box 500 309 | Batavia, IL 60510 310 | USA 311 | 312 | Phone: +1 630 840-3461 313 | EMail: crawdad@fnal.gov 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Crawford Standards Track [Page 6] 339 | 340 | RFC 2673 Binary Labels in the Domain Name System August 1999 341 | 342 | 343 | 10. Full Copyright Statement 344 | 345 | Copyright (C) The Internet Society (1999). All Rights Reserved. 346 | 347 | This document and translations of it may be copied and furnished to 348 | others, and derivative works that comment on or otherwise explain it 349 | or assist in its implementation may be prepared, copied, published 350 | and distributed, in whole or in part, without restriction of any 351 | kind, provided that the above copyright notice and this paragraph are 352 | included on all such copies and derivative works. However, this 353 | document itself may not be modified in any way, such as by removing 354 | the copyright notice or references to the Internet Society or other 355 | Internet organizations, except as needed for the purpose of 356 | developing Internet standards in which case the procedures for 357 | copyrights defined in the Internet Standards process must be 358 | followed, or as required to translate it into languages other than 359 | English. 360 | 361 | The limited permissions granted above are perpetual and will not be 362 | revoked by the Internet Society or its successors or assigns. 363 | 364 | This document and the information contained herein is provided on an 365 | "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING 366 | TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING 367 | BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION 368 | HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF 369 | MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 370 | 371 | Acknowledgement 372 | 373 | Funding for the RFC Editor function is currently provided by the 374 | Internet Society. 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | Crawford Standards Track [Page 7] 395 | 396 | -------------------------------------------------------------------------------- /spec/rfc6901: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Internet Engineering Task Force (IETF) P. Bryan, Ed. 8 | Request for Comments: 6901 Salesforce.com 9 | Category: Standards Track K. Zyp 10 | ISSN: 2070-1721 SitePen (USA) 11 | M. Nottingham, Ed. 12 | Akamai 13 | April 2013 14 | 15 | 16 | JavaScript Object Notation (JSON) Pointer 17 | 18 | Abstract 19 | 20 | JSON Pointer defines a string syntax for identifying a specific value 21 | within a JavaScript Object Notation (JSON) document. 22 | 23 | Status of This Memo 24 | 25 | This is an Internet Standards Track document. 26 | 27 | This document is a product of the Internet Engineering Task Force 28 | (IETF). It represents the consensus of the IETF community. It has 29 | received public review and has been approved for publication by the 30 | Internet Engineering Steering Group (IESG). Further information on 31 | Internet Standards is available in Section 2 of RFC 5741. 32 | 33 | Information about the current status of this document, any errata, 34 | and how to provide feedback on it may be obtained at 35 | http://www.rfc-editor.org/info/rfc6901. 36 | 37 | Copyright Notice 38 | 39 | Copyright (c) 2013 IETF Trust and the persons identified as the 40 | document authors. All rights reserved. 41 | 42 | This document is subject to BCP 78 and the IETF Trust's Legal 43 | Provisions Relating to IETF Documents 44 | (http://trustee.ietf.org/license-info) in effect on the date of 45 | publication of this document. Please review these documents 46 | carefully, as they describe your rights and restrictions with respect 47 | to this document. Code Components extracted from this document must 48 | include Simplified BSD License text as described in Section 4.e of 49 | the Trust Legal Provisions and are provided without warranty as 50 | described in the Simplified BSD License. 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Bryan, et al. Standards Track [Page 1] 59 | 60 | RFC 6901 JSON Pointer April 2013 61 | 62 | 63 | Table of Contents 64 | 65 | 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 2 66 | 2. Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . 2 67 | 3. Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 68 | 4. Evaluation . . . . . . . . . . . . . . . . . . . . . . . . . . 3 69 | 5. JSON String Representation . . . . . . . . . . . . . . . . . . 4 70 | 6. URI Fragment Identifier Representation . . . . . . . . . . . . 5 71 | 7. Error Handling . . . . . . . . . . . . . . . . . . . . . . . . 6 72 | 8. Security Considerations . . . . . . . . . . . . . . . . . . . . 6 73 | 9. Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . 7 74 | 10. References . . . . . . . . . . . . . . . . . . . . . . . . . . 7 75 | 10.1. Normative References . . . . . . . . . . . . . . . . . . . 7 76 | 10.2. Informative References . . . . . . . . . . . . . . . . . . 7 77 | 78 | 1. Introduction 79 | 80 | This specification defines JSON Pointer, a string syntax for 81 | identifying a specific value within a JavaScript Object Notation 82 | (JSON) document [RFC4627]. JSON Pointer is intended to be easily 83 | expressed in JSON string values as well as Uniform Resource 84 | Identifier (URI) [RFC3986] fragment identifiers. 85 | 86 | 2. Conventions 87 | 88 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 89 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this 90 | document are to be interpreted as described in [RFC2119]. 91 | 92 | This specification expresses normative syntax rules using Augmented 93 | Backus-Naur Form (ABNF) [RFC5234] notation. 94 | 95 | 3. Syntax 96 | 97 | A JSON Pointer is a Unicode string (see [RFC4627], Section 3) 98 | containing a sequence of zero or more reference tokens, each prefixed 99 | by a '/' (%x2F) character. 100 | 101 | Because the characters '~' (%x7E) and '/' (%x2F) have special 102 | meanings in JSON Pointer, '~' needs to be encoded as '~0' and '/' 103 | needs to be encoded as '~1' when these characters appear in a 104 | reference token. 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Bryan, et al. Standards Track [Page 2] 115 | 116 | RFC 6901 JSON Pointer April 2013 117 | 118 | 119 | The ABNF syntax of a JSON Pointer is: 120 | 121 | json-pointer = *( "/" reference-token ) 122 | reference-token = *( unescaped / escaped ) 123 | unescaped = %x00-2E / %x30-7D / %x7F-10FFFF 124 | ; %x2F ('/') and %x7E ('~') are excluded from 'unescaped' 125 | escaped = "~" ( "0" / "1" ) 126 | ; representing '~' and '/', respectively 127 | 128 | It is an error condition if a JSON Pointer value does not conform to 129 | this syntax (see Section 7). 130 | 131 | Note that JSON Pointers are specified in characters, not as bytes. 132 | 133 | 4. Evaluation 134 | 135 | Evaluation of a JSON Pointer begins with a reference to the root 136 | value of a JSON document and completes with a reference to some value 137 | within the document. Each reference token in the JSON Pointer is 138 | evaluated sequentially. 139 | 140 | Evaluation of each reference token begins by decoding any escaped 141 | character sequence. This is performed by first transforming any 142 | occurrence of the sequence '~1' to '/', and then transforming any 143 | occurrence of the sequence '~0' to '~'. By performing the 144 | substitutions in this order, an implementation avoids the error of 145 | turning '~01' first into '~1' and then into '/', which would be 146 | incorrect (the string '~01' correctly becomes '~1' after 147 | transformation). 148 | 149 | The reference token then modifies which value is referenced according 150 | to the following scheme: 151 | 152 | o If the currently referenced value is a JSON object, the new 153 | referenced value is the object member with the name identified by 154 | the reference token. The member name is equal to the token if it 155 | has the same number of Unicode characters as the token and their 156 | code points are byte-by-byte equal. No Unicode character 157 | normalization is performed. If a referenced member name is not 158 | unique in an object, the member that is referenced is undefined, 159 | and evaluation fails (see below). 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Bryan, et al. Standards Track [Page 3] 171 | 172 | RFC 6901 JSON Pointer April 2013 173 | 174 | 175 | o If the currently referenced value is a JSON array, the reference 176 | token MUST contain either: 177 | 178 | * characters comprised of digits (see ABNF below; note that 179 | leading zeros are not allowed) that represent an unsigned 180 | base-10 integer value, making the new referenced value the 181 | array element with the zero-based index identified by the 182 | token, or 183 | 184 | * exactly the single character "-", making the new referenced 185 | value the (nonexistent) member after the last array element. 186 | 187 | The ABNF syntax for array indices is: 188 | 189 | array-index = %x30 / ( %x31-39 *(%x30-39) ) 190 | ; "0", or digits without a leading "0" 191 | 192 | Implementations will evaluate each reference token against the 193 | document's contents and will raise an error condition if it fails to 194 | resolve a concrete value for any of the JSON pointer's reference 195 | tokens. For example, if an array is referenced with a non-numeric 196 | token, an error condition will be raised. See Section 7 for details. 197 | 198 | Note that the use of the "-" character to index an array will always 199 | result in such an error condition because by definition it refers to 200 | a nonexistent array element. Thus, applications of JSON Pointer need 201 | to specify how that character is to be handled, if it is to be 202 | useful. 203 | 204 | Any error condition for which a specific action is not defined by the 205 | JSON Pointer application results in termination of evaluation. 206 | 207 | 5. JSON String Representation 208 | 209 | A JSON Pointer can be represented in a JSON string value. Per 210 | [RFC4627], Section 2.5, all instances of quotation mark '"' (%x22), 211 | reverse solidus '\' (%x5C), and control (%x00-1F) characters MUST be 212 | escaped. 213 | 214 | Note that before processing a JSON string as a JSON Pointer, 215 | backslash escape sequences must be unescaped. 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | Bryan, et al. Standards Track [Page 4] 227 | 228 | RFC 6901 JSON Pointer April 2013 229 | 230 | 231 | For example, given the JSON document 232 | 233 | { 234 | "foo": ["bar", "baz"], 235 | "": 0, 236 | "a/b": 1, 237 | "c%d": 2, 238 | "e^f": 3, 239 | "g|h": 4, 240 | "i\\j": 5, 241 | "k\"l": 6, 242 | " ": 7, 243 | "m~n": 8 244 | } 245 | 246 | The following JSON strings evaluate to the accompanying values: 247 | 248 | "" // the whole document 249 | "/foo" ["bar", "baz"] 250 | "/foo/0" "bar" 251 | "/" 0 252 | "/a~1b" 1 253 | "/c%d" 2 254 | "/e^f" 3 255 | "/g|h" 4 256 | "/i\\j" 5 257 | "/k\"l" 6 258 | "/ " 7 259 | "/m~0n" 8 260 | 261 | 6. URI Fragment Identifier Representation 262 | 263 | A JSON Pointer can be represented in a URI fragment identifier by 264 | encoding it into octets using UTF-8 [RFC3629], while percent-encoding 265 | those characters not allowed by the fragment rule in [RFC3986]. 266 | 267 | Note that a given media type needs to specify JSON Pointer as its 268 | fragment identifier syntax explicitly (usually, in its registration 269 | [RFC6838]). That is, just because a document is JSON does not imply 270 | that JSON Pointer can be used as its fragment identifier syntax. In 271 | particular, the fragment identifier syntax for application/json is 272 | not JSON Pointer. 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | Bryan, et al. Standards Track [Page 5] 283 | 284 | RFC 6901 JSON Pointer April 2013 285 | 286 | 287 | Given the same example document as above, the following URI fragment 288 | identifiers evaluate to the accompanying values: 289 | 290 | # // the whole document 291 | #/foo ["bar", "baz"] 292 | #/foo/0 "bar" 293 | #/ 0 294 | #/a~1b 1 295 | #/c%25d 2 296 | #/e%5Ef 3 297 | #/g%7Ch 4 298 | #/i%5Cj 5 299 | #/k%22l 6 300 | #/%20 7 301 | #/m~0n 8 302 | 303 | 7. Error Handling 304 | 305 | In the event of an error condition, evaluation of the JSON Pointer 306 | fails to complete. 307 | 308 | Error conditions include, but are not limited to: 309 | 310 | o Invalid pointer syntax 311 | 312 | o A pointer that references a nonexistent value 313 | 314 | This specification does not define how errors are handled. An 315 | application of JSON Pointer SHOULD specify the impact and handling of 316 | each type of error. 317 | 318 | For example, some applications might stop pointer processing upon an 319 | error, while others may attempt to recover from missing values by 320 | inserting default ones. 321 | 322 | 8. Security Considerations 323 | 324 | A given JSON Pointer is not guaranteed to reference an actual JSON 325 | value. Therefore, applications using JSON Pointer should anticipate 326 | this situation by defining how a pointer that does not resolve ought 327 | to be handled. 328 | 329 | Note that JSON pointers can contain the NUL (Unicode U+0000) 330 | character. Care is needed not to misinterpret this character in 331 | programming languages that use NUL to mark the end of a string. 332 | 333 | 334 | 335 | 336 | 337 | 338 | Bryan, et al. Standards Track [Page 6] 339 | 340 | RFC 6901 JSON Pointer April 2013 341 | 342 | 343 | 9. Acknowledgements 344 | 345 | The following individuals contributed ideas, feedback, and wording to 346 | this specification: 347 | 348 | Mike Acar, Carsten Bormann, Tim Bray, Jacob Davies, Martin J. 349 | Duerst, Bjoern Hoehrmann, James H. Manger, Drew Perttula, and 350 | Julian Reschke. 351 | 352 | 10. References 353 | 354 | 10.1. Normative References 355 | 356 | [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate 357 | Requirement Levels", BCP 14, RFC 2119, March 1997. 358 | 359 | [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO 360 | 10646", STD 63, RFC 3629, November 2003. 361 | 362 | [RFC3986] Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform 363 | Resource Identifier (URI): Generic Syntax", STD 66, 364 | RFC 3986, January 2005. 365 | 366 | [RFC4627] Crockford, D., "The application/json Media Type for 367 | JavaScript Object Notation (JSON)", RFC 4627, July 2006. 368 | 369 | [RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax 370 | Specifications: ABNF", STD 68, RFC 5234, January 2008. 371 | 372 | 10.2. Informative References 373 | 374 | [RFC6838] Freed, N., Klensin, J., and T. Hansen, "Media Type 375 | Specifications and Registration Procedures", BCP 13, 376 | RFC 6838, January 2013. 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | Bryan, et al. Standards Track [Page 7] 395 | 396 | RFC 6901 JSON Pointer April 2013 397 | 398 | 399 | Authors' Addresses 400 | 401 | Paul C. Bryan (editor) 402 | Salesforce.com 403 | 404 | Phone: +1 604 783 1481 405 | EMail: pbryan@anode.ca 406 | 407 | 408 | Kris Zyp 409 | SitePen (USA) 410 | 411 | Phone: +1 650 968 8787 412 | EMail: kris@sitepen.com 413 | 414 | 415 | Mark Nottingham (editor) 416 | Akamai 417 | 418 | EMail: mnot@mnot.net 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | Bryan, et al. Standards Track [Page 8] 451 | 452 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha/schema.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha.schema 4 | (:refer-clojure :exclude [number? integer?]) 5 | #?@ 6 | (:clj 7 | [(:require 8 | [juxt.jinx.alpha.core 9 | :refer 10 | [array? integer? number? object? regex? schema?]] 11 | [lambdaisland.uri :refer [join]]) 12 | (:import clojure.lang.ExceptionInfo)] 13 | :cljs 14 | [(:require 15 | [cljs.core :refer [ExceptionInfo]] 16 | [juxt.jinx.alpha.core 17 | :refer 18 | [array? integer? number? object? regex? schema?]] 19 | [lambdaisland.uri :refer [join]])])) 20 | 21 | (defn- with-base-uri-meta 22 | "For each $id in the schema, add metadata to indicate the base-uri." 23 | ([schema] 24 | (with-base-uri-meta nil schema)) 25 | ([base-uri schema] 26 | (cond 27 | (map? schema) 28 | (if-let [id (get schema "$id")] 29 | (let [new-base-uri (str (join base-uri id))] 30 | (with-meta 31 | (assoc (with-base-uri-meta new-base-uri (dissoc schema "$id")) "$id" id) 32 | {:base-uri new-base-uri 33 | :id id})) 34 | (with-meta 35 | (zipmap (keys schema) (map (partial with-base-uri-meta base-uri) (vals schema))) 36 | {:base-uri base-uri})) 37 | (vector? schema) (mapv (partial with-base-uri-meta base-uri) schema) 38 | :else schema))) 39 | 40 | (defn- index-by-uri [schema] 41 | (cond 42 | (map? schema) 43 | (let [mt (meta schema)] 44 | (if (:id mt) 45 | (cons [(:base-uri mt) schema] 46 | (index-by-uri (with-meta schema (dissoc mt :id)))) 47 | (mapcat index-by-uri (vals schema)))) 48 | 49 | (vector? schema) 50 | (mapcat index-by-uri schema))) 51 | 52 | (declare validate) 53 | 54 | (defmulti validate-keyword (fn [kw v options] kw)) 55 | 56 | (defmethod validate-keyword :default [kw v options] nil) 57 | 58 | (defmethod validate-keyword "type" [kw v options] 59 | (when-not (or (string? v) (array? v)) 60 | (throw (ex-info "The value of 'type' MUST be either a string or an array" {:value v}))) 61 | 62 | (when (array? v) 63 | (when-not (every? string? v) 64 | (throw (ex-info "The value of 'type', if it is an array, elements of the array MUST be strings" {}))) 65 | (when-not (apply distinct? v) 66 | (throw (ex-info "The value of 'type', if it is an array, elements of the array MUST be unique" {})))) 67 | 68 | (let [legal #{"null" "boolean" "object" "array" "number" "string" "integer"}] 69 | (when-not 70 | (or 71 | (and (string? v) (contains? legal v)) 72 | (and (array? v) (every? #(contains? legal %) v))) 73 | (throw (ex-info "String values of 'type' MUST be one of the six primitive types or 'integer'" {:value v}))))) 74 | 75 | (defmethod validate-keyword "enum" [kw v options] 76 | (when-not (array? v) 77 | (throw (ex-info "The value of an enum MUST be an array" {:value v}))) 78 | (when (:strict? options) 79 | (when (empty? v) 80 | (throw (ex-info "The value of an enum SHOULD have at least one element" {:value v}))) 81 | (when-not (apply distinct? v) 82 | (throw (ex-info "Elements in the enum value array SHOULD be unique" {:value v}))))) 83 | 84 | (defmethod validate-keyword "multipleOf" [kw v options] 85 | (when-not (and (number? v) (pos? v)) 86 | (throw (ex-info "The value of multipleOf MUST be a number, strictly greater than 0" {:value v})))) 87 | 88 | (defmethod validate-keyword "maximum" [kw v options] 89 | (when-not (number? v) 90 | (throw (ex-info "The value of maximum MUST be a number" {:value v})))) 91 | 92 | (defmethod validate-keyword "exclusiveMaximum" [kw v options] 93 | (when-not (number? v) 94 | (throw (ex-info "The value of exclusiveMaximum MUST be a number" {:value v})))) 95 | 96 | (defmethod validate-keyword "minimum" [kw v options] 97 | (when-not (number? v) 98 | (throw (ex-info "The value of minimum MUST be a number" {:value v})))) 99 | 100 | (defmethod validate-keyword "exclusiveMinimum" [kw v options] 101 | (when-not (number? v) 102 | (throw (ex-info "The value of exclusiveMinimum MUST be a number" {:value v})))) 103 | 104 | (defmethod validate-keyword "maxLength" [kw v options] 105 | (when-not (and (integer? v) (not (neg? v))) 106 | (throw (ex-info "The value of maxLength MUST be a non-negative integer" {:value v})))) 107 | 108 | (defmethod validate-keyword "minLength" [kw v options] 109 | (when-not (and (integer? v) (not (neg? v))) 110 | (throw (ex-info "The value of minLength MUST be a non-negative integer" {:value v})))) 111 | 112 | (defmethod validate-keyword "pattern" [kw v options] 113 | (when-not (string? v) 114 | (throw (ex-info "The value of pattern MUST be a string" {:value v})))) 115 | 116 | (defmethod validate-keyword "items" [kw v options] 117 | (cond 118 | (schema? v) 119 | (try 120 | (validate v options) 121 | (catch ExceptionInfo cause 122 | (throw (ex-info 123 | "The value of 'items' MUST be a valid JSON Schema" 124 | {:value v} 125 | cause)))) 126 | 127 | (array? v) 128 | (try 129 | (doseq [el v] 130 | (try 131 | (validate el options) 132 | (catch ExceptionInfo cause 133 | (throw (ex-info 134 | "The value of 'items' MUST be an array of valid JSON Schemas, but at least one element isn't valid" 135 | {:element el} 136 | cause)))))) 137 | 138 | :else 139 | (throw (ex-info "The value of 'items' MUST be either a valid JSON Schema or an array of valid JSON Schemas" {})))) 140 | 141 | (defmethod validate-keyword "additionalItems" [kw v options] 142 | (when-not (schema? v) 143 | (throw (ex-info "The value of 'additionalItems' MUST be a valid JSON Schema" {:value v}))) 144 | (try 145 | (validate v options) 146 | (catch ExceptionInfo cause 147 | (throw (ex-info "The value of 'additionalItems' MUST be a valid JSON Schema" {:value v} cause))))) 148 | 149 | (defmethod validate-keyword "maxItems" [kw v options] 150 | (when-not (and (integer? v) (not (neg? v))) 151 | (throw (ex-info "The value of 'maxItems' MUST be a non-negative integer" {:value v})))) 152 | 153 | (defmethod validate-keyword "minItems" [kw v options] 154 | (when-not (and (integer? v) (not (neg? v))) 155 | (throw (ex-info "The value of 'minItems' MUST be a non-negative integer" {:value v})))) 156 | 157 | (defmethod validate-keyword "uniqueItems" [kw v options] 158 | (when-not (boolean? v) 159 | (throw (ex-info "The value of 'uniqueItems' MUST be a boolean" {:value v})))) 160 | 161 | (defmethod validate-keyword "contains" [kw v options] 162 | (when-not (schema? v) 163 | (throw (ex-info "The value of 'contains' MUST be a valid JSON Schema" {:value v}))) 164 | (try 165 | (validate v options) 166 | (catch ExceptionInfo cause 167 | (throw (ex-info "The value of 'contains' MUST be a valid JSON Schema" {:value v} cause))))) 168 | 169 | (defmethod validate-keyword "maxProperties" [kw v options] 170 | (when-not (and (integer? v) (not (neg? v))) 171 | (throw (ex-info "The value of 'maxProperties' MUST be a non-negative integer" {:value v})))) 172 | 173 | (defmethod validate-keyword "minProperties" [kw v options] 174 | (when-not (and (integer? v) (not (neg? v))) 175 | (throw (ex-info "The value of 'minProperties' MUST be a non-negative integer" {:value v})))) 176 | 177 | (defmethod validate-keyword "required" [kw v options] 178 | (when-not (array? v) 179 | (throw (ex-info "The value of 'required' MUST be an array" {:value v}))) 180 | (when (and (array? v) (not-empty v)) 181 | (when-not (every? string? v) 182 | (throw (ex-info "The value of 'required' MUST be an array. Elements of this array, if any, MUST be strings" {:value v}))) 183 | (when-not (apply distinct? v) 184 | (throw (ex-info "The value of 'required' MUST be an array. Elements of this array, if any, MUST be unique" {:value v}))))) 185 | 186 | (defmethod validate-keyword "properties" [kw v options] 187 | (when-not (object? v) 188 | (throw (ex-info "The value of 'properties' MUST be an object" {:value v}))) 189 | (doseq [[subkw subschema] v] 190 | (try 191 | (validate subschema options) 192 | (catch ExceptionInfo cause 193 | (throw (ex-info "Each value of 'properties' MUST be a valid JSON Schema" {:keyword subkw} cause)))))) 194 | 195 | (defmethod validate-keyword "patternProperties" [kw v options] 196 | (when-not (object? v) 197 | (throw (ex-info "The value of 'patternProperties' MUST be an object" {:value v}))) 198 | (doseq [[subkw subschema] v] 199 | (when-not (regex? subkw) 200 | (throw (ex-info "Each property name of a 'patternProperties' object SHOULD be a valid regular expression" {}))) 201 | (try 202 | (validate subschema options) 203 | (catch ExceptionInfo cause 204 | (throw (ex-info "Each value of a 'patternProperties' object MUST be a valid JSON Schema" {:keyword subkw} cause)))))) 205 | 206 | (defmethod validate-keyword "additionalProperties" [kw v options] 207 | (when-not (schema? v) 208 | (throw (ex-info "The value of 'additionalProperties' MUST be a valid JSON Schema" {:value v}))) 209 | (try 210 | (validate v options) 211 | (catch ExceptionInfo cause 212 | (throw (ex-info "The value of 'additionalProperties' MUST be a valid JSON Schema" {:value v} cause))))) 213 | 214 | (defmethod validate-keyword "dependencies" [kw v options] 215 | (when-not (object? v) 216 | (throw (ex-info "The value of 'dependencies' MUST be an object" {}))) 217 | (doseq [v (vals v)] 218 | (when-not (or (array? v) (schema? v)) 219 | (throw (ex-info "Dependency values MUST be an array or a JSON Schema" {:value v}))) 220 | (when (and (array? v) (not-empty v)) 221 | (when-not (every? string? v) 222 | (throw (ex-info "Each element in a dependencies array MUST be a string" {}))) 223 | (when-not (apply distinct? v) 224 | (throw (ex-info "Each element in a dependencies array MUST be unique" {})))) 225 | (when (schema? v) 226 | (try 227 | (validate v options) 228 | (catch ExceptionInfo cause 229 | (throw (ex-info "Dependency values MUST be an array or a valid JSON Schema" {:value v} cause))))))) 230 | 231 | (defmethod validate-keyword "propertyNames" [kw v options] 232 | (when-not (schema? v) 233 | (throw (ex-info "The value of 'propertyNames' MUST be a JSON Schema" {:value v}))) 234 | (try 235 | (validate v options) 236 | (catch ExceptionInfo cause 237 | (throw (ex-info "The value of 'propertyNames' MUST be a valid JSON Schema" {:value v} cause))))) 238 | 239 | (defmethod validate-keyword "if" [kw v options] 240 | (when-not (schema? v) 241 | (throw (ex-info "The value of 'if' MUST be a JSON Schema" {:value v}))) 242 | (try 243 | (validate v options) 244 | (catch ExceptionInfo cause 245 | (throw (ex-info "The value of 'if' MUST be a valid JSON Schema" {:value v} cause))))) 246 | 247 | (defmethod validate-keyword "then" [kw v options] 248 | (when-not (schema? v) 249 | (throw (ex-info "The value of 'then' MUST be a JSON Schema" {:value v}))) 250 | (try 251 | (validate v options) 252 | (catch ExceptionInfo cause 253 | (throw (ex-info "The value of 'then' MUST be a valid JSON Schema" {:value v} cause))))) 254 | 255 | (defmethod validate-keyword "else" [kw v options] 256 | (when-not (schema? v) 257 | (throw (ex-info "The value of 'else' MUST be a JSON Schema" {:value v}))) 258 | (try 259 | (validate v options) 260 | (catch ExceptionInfo cause 261 | (throw (ex-info "The value of 'else' MUST be a valid JSON Schema" {:value v} cause))))) 262 | 263 | (defmethod validate-keyword "allOf" [kw v options] 264 | (when-not (array? v) 265 | (throw (ex-info "The value of 'allOf' MUST be a non-empty array" {:value v}))) 266 | (when (empty? v) 267 | (throw (ex-info "The value of 'allOf' MUST be a non-empty array" {:value v}))) 268 | (doseq [subschema v] 269 | (try 270 | (validate subschema options) 271 | (catch ExceptionInfo cause 272 | (throw (ex-info "Each item of an 'allOf' array MUST be a valid schema" {:value v} cause)))))) 273 | 274 | (defmethod validate-keyword "anyOf" [kw v options] 275 | (when-not (array? v) 276 | (throw (ex-info "The value of 'anyOf' MUST be a non-empty array" {:value v}))) 277 | (when (empty? v) 278 | (throw (ex-info "The value of 'anyOf' MUST be a non-empty array" {:value v}))) 279 | (doseq [subschema v] 280 | (try 281 | (validate subschema options) 282 | (catch ExceptionInfo cause 283 | (throw (ex-info "Each item of an 'anyOf' array MUST be a valid schema" {:value v} cause)))))) 284 | 285 | (defmethod validate-keyword "oneOf" [kw v options] 286 | (when-not (array? v) 287 | (throw (ex-info "The value of 'oneOf' MUST be a non-empty array" {:value v}))) 288 | (when (empty? v) 289 | (throw (ex-info "The value of 'oneOf' MUST be a non-empty array" {:value v}))) 290 | (doseq [subschema v] 291 | (try 292 | (validate subschema options) 293 | (catch ExceptionInfo cause 294 | (throw (ex-info "Each item of an 'oneOf' array MUST be a valid schema" {:value v} cause)))))) 295 | 296 | (defmethod validate-keyword "not" [kw v options] 297 | (when-not (schema? v) 298 | (throw (ex-info "The value of 'not' MUST be a JSON Schema" {:value v}))) 299 | (try 300 | (validate v options) 301 | (catch ExceptionInfo cause 302 | (throw (ex-info "The value of 'not' MUST be a valid JSON Schema" {:value v} cause))))) 303 | 304 | (defmethod validate-keyword "format" [kw v options] 305 | (when-not (string? v) 306 | (throw (ex-info "The value of a 'format' attribute MUST be a string" {:value v})))) 307 | 308 | (defn validate 309 | "Validate a schema, checking it obeys conformance rules. When 310 | the :strict? option is truthy, rules that contain SHOULD are 311 | considered errors." 312 | ([schema] 313 | (validate schema {:strict? true})) 314 | ([schema options] 315 | (or 316 | (boolean? schema) 317 | (nil? schema) 318 | (doseq [[k v] (seq schema)] 319 | (validate-keyword k v options)) 320 | schema))) 321 | 322 | (defn schema 323 | ([s] 324 | (schema s {:strict? true})) 325 | ([schema options] 326 | ;; TODO: Ensure schema is returned as-is if it's existing schema 327 | ;; TODO: Add ^:juxt/schema true - which is the right keyword here? 328 | (validate schema options) 329 | (let [schema (with-base-uri-meta schema) 330 | index (into {} (index-by-uri schema))] 331 | (cond-> 332 | schema 333 | (and #?(:clj (instance? clojure.lang.IMeta schema) :cljs (satisfies? IMeta schema)) index) 334 | (with-meta (-> schema meta (assoc :uri->schema index))))))) 335 | 336 | ;; TODO: Try against all schemas in test-suite 337 | 338 | (comment 339 | (let [schema 340 | {"$id" "http://example.com/root.json" 341 | "definitions" 342 | {"A" {"$id" "#foo"} 343 | "B" {"$id" "other.json" 344 | "definitions" 345 | {"X" {"$id" "#bar"} 346 | "Y" {"$id" "t/inner.json"}}} 347 | "C" {"$id" "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f"}}} 348 | 349 | schema (with-base-uri-meta schema) 350 | schema (into {} (index-by-uri schema))] 351 | 352 | (-> (get schema "http://example.com/root.json") 353 | (get-in ["definitions" "B" "definitions"]) 354 | meta))) 355 | 356 | (comment 357 | (let [schema 358 | {"$id" "http://localhost:1234/tree" 359 | "description" "tree of nodes" 360 | "type" "object" 361 | "properties" 362 | {"meta" {"type" "string"} 363 | "nodes" {"type" "array", "items" {"$ref" "node"}}} 364 | "required" ["meta" "nodes"] 365 | "definitions" 366 | {"node" 367 | {"$id" "http://localhost:1234/node" 368 | "description" "node" 369 | "type" "object" 370 | "properties" 371 | {"value" {"type" "number"}, "subtree" {"$ref" "tree"}} 372 | "required" ["value"]}}} 373 | schema (with-base-uri-meta schema) 374 | index (into {} (index-by-uri schema))] 375 | (-> index 376 | (get "http://localhost:1234/node") 377 | (get-in ["properties" "subtree"]) 378 | meta 379 | :base-uri 380 | (join "tree") 381 | str))) 382 | 383 | 384 | ;; NOTE: A relative $ref will now be easy to resolve to a URI, using 385 | ;; the :base-uri for the object's metadata. 386 | -------------------------------------------------------------------------------- /test/juxt/jinx/schema_test.cljc: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.schema-test 4 | (:require 5 | [juxt.jinx.alpha.schema :as schema] 6 | #?(:clj 7 | [clojure.test :refer [deftest is are testing]] 8 | :cljs 9 | [cljs.test :refer-macros [deftest is testing]] 10 | [cljs.core :refer [ExceptionInfo]])) 11 | #?(:clj 12 | (:import 13 | (clojure.lang ExceptionInfo)))) 14 | 15 | (deftest type-test 16 | (testing "bad type" 17 | (is 18 | (thrown-with-msg? 19 | ExceptionInfo 20 | #"The value of 'type' MUST be either a string or an array" 21 | (schema/validate {"type" 10})))) 22 | 23 | (testing "bad string type" 24 | (is 25 | (thrown-with-msg? 26 | ExceptionInfo 27 | #"String values of 'type' MUST be one of the six primitive types or 'integer'" 28 | (schema/validate {"type" "float"})))) 29 | 30 | (testing "array elements must be strings" 31 | (is 32 | (thrown-with-msg? 33 | ExceptionInfo 34 | #"The value of 'type', if it is an array, elements of the array MUST be strings" 35 | (schema/validate {"type" ["string" 10]})))) 36 | 37 | (testing "array elements must be strings" 38 | (is 39 | (thrown-with-msg? 40 | ExceptionInfo 41 | #"The value of 'type', if it is an array, elements of the array MUST be strings" 42 | (schema/validate {"type" [nil]})))) 43 | 44 | (testing "unique elements" 45 | (is 46 | (thrown-with-msg? 47 | ExceptionInfo 48 | #"The value of 'type', if it is an array, elements of the array MUST be unique" 49 | (schema/validate {"type" ["string" "string"]})))) 50 | 51 | (testing "integer" 52 | (is 53 | (schema/validate {"type" ["integer"]}))) 54 | 55 | (testing "illegal" 56 | (is 57 | (thrown-with-msg? 58 | ExceptionInfo 59 | #"String values of 'type' MUST be one of the six primitive types or 'integer'" 60 | (schema/validate {"type" ["float" "number"]}))))) 61 | 62 | (deftest enum-test 63 | (testing "must be an array" 64 | (is 65 | (schema/validate {"enum" ["foo" "bar"]})) 66 | (is 67 | (thrown-with-msg? 68 | ExceptionInfo 69 | #"The value of an enum MUST be an array" 70 | (schema/validate {"enum" "foo"}))) 71 | (is 72 | (thrown-with-msg? 73 | ExceptionInfo 74 | #"The value of an enum SHOULD have at least one element" 75 | (schema/validate {"enum" []}))) 76 | (is 77 | (thrown-with-msg? 78 | ExceptionInfo 79 | #"Elements in the enum value array SHOULD be unique" 80 | (schema/validate {"enum" ["foo" "foo"]}))))) 81 | 82 | (deftest const-test 83 | (testing "may be any type" 84 | (is 85 | (schema/validate {"const" "foo"})) 86 | (is 87 | (schema/validate {"const" []})) 88 | (is 89 | (schema/validate {"const" ["foo"]})) 90 | (is 91 | (schema/validate {"const" nil})))) 92 | 93 | (deftest multiple-of-test 94 | (is 95 | (thrown-with-msg? 96 | ExceptionInfo 97 | #"The value of multipleOf MUST be a number, strictly greater than 0" 98 | (schema/validate {"multipleOf" "foo"}))) 99 | (is 100 | (thrown-with-msg? 101 | ExceptionInfo 102 | #"The value of multipleOf MUST be a number, strictly greater than 0" 103 | (schema/validate {"multipleOf" 0}))) 104 | (is 105 | (thrown-with-msg? 106 | ExceptionInfo 107 | #"The value of multipleOf MUST be a number, strictly greater than 0" 108 | (schema/validate {"multipleOf" -1}))) 109 | (is 110 | (schema/validate {"multipleOf" 0.1}))) 111 | 112 | (deftest maximum-test 113 | (is 114 | (thrown-with-msg? 115 | ExceptionInfo 116 | #"The value of maximum MUST be a number" 117 | (schema/validate {"maximum" "foo"}))) 118 | (is 119 | (schema/validate {"maximum" 10})) 120 | (is 121 | (schema/validate {"maximum" 10.5}))) 122 | 123 | (deftest exclustive-maximum-test 124 | (is 125 | (thrown-with-msg? 126 | ExceptionInfo 127 | #"The value of exclusiveMaximum MUST be a number" 128 | (schema/validate {"exclusiveMaximum" "foo"})))) 129 | 130 | (deftest minimum-test 131 | (is 132 | (thrown-with-msg? 133 | ExceptionInfo 134 | #"The value of minimum MUST be a number" 135 | (schema/validate {"minimum" "foo"})))) 136 | 137 | (deftest exclusive-minimum-test 138 | (is 139 | (thrown-with-msg? 140 | ExceptionInfo 141 | #"The value of exclusiveMinimum MUST be a number" 142 | (schema/validate {"exclusiveMinimum" "foo"})))) 143 | 144 | (deftest max-length-test 145 | (is 146 | (thrown-with-msg? 147 | ExceptionInfo 148 | #"The value of maxLength MUST be a non-negative integer" 149 | (schema/validate {"maxLength" "foo"}))) 150 | (is 151 | (thrown-with-msg? 152 | ExceptionInfo 153 | #"The value of maxLength MUST be a non-negative integer" 154 | (schema/validate {"maxLength" -1}))) 155 | (is (schema/validate {"maxLength" 0})) 156 | (is (schema/validate {"maxLength" 5}))) 157 | 158 | (deftest min-length-test 159 | (is 160 | (thrown-with-msg? 161 | ExceptionInfo 162 | #"The value of minLength MUST be a non-negative integer" 163 | (schema/validate {"minLength" "foo"}))) 164 | (is 165 | (thrown-with-msg? 166 | ExceptionInfo 167 | #"The value of minLength MUST be a non-negative integer" 168 | (schema/validate {"minLength" -1}))) 169 | (is (schema/validate {"minLength" 0})) 170 | (is (schema/validate {"minLength" 5}))) 171 | 172 | (deftest pattern-test 173 | (is 174 | (thrown-with-msg? 175 | ExceptionInfo 176 | #"The value of pattern MUST be a string" 177 | (schema/validate {"pattern" 10}))) 178 | (is (schema/validate {"pattern" "foobar.?"}))) 179 | 180 | (deftest items-test 181 | (testing "Nil value" 182 | (is 183 | (thrown-with-msg? 184 | ExceptionInfo 185 | #"The value of 'items' MUST be either a valid JSON Schema or an array of valid JSON Schemas" 186 | (schema/validate {"items" nil})))) 187 | 188 | (testing "Bad subschema" 189 | (is 190 | (thrown-with-msg? 191 | ExceptionInfo 192 | #"The value of 'items' MUST be a valid JSON Schema" 193 | (schema/validate {"items" {"type" "foo"}})))) 194 | 195 | (testing "Bad subschemas" 196 | (is 197 | (thrown-with-msg? 198 | ExceptionInfo 199 | #"The value of 'items' MUST be an array of valid JSON Schemas, but at least one element isn't valid" 200 | (schema/validate {"items" [{"type" "string"} 201 | {"type" "number"} 202 | {"type" "float"}]}))))) 203 | 204 | (deftest additional-items-test 205 | (is (schema/validate {"additionalItems" false})) 206 | (is (schema/validate {"additionalItems" true})) 207 | (is 208 | (thrown-with-msg? 209 | ExceptionInfo 210 | #"The value of 'additionalItems' MUST be a valid JSON Schema" 211 | (schema/validate {"additionalItems" nil}))) 212 | (is 213 | (thrown-with-msg? 214 | ExceptionInfo 215 | #"The value of 'additionalItems' MUST be a valid JSON Schema" 216 | (schema/validate {"additionalItems" {"type" "foo"}})))) 217 | 218 | (deftest max-items-test 219 | (is (schema/validate {"maxItems" 10})) 220 | (is (schema/validate {"maxItems" 0})) 221 | (is 222 | (thrown-with-msg? 223 | ExceptionInfo 224 | #"The value of 'maxItems' MUST be a non-negative integer" 225 | (schema/validate {"maxItems" -1}))) 226 | (is 227 | (thrown-with-msg? 228 | ExceptionInfo 229 | #"The value of 'maxItems' MUST be a non-negative integer" 230 | (schema/validate {"maxItems" 0.5})))) 231 | 232 | (deftest min-items-test 233 | (is (schema/validate {"minItems" 10})) 234 | (is (schema/validate {"minItems" 0})) 235 | (is 236 | (thrown-with-msg? 237 | ExceptionInfo 238 | #"The value of 'minItems' MUST be a non-negative integer" 239 | (schema/validate {"minItems" -1}))) 240 | (is 241 | (thrown-with-msg? 242 | ExceptionInfo 243 | #"The value of 'minItems' MUST be a non-negative integer" 244 | (schema/validate {"minItems" 0.5})))) 245 | 246 | (deftest unique-items-test 247 | (is (schema/validate {"uniqueItems" true})) 248 | (is (schema/validate {"uniqueItems" false})) 249 | (is 250 | (thrown-with-msg? 251 | ExceptionInfo 252 | #"The value of 'uniqueItems' MUST be a boolean" 253 | (schema/validate {"uniqueItems" 1})))) 254 | 255 | (deftest contains-test 256 | (is (schema/validate {"contains" true})) 257 | (is (schema/validate {"contains" false})) 258 | (is (schema/validate {"contains" {"type" "string"}})) 259 | (is 260 | (thrown-with-msg? 261 | ExceptionInfo 262 | #"The value of 'contains' MUST be a valid JSON Schema" 263 | (schema/validate {"contains" {"type" "foo"}})))) 264 | 265 | (deftest max-properties-test 266 | (is (schema/validate {"maxProperties" 10})) 267 | (is (schema/validate {"maxProperties" 0})) 268 | (is 269 | (thrown-with-msg? 270 | ExceptionInfo 271 | #"The value of 'maxProperties' MUST be a non-negative integer" 272 | (schema/validate {"maxProperties" -1}))) 273 | (is 274 | (thrown-with-msg? 275 | ExceptionInfo 276 | #"The value of 'maxProperties' MUST be a non-negative integer" 277 | (schema/validate {"maxProperties" 0.5})))) 278 | 279 | (deftest min-properties-test 280 | (is (schema/validate {"minProperties" 10})) 281 | (is (schema/validate {"minProperties" 0})) 282 | (is 283 | (thrown-with-msg? 284 | ExceptionInfo 285 | #"The value of 'minProperties' MUST be a non-negative integer" 286 | (schema/validate {"minProperties" -1}))) 287 | (is 288 | (thrown-with-msg? 289 | ExceptionInfo 290 | #"The value of 'minProperties' MUST be a non-negative integer" 291 | (schema/validate {"minProperties" 0.5})))) 292 | 293 | (deftest required-test 294 | (is (schema/validate {"required" []})) 295 | (is (schema/validate {"required" ["foo" "bar"]})) 296 | 297 | (is 298 | (thrown-with-msg? 299 | ExceptionInfo 300 | #"The value of 'required' MUST be an array" 301 | (schema/validate {"required" "foo"}))) 302 | 303 | (is 304 | (thrown-with-msg? 305 | ExceptionInfo 306 | #"The value of 'required' MUST be an array. Elements of this array, if any, MUST be strings" 307 | (schema/validate {"required" ["foo" 0]}))) 308 | 309 | (is 310 | (thrown-with-msg? 311 | ExceptionInfo 312 | #"The value of 'required' MUST be an array. Elements of this array, if any, MUST be unique" 313 | (schema/validate {"required" ["foo" "foo"]})))) 314 | 315 | 316 | (deftest properties-test 317 | (testing "empty object" 318 | (is (schema/validate {"properties" {}}))) 319 | 320 | (testing "Bad object" 321 | (is 322 | (thrown-with-msg? 323 | ExceptionInfo 324 | #"The value of 'properties' MUST be an object" 325 | (schema/validate {"properties" 10})))) 326 | 327 | (testing "Bad subschema" 328 | (is 329 | (thrown-with-msg? 330 | ExceptionInfo 331 | #"Each value of 'properties' MUST be a valid JSON Schema" 332 | (schema/validate {"properties" {"foo" {"type" "bar"}}}))))) 333 | 334 | (deftest pattern-properties-test 335 | (testing "empty object" 336 | (is (schema/validate {"patternProperties" {}}))) 337 | 338 | (testing "Bad object" 339 | (is 340 | (thrown-with-msg? 341 | ExceptionInfo 342 | #"The value of 'patternProperties' MUST be an object" 343 | (schema/validate {"patternProperties" 10})))) 344 | 345 | (testing "Bad subschema" 346 | (is 347 | (thrown-with-msg? 348 | ExceptionInfo 349 | #"Each value of a 'patternProperties' object MUST be a valid JSON Schema" 350 | (schema/validate {"patternProperties" {"foo" {"type" "bar"}}}))))) 351 | 352 | (deftest additional-properties-test 353 | (is (schema/validate {"additionalProperties" false})) 354 | (is (schema/validate {"additionalProperties" true})) 355 | (is (thrown-with-msg? 356 | ExceptionInfo 357 | #"The value of 'additionalProperties' MUST be a valid JSON Schema" 358 | (schema/validate {"additionalProperties" nil}))) 359 | (is (thrown-with-msg? 360 | ExceptionInfo 361 | #"The value of 'additionalProperties' MUST be a valid JSON Schema" 362 | (schema/validate {"additionalProperties" {"type" "foo"}})))) 363 | 364 | (deftest dependencies-test 365 | (is (schema/validate {"dependencies" {}})) 366 | (is (thrown-with-msg? 367 | ExceptionInfo 368 | #"The value of 'dependencies' MUST be an object" 369 | (schema/validate {"dependencies" true}))) 370 | 371 | (is (schema/validate {"dependencies" {"a" []}})) 372 | (is (schema/validate {"dependencies" {"a" ["foo" "bar"]}})) 373 | (is (thrown-with-msg? 374 | ExceptionInfo 375 | #"Each element in a dependencies array MUST be a string" 376 | (schema/validate {"dependencies" {"a" ["foo" 10]}}))) 377 | (is (thrown-with-msg? 378 | ExceptionInfo 379 | #"Each element in a dependencies array MUST be unique" 380 | (schema/validate {"dependencies" {"a" ["foo" "foo"]}}))) 381 | 382 | (is (thrown-with-msg? 383 | ExceptionInfo 384 | #"Dependency values MUST be an array or a valid JSON Schema" 385 | (schema/validate {"dependencies" {"a" {"type" "foo"}}}))) 386 | 387 | (is (thrown-with-msg? 388 | ExceptionInfo 389 | #"Dependency values MUST be an array or a JSON Schema" 390 | (schema/validate {"dependencies" {"a" nil}})))) 391 | 392 | (deftest property-names-test 393 | (is (schema/validate {"propertyNames" false})) 394 | (is (schema/validate {"propertyNames" true})) 395 | (is (thrown-with-msg? 396 | ExceptionInfo 397 | #"The value of 'propertyNames' MUST be a JSON Schema" 398 | (schema/validate {"propertyNames" nil}))) 399 | (is (thrown-with-msg? 400 | ExceptionInfo 401 | #"The value of 'propertyNames' MUST be a valid JSON Schema" 402 | (schema/validate {"propertyNames" {"type" "foo"}})))) 403 | 404 | (deftest if-test 405 | (is (thrown-with-msg? 406 | ExceptionInfo 407 | #"The value of 'if' MUST be a valid JSON Schema" 408 | (schema/validate {"if" {"type" "foo"}})))) 409 | 410 | (deftest then-test 411 | (is (thrown-with-msg? 412 | ExceptionInfo 413 | #"The value of 'then' MUST be a valid JSON Schema" 414 | (schema/validate {"then" {"type" "foo"}})))) 415 | 416 | (deftest else-test 417 | (is (thrown-with-msg? 418 | ExceptionInfo 419 | #"The value of 'else' MUST be a valid JSON Schema" 420 | (schema/validate {"else" {"type" "foo"}})))) 421 | 422 | (deftest all-of-test 423 | (is (thrown-with-msg? 424 | ExceptionInfo 425 | #"The value of 'allOf' MUST be a non-empty array" 426 | (schema/validate {"allOf" {"type" "string"}}))) 427 | (is (thrown-with-msg? 428 | ExceptionInfo 429 | #"The value of 'allOf' MUST be a non-empty array" 430 | (schema/validate {"allOf" []}))) 431 | (is (thrown-with-msg? 432 | ExceptionInfo 433 | #"Each item of an 'allOf' array MUST be a valid schema" 434 | (schema/validate {"allOf" [{"type" "foo"}]}))) 435 | (is (schema/validate {"allOf" [{"type" "string"}]}))) 436 | 437 | (deftest any-of-test 438 | (is (thrown-with-msg? 439 | ExceptionInfo 440 | #"The value of 'anyOf' MUST be a non-empty array" 441 | (schema/validate {"anyOf" {"type" "string"}}))) 442 | (is (thrown-with-msg? 443 | ExceptionInfo 444 | #"The value of 'anyOf' MUST be a non-empty array" 445 | (schema/validate {"anyOf" []}))) 446 | (is (thrown-with-msg? 447 | ExceptionInfo 448 | #"Each item of an 'anyOf' array MUST be a valid schema" 449 | (schema/validate {"anyOf" [{"type" "foo"}]}))) 450 | (is (schema/validate {"anyOf" [{"type" "string"}]}))) 451 | 452 | (deftest one-of-test 453 | (is (thrown-with-msg? 454 | ExceptionInfo 455 | #"The value of 'oneOf' MUST be a non-empty array" 456 | (schema/validate {"oneOf" {"type" "string"}}))) 457 | (is (thrown-with-msg? 458 | ExceptionInfo 459 | #"The value of 'oneOf' MUST be a non-empty array" 460 | (schema/validate {"oneOf" []}))) 461 | (is (thrown-with-msg? 462 | ExceptionInfo 463 | #"Each item of an 'oneOf' array MUST be a valid schema" 464 | (schema/validate {"oneOf" [{"type" "foo"}]}))) 465 | (is (schema/validate {"oneOf" [{"type" "string"}]}))) 466 | 467 | (deftest not-test 468 | (is (thrown-with-msg? 469 | ExceptionInfo 470 | #"The value of 'not' MUST be a valid JSON Schema" 471 | (schema/validate {"not" {"type" "foo"}}))) 472 | (is (schema/validate {"not" {"type" "string"}}))) 473 | 474 | (deftest format-test 475 | (is (thrown-with-msg? 476 | ExceptionInfo 477 | #"The value of a 'format' attribute MUST be a string" 478 | (schema/validate {"format" nil}))) 479 | (is (thrown-with-msg? 480 | ExceptionInfo 481 | #"The value of a 'format' attribute MUST be a string" 482 | (schema/validate {"format" []}))) 483 | (is (schema/validate {"format" "regex"}))) 484 | -------------------------------------------------------------------------------- /src/juxt/jinx/alpha/patterns.clj: -------------------------------------------------------------------------------- 1 | ;; Copyright © 2019, JUXT LTD. 2 | 3 | (ns juxt.jinx.alpha.patterns 4 | (:require 5 | [clojure.set :as set] 6 | [clojure.string :as str])) 7 | 8 | ;; The purpose of this namespace is to allow the accurate computation 9 | ;; of Java regex patterns. 10 | 11 | (defn matched [^java.util.regex.Pattern re ^CharSequence s] 12 | (let [m (re-matcher re s)] 13 | (when (.matches m) 14 | m))) 15 | 16 | (defn re-group-by-name [^java.util.regex.Matcher matcher ^String name] 17 | (when matcher 18 | (.group matcher name))) 19 | 20 | (defn partition-into-ranges-iter 21 | "Find consecutive number sequences. O(n)" 22 | [coll] 23 | (loop [[x & xs] (sort coll) 24 | subsequent 0 25 | curr [] 26 | ranges []] 27 | (if-not x 28 | (cond-> ranges (seq curr) (conj curr)) 29 | (if (= (inc subsequent) (int x)) 30 | (recur xs (int x) (conj curr x) ranges) 31 | (recur xs (int x) [x] (cond-> ranges (seq curr) (conj curr))))))) 32 | 33 | (defn partition-into-ranges-fj 34 | "Find consecutive number sequences. O(log n)" 35 | [coll] 36 | (let [consecutive? 37 | (fn [coll] 38 | (= (count coll) (inc (- (int (or (last coll) -1)) (int (first coll)))))) 39 | 40 | fork (fn fork [coll] 41 | (if (consecutive? coll) 42 | coll 43 | (let [midpoint (quot (count coll) 2)] 44 | [(fork (subvec coll 0 midpoint)) 45 | (fork (subvec coll midpoint))]))) 46 | 47 | join (fn join [[coll & colls]] 48 | (let [consecutive? (= (inc (int (last coll))) (int (or (ffirst colls) -1)))] 49 | (if consecutive? 50 | (join (cons (into coll (first colls)) (rest colls))) 51 | (if colls 52 | (cons coll (lazy-seq (join colls))) 53 | [coll]))))] 54 | 55 | (->> coll vec fork (tree-seq (comp sequential? first) seq) 56 | rest (filter (comp not sequential? first)) 57 | join))) 58 | 59 | (defprotocol RegExpressable 60 | (as-regex-str [_] "Return a string that represents the Java regex")) 61 | 62 | (defn int-range 63 | "Range between n1 (inclusive) and n2 (inclusive)" 64 | [n1 n2] 65 | (range (int n1) (inc (int n2)))) 66 | 67 | (def regex-chars 68 | (merge 69 | {(int \\) "\\\\" 70 | (int \u0009) "\\t" 71 | (int \u000A) "\\n" 72 | (int \u000D) "\\r" 73 | (int \u000C) "\\f" 74 | (int \u0007) "\\a" 75 | (int \u001B) "\\e"} 76 | (into {} (for [n (concat 77 | (int-range \A \Z) 78 | (int-range \a \z) 79 | (int-range \0 \9))] 80 | [n (str (char n))])))) 81 | 82 | (defn int->regex [n] 83 | (cond (< n 256) (get regex-chars n (format "\\x%02X" n)) 84 | (< n 65536) (format "\\u%04X" n) 85 | :else (format "\\x{%04X}" n))) 86 | 87 | (defn expand-with-character-classes 88 | "Take a collection of characters and return a string representing the 89 | concatenation of the Java regex characters, including the use 90 | character classes wherever possible without conformance loss. This 91 | function is not designed for performance and should be called to 92 | prepare systems prior to the handling of HTTP requests." 93 | [s] 94 | (let [{:keys [classes remaining]} 95 | (reduce 96 | (fn [{:keys [remaining] :as acc} {:keys [class set]}] 97 | (cond-> acc 98 | (set/subset? set remaining) (-> (update :classes conj class) 99 | (update :remaining set/difference set)))) 100 | {:remaining (set s) :classes []} 101 | 102 | [{:class "Alnum" :set (set (concat (int-range \A \Z) (int-range \a \z) (int-range \0 \9)))} 103 | {:class "Alpha" :set (set (concat (int-range \A \Z) (int-range \a \z)))} 104 | {:class "XDigit" :set (set (concat (int-range \0 \9) (int-range \A \F) (int-range \a \f)))} 105 | {:class "Digit" :set (set (int-range \0 \9))} 106 | {:class "Cntrl" :set (set (concat (int-range \u0000 \u001f) [(int \u007f)]))} 107 | {:class "Punct" :set (set (map int [\! \" \# \$ \% \& \' \( 108 | \) \* \+ \, \- \. \/ \: 109 | \; \< \= \> \? \@ \[ \\ 110 | \] \^ \_ \` \{ \| \} \~]))} 111 | {:class "Blank" :set (set (map int [\space \tab]))}])] 112 | 113 | 114 | (let [cs (concat 115 | (map #(format "\\p{%s}" %) classes) 116 | ;; Find ranges 117 | (map (fn [x] (if (> (count x) 1) 118 | (format "[%s-%s]" 119 | (int->regex (first x)) 120 | (int->regex (last x))) 121 | (int->regex (first x)))) 122 | (partition-into-ranges-iter remaining)))] 123 | (if (> (count cs) 1) 124 | (format "[%s]" (apply str cs)) 125 | (apply str cs))))) 126 | 127 | (extend-protocol RegExpressable 128 | clojure.lang.ISeq 129 | (as-regex-str [s] 130 | (expand-with-character-classes (map int s))) 131 | clojure.lang.PersistentVector 132 | (as-regex-str [s] 133 | (expand-with-character-classes (map int s))) 134 | String 135 | (as-regex-str [s] s) 136 | Character 137 | (as-regex-str [c] 138 | (int->regex (int c))) 139 | Integer 140 | (as-regex-str [n] 141 | (int->regex n)) 142 | Long 143 | (as-regex-str [n] 144 | (assert (<= n Integer/MAX_VALUE)) 145 | (int->regex (int n))) 146 | java.util.regex.Pattern 147 | (as-regex-str [re] 148 | (str re)) 149 | clojure.lang.PersistentHashSet 150 | (as-regex-str [s] 151 | (as-regex-str (seq s)))) 152 | 153 | (defn concatenate 154 | [& args] 155 | (re-pattern (apply str (map as-regex-str args)))) 156 | 157 | (defn compose [fmt & args] 158 | (re-pattern (apply format fmt (map as-regex-str args)))) 159 | 160 | ;; RFC 5234 B.1 161 | 162 | (def ALPHA (concat (int-range \A \Z) (int-range \a \z))) 163 | 164 | (def BIT [\0 \1]) 165 | 166 | (def CHAR (int-range 0x01 0x7F)) 167 | 168 | (def CR \return) 169 | 170 | (def CRLF (concatenate \return \newline)) 171 | 172 | (def CTL (conj (int-range 0x00 0x1F) 0x7F)) 173 | 174 | (def DIGIT (int-range \0 \9)) 175 | 176 | (def DQUOTE \") 177 | 178 | ;; HEXDIG includes lower-case. RFC 5234: "ABNF strings are case 179 | ;; insensitive and the character set for these strings is US-ASCII." 180 | (def HEXDIG (concat DIGIT (int-range \A \F) (int-range \a \f))) 181 | 182 | (def HTAB \tab) 183 | 184 | (def LF \newline) 185 | 186 | (def OCTET (int-range 0x00 0xFF)) 187 | 188 | (def SP \space) 189 | 190 | (def WSP [\space \tab]) 191 | 192 | 193 | ;; Useful 194 | 195 | (def COLON 0x3A) 196 | (def QUESTION-MARK 0x3F) 197 | (def PERIOD 0x2E) 198 | 199 | 200 | ;; RFC 1034, Section 3.1: Name space specifications and terminology 201 | 202 | (def ldh-str (compose "%s*" (concat ALPHA DIGIT [\-]))) 203 | 204 | (def label (compose "%s(?:%s?%s)?" ALPHA ldh-str (concat ALPHA DIGIT))) 205 | 206 | (def subdomain (compose "%s(?:%s%s)*" label PERIOD label)) 207 | 208 | ;; RFC 3986, Appendix A. Collected ABNF for URI 209 | 210 | (def dec-octet (compose "(?:%s|%s|%s|%s|%s)" 211 | DIGIT 212 | (concatenate (int-range 0x31 0x39) DIGIT) 213 | (concatenate \1 DIGIT DIGIT) 214 | (concatenate \2 (int-range 0x30 0x34) DIGIT) 215 | (concatenate \2 \5 (int-range 0x30 0x35)))) 216 | 217 | (def IPv4address (compose "%s%s%s%s%s%s%s" dec-octet PERIOD dec-octet PERIOD dec-octet PERIOD dec-octet)) 218 | 219 | (def h16 (compose "%s{1,4}" HEXDIG)) 220 | 221 | (def ls32 (compose "(?:%s%s%s|%s)" h16 COLON h16 IPv4address)) 222 | 223 | ;; For ease of debugging 224 | ;; 6( h16 ":" ) ls32 225 | (def IPv6-1 (compose "(?:%s:){6}%s" h16 ls32)) 226 | 227 | ;; "::" 5( h16 ":" ) ls32 228 | (def IPv6-2 (compose "::(?:%s:){5}%s" h16 ls32)) 229 | 230 | ;; [ h16 ] "::" 4( h16 ":" ) ls32 231 | (def IPv6-3 (compose "%s?::(?:%s:){4}%s" h16 h16 ls32)) 232 | 233 | ;; [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 234 | (def IPv6-4 (compose "(?:(?:%s:){0,1}%s)?::(?:%s:){3}%s" h16 h16 h16 ls32)) 235 | 236 | ;; [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 237 | (def IPv6-5 (compose "(?:(?:%s:){0,2}%s)?::(?:%s:){2}%s" h16 h16 h16 ls32)) 238 | 239 | ;; [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 240 | (def IPv6-6 (compose "(?:(?:%s:){0,3}%s)?::%s:%s" h16 h16 h16 ls32)) 241 | 242 | ;; [ *4( h16 ":" ) h16 ] "::" ls32 243 | (def IPv6-7 (compose "(?:(?:%s:){0,4}%s)?::%s" h16 h16 ls32)) 244 | 245 | ;; [ *5( h16 ":" ) h16 ] "::" h16 246 | (def IPv6-8 (compose "(?:(?:%s:){0,5}%s)?::%s" h16 h16 h16)) 247 | 248 | ;; [ *6( h16 ":" ) h16 ] "::" 249 | (def IPv6-9 (compose "(?:(?:%s:){0,6}%s)?::" h16 h16)) 250 | 251 | (def IPv6address 252 | (compose 253 | "(?:%s|%s|%s|%s|%s|%s|%s|%s|%s)" 254 | IPv6-1 IPv6-2 IPv6-3 IPv6-4 IPv6-5 IPv6-6 IPv6-7 IPv6-8 IPv6-9)) 255 | 256 | (def gen-delims [\: \/ \? \# \[ \] \@]) 257 | 258 | (def unreserved (concat ALPHA DIGIT [\- \. \_ \~])) 259 | 260 | (def sub-delims [\! \$ \& \' \( \) \* \+ \, \; \=]) 261 | 262 | (def IPvFuture (compose "v[%s]+%s(?:%s|%s|%s)+" HEXDIG PERIOD unreserved sub-delims COLON)) 263 | 264 | (def IP-literal (compose "%s(?:%s|%s)%s" \[ IPv6address IPvFuture \])) 265 | 266 | (def scheme (compose "%s[%s]*" ALPHA (set (concat ALPHA DIGIT [\+ \- \.])))) 267 | 268 | (def pct-encoded (concatenate \% HEXDIG HEXDIG)) 269 | 270 | (def userinfo (compose "(?(?:%s|%s|%s|%s)*)" unreserved pct-encoded sub-delims ":")) 271 | 272 | (def reg-name (compose "(?:%s|%s|%s)*" unreserved pct-encoded sub-delims)) 273 | 274 | (def host (compose "(?%s|%s|%s)" IP-literal IPv4address reg-name)) 275 | 276 | (def port (compose "(?(?:%s)*)" DIGIT)) 277 | 278 | (def authority (compose (str "(?" (str "(?:%s@)?" "(?:%s)" "(?:%s%s)?") ")") userinfo host COLON port)) 279 | 280 | (def segment (compose "(?:%s|%s|%s|%s|%s)*" unreserved pct-encoded sub-delims \: \@)) 281 | (def segment-nz (compose "(?:%s|%s|%s|%s|%s)+" unreserved pct-encoded sub-delims \: \@)) 282 | (def segment-nz-nc (compose "(?:%s|%s|%s|%s)+" unreserved pct-encoded sub-delims \@)) 283 | 284 | (def path-abempty (compose "(?(?:/%s)*)" segment)) 285 | (def path-absolute (compose "/%s(?:/%s)*" segment-nz segment)) 286 | (def path-noscheme (compose "%s(?:/%s)*" segment-nz-nc segment)) 287 | (def path-rootless (compose "%s(?:/%s)*" segment-nz segment)) 288 | (def path-empty "") 289 | 290 | (def hier-part (compose "(?://%s%s|%s|%s|%s)" authority path-abempty path-absolute path-rootless path-empty)) 291 | 292 | (def pchar (compose "(?:%s|%s|%s|%s|%s)" unreserved pct-encoded sub-delims \: \@)) 293 | 294 | (def query (compose "(?:%s|%s|%s)*" pchar \/ \?)) 295 | 296 | (def fragment (compose "(?:%s|%s|%s)*" pchar \/ \?)) 297 | 298 | (def URI (compose "(?%s):(?:%s)(?:%s(?%s))?(?:#(?%s))?" scheme hier-part QUESTION-MARK query fragment)) 299 | 300 | (def relative-part (compose "(?://%s%s|%s|%s|%s)" 301 | authority path-abempty path-absolute 302 | path-noscheme path-empty)) 303 | 304 | (def relative-ref (compose "%s(?:%s(?%s))?(?:#(?%s))?" relative-part QUESTION-MARK query fragment)) 305 | 306 | ;; RFC 3987 307 | 308 | (def ucschar 309 | (set (concat (int-range 0xA0 0xD7FF) 310 | (int-range 0xF900 0xFDCF) 311 | (int-range 0xFDF0 0xFFEF) 312 | 313 | ;; These higher code-points that lie outside the BMP 314 | ;; are significantly impacting compile performance. We 315 | ;; need to be able to do partition-into-ranges in a 316 | ;; much more performant way. Perhap with intervals 317 | ;; rather than brute-force expansion of ranges into 318 | ;; sequences of ints. 319 | 320 | ;;(int-range 0x10000 0x1FFFD) 321 | ;;(int-range 0x20000 0x2FFFD) 322 | ;;(int-range 0x30000 0x3FFFD) 323 | ;;(int-range 0x40000 0x4FFFD) 324 | ;;(int-range 0x50000 0x5FFFD) 325 | ;;(int-range 0x60000 0x6FFFD) 326 | ;;(int-range 0x70000 0x7FFFD) 327 | ;;(int-range 0x80000 0x8FFFD) 328 | ;;(int-range 0x90000 0x9FFFD) 329 | ;;(int-range 0xA0000 0xAFFFD) 330 | ;;(int-range 0xB0000 0xBFFFD) 331 | ;;(int-range 0xC0000 0xCFFFD) 332 | ;;(int-range 0xD0000 0xDFFFD) 333 | ;;(int-range 0xE1000 0xEFFFD) 334 | ))) 335 | 336 | 337 | 338 | (comment ;; Example of a higher code-point - a Chinese character 339 | (String. (int-array [0x2F81A]) 0 1)) 340 | 341 | (def iunreserved (concat ALPHA DIGIT [\- \. \_ \~] ucschar)) 342 | 343 | (def iuserinfo (compose "(?(?:%s|%s|%s|%s)*)" iunreserved pct-encoded sub-delims ":")) 344 | 345 | (def ireg-name (compose "(?:%s|%s|%s)*" iunreserved pct-encoded sub-delims)) 346 | 347 | (def ihost (compose "(?%s|%s|%s)" IP-literal IPv4address ireg-name)) 348 | 349 | (def iauthority (compose (str "(?" 350 | (str "(?:%s@)?" 351 | "(?:%s)" 352 | "(?:%s%s)?") ")") iuserinfo ihost COLON port)) 353 | 354 | (def ipchar (compose "(?:%s|%s|%s|%s|%s)" iunreserved pct-encoded sub-delims \: \@)) 355 | 356 | (def isegment (compose "(?:%s|%s|%s|%s|%s)*" iunreserved pct-encoded sub-delims \: \@)) 357 | (def isegment-nz (compose "(?:%s|%s|%s|%s|%s)+" iunreserved pct-encoded sub-delims \: \@)) 358 | (def isegment-nz-nc (compose "(?:%s|%s|%s|%s)+" iunreserved pct-encoded sub-delims \@)) 359 | 360 | (def ipath-abempty (compose "(?(?:/%s)*)" isegment)) 361 | (def ipath-absolute (compose "/%s(?:/%s)*" isegment-nz isegment)) 362 | (def ipath-noscheme (compose "%s(?:/%s)*" isegment-nz-nc isegment)) 363 | (def ipath-rootless (compose "%s(?:/%s)*" isegment-nz isegment)) 364 | (def ipath-empty "") 365 | 366 | (def ihier-part (compose "(?://%s%s|%s|%s|%s)" iauthority ipath-abempty ipath-absolute ipath-rootless ipath-empty)) 367 | 368 | 369 | (def iprivate (concat (int-range 0xE000 0xF8FF) 370 | ;;(int-range 0xF0000 0xFFFFD) 371 | ;;(int-range 0x100000 0x10FFFD) 372 | )) 373 | 374 | 375 | (def iquery (compose "(?:%s|%s|%s|%s)*" ipchar iprivate \/ \?)) 376 | 377 | (def ifragment (compose "(?:%s|%s|%s)*" ipchar \/ \?)) 378 | 379 | 380 | (def IRI (compose "(?%s):(?:%s)(?:%s(?%s))?(?:#(?%s))?" scheme ihier-part QUESTION-MARK iquery ifragment)) 381 | 382 | (def irelative-part (compose "(?://%s%s|%s|%s|%s)" 383 | iauthority ipath-abempty ipath-absolute 384 | ipath-noscheme ipath-empty)) 385 | 386 | (def irelative-ref (compose "%s(?:%s(?%s))?(?:#(?%s))?" irelative-part QUESTION-MARK iquery ifragment)) 387 | 388 | ;; Can't do this AND have named groups - better to ask app logic to 389 | ;; ask if a string is either an IRI or a irelative-ref 390 | 391 | #_(def IRI-reference (compose "(?:%s|%s)" IRI irelative-ref)) 392 | 393 | (comment 394 | (re-group-by-name (matched IRI "https://jon:pither@juxt.pro/malcolm?foo#bar") "host")) 395 | 396 | 397 | ;; RFC 5322, Section 3.2.3 398 | 399 | (def atext (concat ALPHA DIGIT [\! \# \$ \% \& \' \* \+ \- \/ \= \? \^ \_ \` \{ \| \} \~])) 400 | 401 | ;(def rfc5322_atom (compose "[%s]+" (as-regex-str atext))) 402 | 403 | (def dot-atom-text (compose "[%s]+(?:\\.[%s]+)*" (as-regex-str atext) (as-regex-str atext))) 404 | 405 | ;; RFC 5322, Section 3.4.1 406 | 407 | (def domain dot-atom-text) 408 | 409 | (def addr-spec (compose "(?%s)@(?%s)" dot-atom-text domain)) 410 | 411 | (comment 412 | (re-matches addr-spec "mal@juxt.pro")) 413 | 414 | ;; RFC 6532: Internationalized Email Headers 415 | 416 | ;; We define UTF8-non-ascii, then use that to extent atext above, as per Section 3.2 417 | 418 | (def UTF8-tail (int-range 0x80 0xBF)) 419 | 420 | (def UTF8-2 (compose "[%s]%s" (int-range 0xC2 0xDF) UTF8-tail)) 421 | 422 | (def UTF8-3 (compose 423 | "(?:%s[%s]%s|[%s](?:%s){2}|%s[%s]%s|[%s](?:%s){2})" 424 | 0xE0 (int-range 0xA0 0xBF) UTF8-tail 425 | (int-range 0xE1 0xEC) UTF8-tail 426 | 0xED (int-range 0x80 0x9F) UTF8-tail 427 | (int-range 0xEE 0xEF) UTF8-tail)) 428 | 429 | (def UTF8-4 (compose 430 | "(?:%s[%s](?:%s){2}|[%s](?:%s){3}|%s[%s](?:%s){2})" 431 | 0xF0 (int-range 0x90 0xBF) UTF8-tail 432 | (int-range 0xF1 0xF3) UTF8-tail 433 | 0xF4 (int-range 0x80 0x8F) UTF8-tail)) 434 | 435 | (def UTF8-non-ascii (compose "(?:%s|%s|%s)" UTF8-2 UTF8-3 UTF8-4)) 436 | 437 | (def iatext (compose "(?:%s|%s)" atext UTF8-non-ascii)) 438 | 439 | (def idot-atom-text (compose "[%s]+(?:\\.[%s]+)*" (as-regex-str iatext) (as-regex-str iatext))) 440 | 441 | (def idomain idot-atom-text) 442 | 443 | (def iaddr-spec (compose "(?%s)@(?%s)" idot-atom-text idomain)) 444 | 445 | ;; TODO: Normalize as per iaddr-spec 3.1. UTF-8 Syntax and Normalization 446 | 447 | 448 | ;; RFC 6901: JavaScript Object Notation (JSON) Pointer 449 | 450 | (def unescaped (concat (int-range 0x00 0x2E) 451 | (int-range 0x30 0x7D) 452 | ;; Should be this: 453 | #_(int-range 0x7F 0x10FFFF) 454 | ;; but too slow, so do this instead for now: 455 | (int-range 0x7F 0xFFFF))) 456 | 457 | 458 | (def referenced-token (compose "(?:[%s]|~0|~1)*" unescaped)) 459 | 460 | (def json-pointer (compose "(?:/%s)*" referenced-token)) 461 | 462 | ;; draft-handrews-relative-json-pointer-01 463 | (def non-negative-integer (compose "(?:%s|%s%s*)" \0 (int-range \1 \9) (int-range \0 \9))) 464 | 465 | (def relative-json-pointer (compose "%s(?:#|%s)" non-negative-integer json-pointer)) 466 | 467 | 468 | ;; TODO: Define U-label (RFC 5890, Section 2.3.2.1) 469 | 470 | ;; RFC 6570: URI Template 471 | -------------------------------------------------------------------------------- /spec/rfc6532: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Internet Engineering Task Force (IETF) A. Yang 8 | Request for Comments: 6532 TWNIC 9 | Obsoletes: 5335 S. Steele 10 | Updates: 2045 Microsoft 11 | Category: Standards Track N. Freed 12 | ISSN: 2070-1721 Oracle 13 | February 2012 14 | 15 | 16 | Internationalized Email Headers 17 | 18 | Abstract 19 | 20 | Internet mail was originally limited to 7-bit ASCII. MIME added 21 | support for the use of 8-bit character sets in body parts, and also 22 | defined an encoded-word construct so other character sets could be 23 | used in certain header field values. However, full 24 | internationalization of electronic mail requires additional 25 | enhancements to allow the use of Unicode, including characters 26 | outside the ASCII repertoire, in mail addresses as well as direct use 27 | of Unicode in header fields like "From:", "To:", and "Subject:", 28 | without requiring the use of complex encoded-word constructs. This 29 | document specifies an enhancement to the Internet Message Format and 30 | to MIME that allows use of Unicode in mail addresses and most header 31 | field content. 32 | 33 | This specification updates Section 6.4 of RFC 2045 to eliminate the 34 | restriction prohibiting the use of non-identity content-transfer- 35 | encodings on subtypes of "message/". 36 | 37 | Status of This Memo 38 | 39 | This is an Internet Standards Track document. 40 | 41 | This document is a product of the Internet Engineering Task Force 42 | (IETF). It represents the consensus of the IETF community. It has 43 | received public review and has been approved for publication by the 44 | Internet Engineering Steering Group (IESG). Further information on 45 | Internet Standards is available in Section 2 of RFC 5741. 46 | 47 | Information about the current status of this document, any errata, 48 | and how to provide feedback on it may be obtained at 49 | http://www.rfc-editor.org/info/rfc6532. 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Yang, et al. Standards Track [Page 1] 59 | 60 | RFC 6532 Internationalized Email Headers February 2012 61 | 62 | 63 | Copyright Notice 64 | 65 | Copyright (c) 2012 IETF Trust and the persons identified as the 66 | document authors. All rights reserved. 67 | 68 | This document is subject to BCP 78 and the IETF Trust's Legal 69 | Provisions Relating to IETF Documents 70 | (http://trustee.ietf.org/license-info) in effect on the date of 71 | publication of this document. Please review these documents 72 | carefully, as they describe your rights and restrictions with respect 73 | to this document. Code Components extracted from this document must 74 | include Simplified BSD License text as described in Section 4.e of 75 | the Trust Legal Provisions and are provided without warranty as 76 | described in the Simplified BSD License. 77 | 78 | Table of Contents 79 | 80 | 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 3 81 | 2. Terminology Used in This Specification . . . . . . . . . . . . 3 82 | 3. Changes to Message Header Fields . . . . . . . . . . . . . . . 4 83 | 3.1. UTF-8 Syntax and Normalization . . . . . . . . . . . . . . 4 84 | 3.2. Syntax Extensions to RFC 5322 . . . . . . . . . . . . . . 5 85 | 3.3. Use of 8-bit UTF-8 in Message-IDs . . . . . . . . . . . . 5 86 | 3.4. Effects on Line Length Limits . . . . . . . . . . . . . . 5 87 | 3.5. Changes to MIME Message Type Encoding Restrictions . . . . 6 88 | 3.6. Use of MIME Encoded-Words . . . . . . . . . . . . . . . . 6 89 | 3.7. The message/global Media Type . . . . . . . . . . . . . . 7 90 | 4. Security Considerations . . . . . . . . . . . . . . . . . . . 8 91 | 5. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 9 92 | 6. Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . 9 93 | 7. References . . . . . . . . . . . . . . . . . . . . . . . . . . 10 94 | 7.1. Normative References . . . . . . . . . . . . . . . . . . . 10 95 | 7.2. Informative References . . . . . . . . . . . . . . . . . . 10 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Yang, et al. Standards Track [Page 2] 115 | 116 | RFC 6532 Internationalized Email Headers February 2012 117 | 118 | 119 | 1. Introduction 120 | 121 | Internet mail distinguishes a message from its transport and further 122 | divides a message between a header and a body [RFC5322]. Internet 123 | mail header field values contain a variety of strings that are 124 | intended to be user-visible. The range of supported characters for 125 | these strings was originally limited to [ASCII] in 7-bit form. MIME 126 | [RFC2045] [RFC2046] [RFC2047] provides the ability to use additional 127 | character sets, but this support is limited to body part data and to 128 | special encoded-word constructs that were only allowed in a limited 129 | number of places in header field values. 130 | 131 | Globalization of the Internet requires support of the much larger set 132 | of characters provided by Unicode [RFC5198] in both mail addresses 133 | and most header field values. Additionally, complex encoding schemes 134 | like encoded-words introduce inefficiencies as well as significant 135 | opportunities for processing errors. And finally, native support for 136 | the UTF-8 charset is now available on most systems. Hence, it is 137 | strongly desirable for Internet mail to support UTF-8 [RFC3629] 138 | directly. 139 | 140 | This document specifies an enhancement to the Internet Message Format 141 | [RFC5322] and to MIME that permits the direct use of UTF-8, rather 142 | than only ASCII, in header field values, including mail addresses. A 143 | new media type, message/global, is defined for messages that use this 144 | extended format. This specification also lifts the MIME restriction 145 | on having non-identity content-transfer-encodings on any subtype of 146 | the message top-level type so that message/global parts can be safely 147 | transmitted across existing mail infrastructure. 148 | 149 | This specification is based on a model of native, end-to-end support 150 | for UTF-8, which depends on having an "8-bit-clean" environment 151 | assured by the transport system. Support for carriage across legacy, 152 | 7-bit infrastructure and for processing by 7-bit receivers requires 153 | additional mechanisms that are not provided by these specifications. 154 | 155 | This specification is a revision of and replacement for [RFC5335]. 156 | Section 6 of [RFC6530] describes the change in approach between this 157 | specification and the previous version. 158 | 159 | 2. Terminology Used in This Specification 160 | 161 | A plain ASCII string is fully compatible with [RFC5321] and 162 | [RFC5322]. In this document, non-ASCII strings are UTF-8 strings if 163 | they are in header field values that contain at least one 164 | (see Section 3.1). 165 | 166 | 167 | 168 | 169 | 170 | Yang, et al. Standards Track [Page 3] 171 | 172 | RFC 6532 Internationalized Email Headers February 2012 173 | 174 | 175 | Unless otherwise noted, all terms used here are defined in [RFC5321], 176 | [RFC5322], [RFC6530], or [RFC6531]. 177 | 178 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 179 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this 180 | document are to be interpreted as described in [RFC2119]. 181 | 182 | The term "8-bit" means octets are present in the data with values 183 | above 0x7F. 184 | 185 | 3. Changes to Message Header Fields 186 | 187 | To permit non-ASCII Unicode characters in field values, the header 188 | definition in [RFC5322] is extended to support the new format. The 189 | following sections specify the necessary changes to RFC 5322's ABNF. 190 | 191 | The syntax rules not mentioned below remain defined as in [RFC5322]. 192 | 193 | Note that this protocol does not change rules in RFC 5322 for 194 | defining header field names. The bodies of header fields are allowed 195 | to contain Unicode characters, but the header field names themselves 196 | must consist of ASCII characters only. 197 | 198 | Also note that messages in this format require the use of the 199 | SMTPUTF8 extension [RFC6531] to be transferred via SMTP. 200 | 201 | 3.1. UTF-8 Syntax and Normalization 202 | 203 | UTF-8 characters can be defined in terms of octets using the 204 | following ABNF [RFC5234], taken from [RFC3629]: 205 | 206 | UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4 207 | 208 | UTF8-2 = 209 | 210 | UTF8-3 = 211 | 212 | UTF8-4 = 213 | 214 | See [RFC5198] for a discussion of Unicode normalization; 215 | normalization form NFC [UNF] SHOULD be used. Actually, if one is 216 | going to do internationalization properly, one of the most often 217 | cited goals is to permit people to spell their names correctly. 218 | Since many mailbox local parts reflect personal names, that principle 219 | applies to mailboxes as well. The NFKC normalization form [UNF] 220 | SHOULD NOT be used because it may lose information that is needed to 221 | correctly spell some names in some unusual circumstances. 222 | 223 | 224 | 225 | 226 | Yang, et al. Standards Track [Page 4] 227 | 228 | RFC 6532 Internationalized Email Headers February 2012 229 | 230 | 231 | 3.2. Syntax Extensions to RFC 5322 232 | 233 | The following rules extend the ABNF syntax defined in [RFC5322] and 234 | [RFC5234] in order to allow UTF-8 content. 235 | 236 | VCHAR =/ UTF8-non-ascii 237 | 238 | ctext =/ UTF8-non-ascii 239 | 240 | atext =/ UTF8-non-ascii 241 | 242 | qtext =/ UTF8-non-ascii 243 | 244 | text =/ UTF8-non-ascii 245 | ; note that this upgrades the body to UTF-8 246 | 247 | dtext =/ UTF8-non-ascii 248 | 249 | The preceding changes mean that the following constructs now allow 250 | UTF-8: 251 | 252 | 1. Unstructured text, used in header fields like "Subject:" or 253 | "Content-description:". 254 | 255 | 2. Any construct that uses atoms, including but not limited to the 256 | local parts of addresses and Message-IDs. This includes 257 | addresses in the "for" clauses of "Received:" header fields. 258 | 259 | 3. Quoted strings. 260 | 261 | 4. Domains. 262 | 263 | Note that header field names are not on this list; these are still 264 | restricted to ASCII. 265 | 266 | 3.3. Use of 8-bit UTF-8 in Message-IDs 267 | 268 | Implementers of Message-ID generation algorithms MAY prefer to 269 | restrain their output to ASCII since that has some advantages, such 270 | as when constructing "In-reply-to:" and "References:" header fields 271 | in mailing-list threads where some senders use internationalized 272 | addresses and others do not. 273 | 274 | 3.4. Effects on Line Length Limits 275 | 276 | Section 2.1.1 of [RFC5322] limits lines to 998 characters and 277 | recommends that the lines be restricted to only 78 characters. This 278 | specification changes the former limit to 998 octets. (Note that, in 279 | 280 | 281 | 282 | Yang, et al. Standards Track [Page 5] 283 | 284 | RFC 6532 Internationalized Email Headers February 2012 285 | 286 | 287 | ASCII, octets and characters are effectively the same, but this is 288 | not true in UTF-8.) The 78-character limit remains defined in terms 289 | of characters, not octets, since it is intended to address display 290 | width issues, not line-length issues. 291 | 292 | 3.5. Changes to MIME Message Type Encoding Restrictions 293 | 294 | This specification updates Section 6.4 of [RFC2045]. [RFC2045] 295 | prohibits applying a content-transfer-encoding to any subtypes of 296 | "message/". This specification relaxes that rule -- it allows newly 297 | defined MIME types to permit content-transfer-encoding, and it allows 298 | content-transfer-encoding for message/global (see Section 3.7). 299 | 300 | Background: Normally, transfer of message/global will be done in 301 | 8-bit-clean channels, and body parts will have "identity" encodings, 302 | that is, no decoding is necessary. 303 | 304 | But in the case where a message containing a message/global is 305 | downgraded from 8-bit to 7-bit as described in [RFC6152], an encoding 306 | might have to be applied to the message. If the message travels 307 | multiple times between a 7-bit environment and an environment 308 | implementing these extensions, multiple levels of encoding may occur. 309 | This is expected to be rarely seen in practice, and the potential 310 | complexity of other ways of dealing with the issue is thought to be 311 | larger than the complexity of allowing nested encodings where 312 | necessary. 313 | 314 | 3.6. Use of MIME Encoded-Words 315 | 316 | The MIME encoded-words facility [RFC2047] provides the ability to 317 | place non-ASCII text, but only in a subset of the places allowed by 318 | this extension. Additionally, encoded-words are substantially more 319 | complex since they allow the use of arbitrary charsets. Accordingly, 320 | encoded-words SHOULD NOT be used when generating header fields for 321 | messages employing this extension. Agents MAY, when incorporating 322 | material from another message, convert encoded-word use to direct use 323 | of UTF-8. 324 | 325 | Note that care must be taken when decoding encoded-words because the 326 | results after replacing an encoded-word with its decoded equivalent 327 | in UTF-8 may be syntactically invalid. Processors that elect to 328 | decode encoded-words MUST NOT generate syntactically invalid fields. 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Yang, et al. Standards Track [Page 6] 339 | 340 | RFC 6532 Internationalized Email Headers February 2012 341 | 342 | 343 | 3.7. The message/global Media Type 344 | 345 | Internationalized messages in this format MUST only be transmitted as 346 | authorized by [RFC6531] or within a non-SMTP environment that 347 | supports these messages. A message is a "message/global message" if: 348 | 349 | o it contains 8-bit UTF-8 header values as specified in this 350 | document, or 351 | 352 | o it contains 8-bit UTF-8 values in the header fields of body parts. 353 | 354 | The content of a message/global part is otherwise identical to that 355 | of a message/rfc822 part. 356 | 357 | If an object of this type is sent to a 7-bit-only system, it MUST 358 | have an appropriate content-transfer-encoding applied. (Note that a 359 | system compliant with MIME that doesn't recognize message/global is 360 | supposed to treat it as "application/octet-stream" as described in 361 | Section 5.2.4 of [RFC2046].) 362 | 363 | The registration is as follows: 364 | 365 | Type name: message 366 | 367 | Subtype name: global 368 | 369 | Required parameters: none 370 | 371 | Optional parameters: none 372 | 373 | Encoding considerations: Any content-transfer-encoding is permitted. 374 | The 8-bit or binary content-transfer-encodings are recommended 375 | where permitted. 376 | 377 | Security considerations: See Section 4. 378 | 379 | Interoperability considerations: This media type provides 380 | functionality similar to the message/rfc822 content type for email 381 | messages with internationalized email headers. When there is a 382 | need to embed or return such content in another message, there is 383 | generally an option to use this media type and leave the content 384 | unchanged or down-convert the content to message/rfc822. Each of 385 | these choices will interoperate with the installed base, but with 386 | different properties. Systems unaware of internationalized 387 | headers will typically treat a message/global body part as an 388 | unknown attachment, while they will understand the structure of a 389 | message/rfc822. However, systems that understand message/global 390 | 391 | 392 | 393 | 394 | Yang, et al. Standards Track [Page 7] 395 | 396 | RFC 6532 Internationalized Email Headers February 2012 397 | 398 | 399 | will provide functionality superior to the result of a down- 400 | conversion to message/rfc822. The most interoperable choice 401 | depends on the deployed software. 402 | 403 | Published specification: RFC 6532 404 | 405 | Applications that use this media type: SMTP servers and email 406 | clients that support multipart/report generation or parsing. 407 | Email clients that forward messages with internationalized headers 408 | as attachments. 409 | 410 | Additional information: 411 | 412 | Magic number(s): none 413 | 414 | File extension(s): The extension ".u8msg" is suggested. 415 | 416 | Macintosh file type code(s): A uniform type identifier (UTI) of 417 | "public.utf8-email-message" is suggested. This conforms to 418 | "public.message" and "public.composite-content", but does not 419 | necessarily conform to "public.utf8-plain-text". 420 | 421 | Person & email address to contact for further information: See the 422 | Authors' Addresses section of this document. 423 | 424 | Intended usage: COMMON 425 | 426 | Restrictions on usage: This is a structured media type that embeds 427 | other MIME media types. An 8-bit or binary content-transfer- 428 | encoding SHOULD be used unless this media type is sent over a 429 | 7-bit-only transport. 430 | 431 | Author: See the Authors' Addresses section of this document. 432 | 433 | Change controller: IETF Standards Process 434 | 435 | 4. Security Considerations 436 | 437 | Because UTF-8 often requires several octets to encode a single 438 | character, internationalization may cause header field values (in 439 | general) and mail addresses (in particular) to become longer. As 440 | specified in [RFC5322], each line of characters MUST be no more than 441 | 998 octets, excluding the CRLF. On the other hand, MDA (Mail 442 | Delivery Agent) processes that parse, store, or handle email 443 | addresses or local parts must take extra care not to overflow 444 | buffers, truncate addresses, or exceed storage allotments. Also, 445 | they must take care, when comparing, to use the entire lengths of the 446 | addresses. 447 | 448 | 449 | 450 | Yang, et al. Standards Track [Page 8] 451 | 452 | RFC 6532 Internationalized Email Headers February 2012 453 | 454 | 455 | There are lots of ways to use UTF-8 to represent something equivalent 456 | or similar to a particular displayed character or group of 457 | characters; see the security considerations in [RFC3629] for details 458 | on the problems this can cause. The normalization process described 459 | in Section 3.1 is recommended to minimize these issues. 460 | 461 | The security impact of UTF-8 headers on email signature systems such 462 | as Domain Keys Identified Mail (DKIM), S/MIME, and OpenPGP is 463 | discussed in Section 14 of [RFC6530]. 464 | 465 | If a user has a non-ASCII mailbox address and an ASCII mailbox 466 | address, a digital certificate that identifies that user might have 467 | both addresses in the identity. Having multiple email addresses as 468 | identities in a single certificate is already supported in PKIX 469 | (Public Key Infrastructure using X.509) [RFC5280] and OpenPGP 470 | [RFC3156], but there may be user-interface issues associated with the 471 | introduction of UTF-8 into addresses in this context. 472 | 473 | 5. IANA Considerations 474 | 475 | IANA has updated the registration of the message/global MIME type 476 | using the registration form contained in Section 3.7. 477 | 478 | 6. Acknowledgements 479 | 480 | This document incorporates many ideas first described in a draft 481 | document by Paul Hoffman, although many details have changed from 482 | that earlier work. 483 | 484 | The authors especially thank Jeff Yeh for his efforts and 485 | contributions on editing previous versions. 486 | 487 | Most of the content of this document was provided by John C Klensin 488 | and Dave Crocker. Significant comments and suggestions were received 489 | from Martin Duerst, Julien Elie, Arnt Gulbrandsen, Kristin Hubner, 490 | Kari Hurtta, Yangwoo Ko, Charles H. Lindsey, Alexey Melnikov, Chris 491 | Newman, Pete Resnick, Yoshiro Yoneya, and additional members of the 492 | Joint Engineering Team (JET) and were incorporated into the document. 493 | The authors wish to sincerely thank them all for their contributions. 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | Yang, et al. Standards Track [Page 9] 507 | 508 | RFC 6532 Internationalized Email Headers February 2012 509 | 510 | 511 | 7. References 512 | 513 | 7.1. Normative References 514 | 515 | [ASCII] "Coded Character Set -- 7-bit American Standard Code for 516 | Information Interchange", ANSI X3.4, 1986. 517 | 518 | [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate 519 | Requirement Levels", BCP 14, RFC 2119, March 1997. 520 | 521 | [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO 522 | 10646", STD 63, RFC 3629, November 2003. 523 | 524 | [RFC5198] Klensin, J. and M. Padlipsky, "Unicode Format for Network 525 | Interchange", RFC 5198, March 2008. 526 | 527 | [RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax 528 | Specifications: ABNF", STD 68, RFC 5234, January 2008. 529 | 530 | [RFC5321] Klensin, J., "Simple Mail Transfer Protocol", RFC 5321, 531 | October 2008. 532 | 533 | [RFC5322] Resnick, P., Ed., "Internet Message Format", RFC 5322, 534 | October 2008. 535 | 536 | [RFC6530] Klensin, J. and Y. Ko, "Overview and Framework for 537 | Internationalized Email", RFC 6530, February 2012. 538 | 539 | [RFC6531] Yao, J. and W. Mao, "SMTP Extension for Internationalized 540 | Email", RFC 6531, February 2012. 541 | 542 | [UNF] Davis, M. and K. Whistler, "Unicode Standard Annex #15: 543 | Unicode Normalization Forms", September 2010, 544 | . 545 | 546 | 7.2. Informative References 547 | 548 | [RFC2045] Freed, N. and N. Borenstein, "Multipurpose Internet Mail 549 | Extensions (MIME) Part One: Format of Internet Message 550 | Bodies", RFC 2045, November 1996. 551 | 552 | [RFC2046] Freed, N. and N. Borenstein, "Multipurpose Internet Mail 553 | Extensions (MIME) Part Two: Media Types", RFC 2046, 554 | November 1996. 555 | 556 | [RFC2047] Moore, K., "MIME (Multipurpose Internet Mail Extensions) 557 | Part Three: Message Header Extensions for Non-ASCII Text", 558 | RFC 2047, November 1996. 559 | 560 | 561 | 562 | Yang, et al. Standards Track [Page 10] 563 | 564 | RFC 6532 Internationalized Email Headers February 2012 565 | 566 | 567 | [RFC3156] Elkins, M., Del Torto, D., Levien, R., and T. Roessler, 568 | "MIME Security with OpenPGP", RFC 3156, August 2001. 569 | 570 | [RFC5280] Cooper, D., Santesson, S., Farrell, S., Boeyen, S., 571 | Housley, R., and W. Polk, "Internet X.509 Public Key 572 | Infrastructure Certificate and Certificate Revocation List 573 | (CRL) Profile", RFC 5280, May 2008. 574 | 575 | [RFC5335] Yang, A., "Internationalized Email Headers", RFC 5335, 576 | September 2008. 577 | 578 | [RFC6152] Klensin, J., Freed, N., Rose, M., and D. Crocker, "SMTP 579 | Service Extension for 8-bit MIME Transport", STD 71, 580 | RFC 6152, March 2011. 581 | 582 | Authors' Addresses 583 | 584 | Abel Yang 585 | TWNIC 586 | 4F-2, No. 9, Sec 2, Roosevelt Rd. 587 | Taipei 100 588 | Taiwan 589 | 590 | Phone: +886 2 23411313 ext 505 591 | EMail: abelyang@twnic.net.tw 592 | 593 | 594 | Shawn Steele 595 | Microsoft 596 | 597 | EMail: Shawn.Steele@microsoft.com 598 | 599 | 600 | Ned Freed 601 | Oracle 602 | 800 Royal Oaks 603 | Monrovia, CA 91016-6347 604 | USA 605 | 606 | EMail: ned+ietf@mrochek.com 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | Yang, et al. Standards Track [Page 11] 619 | 620 | -------------------------------------------------------------------------------- /spec/rfc2234: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Network Working Group D. Crocker, Ed. 8 | Request for Comments: 2234 Internet Mail Consortium 9 | Category: Standards Track P. Overell 10 | Demon Internet Ltd. 11 | November 1997 12 | 13 | 14 | Augmented BNF for Syntax Specifications: ABNF 15 | 16 | 17 | Status of this Memo 18 | 19 | This document specifies an Internet standards track protocol for the 20 | Internet community, and requests discussion and suggestions for 21 | improvements. Please refer to the current edition of the "Internet 22 | Official Protocol Standards" (STD 1) for the standardization state 23 | and status of this protocol. Distribution of this memo is unlimited. 24 | 25 | Copyright Notice 26 | 27 | Copyright (C) The Internet Society (1997). All Rights Reserved. 28 | 29 | TABLE OF CONTENTS 30 | 31 | 1. INTRODUCTION .................................................. 2 32 | 33 | 2. RULE DEFINITION ............................................... 2 34 | 2.1 RULE NAMING .................................................. 2 35 | 2.2 RULE FORM .................................................... 3 36 | 2.3 TERMINAL VALUES .............................................. 3 37 | 2.4 EXTERNAL ENCODINGS ........................................... 5 38 | 39 | 3. OPERATORS ..................................................... 5 40 | 3.1 CONCATENATION RULE1 RULE2 ............................. 5 41 | 3.2 ALTERNATIVES RULE1 / RULE2 ................................... 6 42 | 3.3 INCREMENTAL ALTERNATIVES RULE1 =/ RULE2 .................... 6 43 | 3.4 VALUE RANGE ALTERNATIVES %C##-## ........................... 7 44 | 3.5 SEQUENCE GROUP (RULE1 RULE2) ................................. 7 45 | 3.6 VARIABLE REPETITION *RULE .................................... 8 46 | 3.7 SPECIFIC REPETITION NRULE .................................... 8 47 | 3.8 OPTIONAL SEQUENCE [RULE] ..................................... 8 48 | 3.9 ; COMMENT .................................................... 8 49 | 3.10 OPERATOR PRECEDENCE ......................................... 9 50 | 51 | 4. ABNF DEFINITION OF ABNF ....................................... 9 52 | 53 | 5. SECURITY CONSIDERATIONS ....................................... 10 54 | 55 | 56 | 57 | 58 | Crocker & Overell Standards Track [Page 1] 59 | 60 | RFC 2234 ABNF for Syntax Specifications November 1997 61 | 62 | 63 | 6. APPENDIX A - CORE ............................................. 11 64 | 6.1 CORE RULES ................................................... 11 65 | 6.2 COMMON ENCODING .............................................. 12 66 | 67 | 7. ACKNOWLEDGMENTS ............................................... 12 68 | 69 | 8. REFERENCES .................................................... 13 70 | 71 | 9. CONTACT ....................................................... 13 72 | 73 | 10. FULL COPYRIGHT STATEMENT ..................................... 14 74 | 75 | 1. INTRODUCTION 76 | 77 | Internet technical specifications often need to define a format 78 | syntax and are free to employ whatever notation their authors deem 79 | useful. Over the years, a modified version of Backus-Naur Form 80 | (BNF), called Augmented BNF (ABNF), has been popular among many 81 | Internet specifications. It balances compactness and simplicity, 82 | with reasonable representational power. In the early days of the 83 | Arpanet, each specification contained its own definition of ABNF. 84 | This included the email specifications, RFC733 and then RFC822 which 85 | have come to be the common citations for defining ABNF. The current 86 | document separates out that definition, to permit selective 87 | reference. Predictably, it also provides some modifications and 88 | enhancements. 89 | 90 | The differences between standard BNF and ABNF involve naming rules, 91 | repetition, alternatives, order-independence, and value ranges. 92 | Appendix A (Core) supplies rule definitions and encoding for a core 93 | lexical analyzer of the type common to several Internet 94 | specifications. It is provided as a convenience and is otherwise 95 | separate from the meta language defined in the body of this document, 96 | and separate from its formal status. 97 | 98 | 2. RULE DEFINITION 99 | 100 | 2.1 Rule Naming 101 | 102 | The name of a rule is simply the name itself; that is, a sequence of 103 | characters, beginning with an alphabetic character, and followed by 104 | a combination of alphabetics, digits and hyphens (dashes). 105 | 106 | NOTE: Rule names are case-insensitive 107 | 108 | The names , , and all refer 109 | to the same rule. 110 | 111 | 112 | 113 | 114 | Crocker & Overell Standards Track [Page 2] 115 | 116 | RFC 2234 ABNF for Syntax Specifications November 1997 117 | 118 | 119 | Unlike original BNF, angle brackets ("<", ">") are not required. 120 | However, angle brackets may be used around a rule name whenever their 121 | presence will facilitate discerning the use of a rule name. This is 122 | typically restricted to rule name references in free-form prose, or 123 | to distinguish partial rules that combine into a string not separated 124 | by white space, such as shown in the discussion about repetition, 125 | below. 126 | 127 | 2.2 Rule Form 128 | 129 | A rule is defined by the following sequence: 130 | 131 | name = elements crlf 132 | 133 | where is the name of the rule, is one or more rule 134 | names or terminal specifications and is the end-of- line 135 | indicator, carriage return followed by line feed. The equal sign 136 | separates the name from the definition of the rule. The elements 137 | form a sequence of one or more rule names and/or value definitions, 138 | combined according to the various operators, defined in this 139 | document, such as alternative and repetition. 140 | 141 | For visual ease, rule definitions are left aligned. When a rule 142 | requires multiple lines, the continuation lines are indented. The 143 | left alignment and indentation are relative to the first lines of the 144 | ABNF rules and need not match the left margin of the document. 145 | 146 | 2.3 Terminal Values 147 | 148 | Rules resolve into a string of terminal values, sometimes called 149 | characters. In ABNF a character is merely a non-negative integer. 150 | In certain contexts a specific mapping (encoding) of values into a 151 | character set (such as ASCII) will be specified. 152 | 153 | Terminals are specified by one or more numeric characters with the 154 | base interpretation of those characters indicated explicitly. The 155 | following bases are currently defined: 156 | 157 | b = binary 158 | 159 | d = decimal 160 | 161 | x = hexadecimal 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Crocker & Overell Standards Track [Page 3] 171 | 172 | RFC 2234 ABNF for Syntax Specifications November 1997 173 | 174 | 175 | Hence: 176 | 177 | CR = %d13 178 | 179 | CR = %x0D 180 | 181 | respectively specify the decimal and hexadecimal representation of 182 | [US-ASCII] for carriage return. 183 | 184 | A concatenated string of such values is specified compactly, using a 185 | period (".") to indicate separation of characters within that value. 186 | Hence: 187 | 188 | CRLF = %d13.10 189 | 190 | ABNF permits specifying literal text string directly, enclosed in 191 | quotation-marks. Hence: 192 | 193 | command = "command string" 194 | 195 | Literal text strings are interpreted as a concatenated set of 196 | printable characters. 197 | 198 | NOTE: ABNF strings are case-insensitive and 199 | the character set for these strings is us-ascii. 200 | 201 | Hence: 202 | 203 | rulename = "abc" 204 | 205 | and: 206 | 207 | rulename = "aBc" 208 | 209 | will match "abc", "Abc", "aBc", "abC", "ABc", "aBC", "AbC" and "ABC". 210 | 211 | To specify a rule which IS case SENSITIVE, 212 | specify the characters individually. 213 | 214 | For example: 215 | 216 | rulename = %d97 %d98 %d99 217 | 218 | or 219 | 220 | rulename = %d97.98.99 221 | 222 | 223 | 224 | 225 | 226 | Crocker & Overell Standards Track [Page 4] 227 | 228 | RFC 2234 ABNF for Syntax Specifications November 1997 229 | 230 | 231 | will match only the string which comprises only lowercased 232 | characters, abc. 233 | 234 | 2.4 External Encodings 235 | 236 | External representations of terminal value characters will vary 237 | according to constraints in the storage or transmission environment. 238 | Hence, the same ABNF-based grammar may have multiple external 239 | encodings, such as one for a 7-bit US-ASCII environment, another for 240 | a binary octet environment and still a different one when 16-bit 241 | Unicode is used. Encoding details are beyond the scope of ABNF, 242 | although Appendix A (Core) provides definitions for a 7-bit US-ASCII 243 | environment as has been common to much of the Internet. 244 | 245 | By separating external encoding from the syntax, it is intended that 246 | alternate encoding environments can be used for the same syntax. 247 | 248 | 3. OPERATORS 249 | 250 | 3.1 Concatenation Rule1 Rule2 251 | 252 | A rule can define a simple, ordered string of values -- i.e., a 253 | concatenation of contiguous characters -- by listing a sequence of 254 | rule names. For example: 255 | 256 | foo = %x61 ; a 257 | 258 | bar = %x62 ; b 259 | 260 | mumble = foo bar foo 261 | 262 | So that the rule matches the lowercase string "aba". 263 | 264 | LINEAR WHITE SPACE: Concatenation is at the core of the ABNF 265 | parsing model. A string of contiguous characters (values) is 266 | parsed according to the rules defined in ABNF. For Internet 267 | specifications, there is some history of permitting linear white 268 | space (space and horizontal tab) to be freelyPand 269 | implicitlyPinterspersed around major constructs, such as 270 | delimiting special characters or atomic strings. 271 | 272 | NOTE: This specification for ABNF does not 273 | provide for implicit specification of linear white 274 | space. 275 | 276 | Any grammar which wishes to permit linear white space around 277 | delimiters or string segments must specify it explicitly. It is 278 | often useful to provide for such white space in "core" rules that are 279 | 280 | 281 | 282 | Crocker & Overell Standards Track [Page 5] 283 | 284 | RFC 2234 ABNF for Syntax Specifications November 1997 285 | 286 | 287 | then used variously among higher-level rules. The "core" rules might 288 | be formed into a lexical analyzer or simply be part of the main 289 | ruleset. 290 | 291 | 3.2 Alternatives Rule1 / Rule2 292 | 293 | Elements separated by forward slash ("/") are alternatives. 294 | Therefore, 295 | 296 | foo / bar 297 | 298 | will accept or . 299 | 300 | NOTE: A quoted string containing alphabetic 301 | characters is special form for specifying alternative 302 | characters and is interpreted as a non-terminal 303 | representing the set of combinatorial strings with the 304 | contained characters, in the specified order but with 305 | any mixture of upper and lower case.. 306 | 307 | 3.3 Incremental Alternatives Rule1 =/ Rule2 308 | 309 | It is sometimes convenient to specify a list of alternatives in 310 | fragments. That is, an initial rule may match one or more 311 | alternatives, with later rule definitions adding to the set of 312 | alternatives. This is particularly useful for otherwise- independent 313 | specifications which derive from the same parent rule set, such as 314 | often occurs with parameter lists. ABNF permits this incremental 315 | definition through the construct: 316 | 317 | oldrule =/ additional-alternatives 318 | 319 | So that the rule set 320 | 321 | ruleset = alt1 / alt2 322 | 323 | ruleset =/ alt3 324 | 325 | ruleset =/ alt4 / alt5 326 | 327 | is the same as specifying 328 | 329 | ruleset = alt1 / alt2 / alt3 / alt4 / alt5 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | Crocker & Overell Standards Track [Page 6] 339 | 340 | RFC 2234 ABNF for Syntax Specifications November 1997 341 | 342 | 343 | 3.4 Value Range Alternatives %c##-## 344 | 345 | A range of alternative numeric values can be specified compactly, 346 | using dash ("-") to indicate the range of alternative values. Hence: 347 | 348 | DIGIT = %x30-39 349 | 350 | is equivalent to: 351 | 352 | DIGIT = "0" / "1" / "2" / "3" / "4" / "5" / "6" / 353 | 354 | "7" / "8" / "9" 355 | 356 | Concatenated numeric values and numeric value ranges can not be 357 | specified in the same string. A numeric value may use the dotted 358 | notation for concatenation or it may use the dash notation to specify 359 | one value range. Hence, to specify one printable character, between 360 | end of line sequences, the specification could be: 361 | 362 | char-line = %x0D.0A %x20-7E %x0D.0A 363 | 364 | 3.5 Sequence Group (Rule1 Rule2) 365 | 366 | Elements enclosed in parentheses are treated as a single element, 367 | whose contents are STRICTLY ORDERED. Thus, 368 | 369 | elem (foo / bar) blat 370 | 371 | which matches (elem foo blat) or (elem bar blat). 372 | 373 | elem foo / bar blat 374 | 375 | matches (elem foo) or (bar blat). 376 | 377 | NOTE: It is strongly advised to use grouping 378 | notation, rather than to rely on proper reading of 379 | "bare" alternations, when alternatives consist of 380 | multiple rule names or literals. 381 | 382 | Hence it is recommended that instead of the above form, the form: 383 | 384 | (elem foo) / (bar blat) 385 | 386 | be used. It will avoid misinterpretation by casual readers. 387 | 388 | The sequence group notation is also used within free text to set off 389 | an element sequence from the prose. 390 | 391 | 392 | 393 | 394 | Crocker & Overell Standards Track [Page 7] 395 | 396 | RFC 2234 ABNF for Syntax Specifications November 1997 397 | 398 | 399 | 3.6 Variable Repetition *Rule 400 | 401 | The operator "*" preceding an element indicates repetition. The full 402 | form is: 403 | 404 | *element 405 | 406 | where and are optional decimal values, indicating at least 407 | and at most occurrences of element. 408 | 409 | Default values are 0 and infinity so that * allows any 410 | number, including zero; 1* requires at least one; 411 | 3*3 allows exactly 3 and 1*2 allows one or two. 412 | 413 | 3.7 Specific Repetition nRule 414 | 415 | A rule of the form: 416 | 417 | element 418 | 419 | is equivalent to 420 | 421 | *element 422 | 423 | That is, exactly occurrences of . Thus 2DIGIT is a 424 | 2-digit number, and 3ALPHA is a string of three alphabetic 425 | characters. 426 | 427 | 3.8 Optional Sequence [RULE] 428 | 429 | Square brackets enclose an optional element sequence: 430 | 431 | [foo bar] 432 | 433 | is equivalent to 434 | 435 | *1(foo bar). 436 | 437 | 3.9 ; Comment 438 | 439 | A semi-colon starts a comment that continues to the end of line. 440 | This is a simple way of including useful notes in parallel with the 441 | specifications. 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | Crocker & Overell Standards Track [Page 8] 451 | 452 | RFC 2234 ABNF for Syntax Specifications November 1997 453 | 454 | 455 | 3.10 Operator Precedence 456 | 457 | The various mechanisms described above have the following precedence, 458 | from highest (binding tightest) at the top, to lowest and loosest at 459 | the bottom: 460 | 461 | Strings, Names formation 462 | Comment 463 | Value range 464 | Repetition 465 | Grouping, Optional 466 | Concatenation 467 | Alternative 468 | 469 | Use of the alternative operator, freely mixed with concatenations can 470 | be confusing. 471 | 472 | Again, it is recommended that the grouping operator be used to 473 | make explicit concatenation groups. 474 | 475 | 4. ABNF DEFINITION OF ABNF 476 | 477 | This syntax uses the rules provided in Appendix A (Core). 478 | 479 | rulelist = 1*( rule / (*c-wsp c-nl) ) 480 | 481 | rule = rulename defined-as elements c-nl 482 | ; continues if next line starts 483 | ; with white space 484 | 485 | rulename = ALPHA *(ALPHA / DIGIT / "-") 486 | 487 | defined-as = *c-wsp ("=" / "=/") *c-wsp 488 | ; basic rules definition and 489 | ; incremental alternatives 490 | 491 | elements = alternation *c-wsp 492 | 493 | c-wsp = WSP / (c-nl WSP) 494 | 495 | c-nl = comment / CRLF 496 | ; comment or newline 497 | 498 | comment = ";" *(WSP / VCHAR) CRLF 499 | 500 | alternation = concatenation 501 | *(*c-wsp "/" *c-wsp concatenation) 502 | 503 | 504 | 505 | 506 | Crocker & Overell Standards Track [Page 9] 507 | 508 | RFC 2234 ABNF for Syntax Specifications November 1997 509 | 510 | 511 | concatenation = repetition *(1*c-wsp repetition) 512 | 513 | repetition = [repeat] element 514 | 515 | repeat = 1*DIGIT / (*DIGIT "*" *DIGIT) 516 | 517 | element = rulename / group / option / 518 | char-val / num-val / prose-val 519 | 520 | group = "(" *c-wsp alternation *c-wsp ")" 521 | 522 | option = "[" *c-wsp alternation *c-wsp "]" 523 | 524 | char-val = DQUOTE *(%x20-21 / %x23-7E) DQUOTE 525 | ; quoted string of SP and VCHAR 526 | without DQUOTE 527 | 528 | num-val = "%" (bin-val / dec-val / hex-val) 529 | 530 | bin-val = "b" 1*BIT 531 | [ 1*("." 1*BIT) / ("-" 1*BIT) ] 532 | ; series of concatenated bit values 533 | ; or single ONEOF range 534 | 535 | dec-val = "d" 1*DIGIT 536 | [ 1*("." 1*DIGIT) / ("-" 1*DIGIT) ] 537 | 538 | hex-val = "x" 1*HEXDIG 539 | [ 1*("." 1*HEXDIG) / ("-" 1*HEXDIG) ] 540 | 541 | prose-val = "<" *(%x20-3D / %x3F-7E) ">" 542 | ; bracketed string of SP and VCHAR 543 | without angles 544 | ; prose description, to be used as 545 | last resort 546 | 547 | 548 | 5. SECURITY CONSIDERATIONS 549 | 550 | Security is truly believed to be irrelevant to this document. 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | Crocker & Overell Standards Track [Page 10] 563 | 564 | RFC 2234 ABNF for Syntax Specifications November 1997 565 | 566 | 567 | 6. APPENDIX A - CORE 568 | 569 | This Appendix is provided as a convenient core for specific grammars. 570 | The definitions may be used as a core set of rules. 571 | 572 | 6.1 Core Rules 573 | 574 | Certain basic rules are in uppercase, such as SP, HTAB, CRLF, 575 | DIGIT, ALPHA, etc. 576 | 577 | ALPHA = %x41-5A / %x61-7A ; A-Z / a-z 578 | 579 | BIT = "0" / "1" 580 | 581 | CHAR = %x01-7F 582 | ; any 7-bit US-ASCII character, 583 | excluding NUL 584 | 585 | CR = %x0D 586 | ; carriage return 587 | 588 | CRLF = CR LF 589 | ; Internet standard newline 590 | 591 | CTL = %x00-1F / %x7F 592 | ; controls 593 | 594 | DIGIT = %x30-39 595 | ; 0-9 596 | 597 | DQUOTE = %x22 598 | ; " (Double Quote) 599 | 600 | HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" 601 | 602 | HTAB = %x09 603 | ; horizontal tab 604 | 605 | LF = %x0A 606 | ; linefeed 607 | 608 | LWSP = *(WSP / CRLF WSP) 609 | ; linear white space (past newline) 610 | 611 | OCTET = %x00-FF 612 | ; 8 bits of data 613 | 614 | SP = %x20 615 | 616 | 617 | 618 | Crocker & Overell Standards Track [Page 11] 619 | 620 | RFC 2234 ABNF for Syntax Specifications November 1997 621 | 622 | 623 | ; space 624 | 625 | VCHAR = %x21-7E 626 | ; visible (printing) characters 627 | 628 | WSP = SP / HTAB 629 | ; white space 630 | 631 | 6.2 Common Encoding 632 | 633 | Externally, data are represented as "network virtual ASCII", namely 634 | 7-bit US-ASCII in an 8-bit field, with the high (8th) bit set to 635 | zero. A string of values is in "network byte order" with the 636 | higher-valued bytes represented on the left-hand side and being sent 637 | over the network first. 638 | 639 | 7. ACKNOWLEDGMENTS 640 | 641 | The syntax for ABNF was originally specified in RFC 733. Ken L. 642 | Harrenstien, of SRI International, was responsible for re-coding the 643 | BNF into an augmented BNF that makes the representation smaller and 644 | easier to understand. 645 | 646 | This recent project began as a simple effort to cull out the portion 647 | of RFC 822 which has been repeatedly cited by non-email specification 648 | writers, namely the description of augmented BNF. Rather than simply 649 | and blindly converting the existing text into a separate document, 650 | the working group chose to give careful consideration to the 651 | deficiencies, as well as benefits, of the existing specification and 652 | related specifications available over the last 15 years and therefore 653 | to pursue enhancement. This turned the project into something rather 654 | more ambitious than first intended. Interestingly the result is not 655 | massively different from that original, although decisions such as 656 | removing the list notation came as a surprise. 657 | 658 | The current round of specification was part of the DRUMS working 659 | group, with significant contributions from Jerome Abela , Harald 660 | Alvestrand, Robert Elz, Roger Fajman, Aviva Garrett, Tom Harsch, Dan 661 | Kohn, Bill McQuillan, Keith Moore, Chris Newman , Pete Resnick and 662 | Henning Schulzrinne. 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | Crocker & Overell Standards Track [Page 12] 675 | 676 | RFC 2234 ABNF for Syntax Specifications November 1997 677 | 678 | 679 | 8. REFERENCES 680 | 681 | [US-ASCII] Coded Character Set--7-Bit American Standard Code for 682 | Information Interchange, ANSI X3.4-1986. 683 | 684 | [RFC733] Crocker, D., Vittal, J., Pogran, K., and D. Henderson, 685 | "Standard for the Format of ARPA Network Text Message," RFC 733, 686 | November 1977. 687 | 688 | [RFC822] Crocker, D., "Standard for the Format of ARPA Internet Text 689 | Messages", STD 11, RFC 822, August 1982. 690 | 691 | 9. CONTACT 692 | 693 | David H. Crocker Paul Overell 694 | 695 | Internet Mail Consortium Demon Internet Ltd 696 | 675 Spruce Dr. Dorking Business Park 697 | Sunnyvale, CA 94086 USA Dorking 698 | Surrey, RH4 1HN 699 | UK 700 | 701 | Phone: +1 408 246 8253 702 | Fax: +1 408 249 6205 703 | EMail: dcrocker@imc.org paulo@turnpike.com 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | Crocker & Overell Standards Track [Page 13] 731 | 732 | RFC 2234 ABNF for Syntax Specifications November 1997 733 | 734 | 735 | 10. Full Copyright Statement 736 | 737 | Copyright (C) The Internet Society (1997). All Rights Reserved. 738 | 739 | This document and translations of it may be copied and furnished to 740 | others, and derivative works that comment on or otherwise explain it 741 | or assist in its implementation may be prepared, copied, published 742 | and distributed, in whole or in part, without restriction of any 743 | kind, provided that the above copyright notice and this paragraph are 744 | included on all such copies and derivative works. However, this 745 | document itself may not be modified in any way, such as by removing 746 | the copyright notice or references to the Internet Society or other 747 | Internet organizations, except as needed for the purpose of 748 | developing Internet standards in which case the procedures for 749 | copyrights defined in the Internet Standards process must be 750 | followed, or as required to translate it into languages other than 751 | English. 752 | 753 | The limited permissions granted above are perpetual and will not be 754 | revoked by the Internet Society or its successors or assigns. 755 | 756 | This document and the information contained herein is provided on an 757 | "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING 758 | TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING 759 | BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION 760 | HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF 761 | MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | Crocker & Overell Standards Track [Page 14] 787 | 788 | --------------------------------------------------------------------------------