├── 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 |
--------------------------------------------------------------------------------