├── .gitignore ├── .gitmodules ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── elm.json ├── generate-tests.js ├── index.html ├── package.json ├── src ├── Json │ ├── Schema.elm │ ├── Schema │ │ ├── Builder.elm │ │ ├── Definitions.elm │ │ ├── Examples.elm │ │ ├── Helpers.elm │ │ ├── Random.elm │ │ └── Validation.elm │ └── Schemata.elm ├── Ref.elm └── Util.elm ├── tests ├── .gitignore ├── Decoding.elm ├── Draft4.elm ├── Draft6.elm ├── Generator.elm ├── Type.elm.rm ├── Validations.elm └── elm-package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | repl-temp-* 3 | elm-stuff 4 | dist 5 | .DS_Store 6 | npm-debug.log 7 | deps 8 | documentation.json 9 | elm.js 10 | docs.json 11 | JSON-Schema-Test-Suite/ 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/json-tools/json-schema/6dd54e2d077400642950ce06c637c7485b0490f3/.gitmodules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: node 5 | 6 | cache: 7 | directories: 8 | - elm-stuff/build-artifacts 9 | - elm-stuff/packages 10 | - sysconfcpus 11 | os: 12 | - linux 13 | 14 | env: ELM_VERSION=0.18.0 15 | 16 | before_install: 17 | - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 18 | 19 | install: 20 | - node --version 21 | - npm --version 22 | - npm install -g elm@$ELM_VERSION elm-test 23 | - git clone https://github.com/NoRedInk/elm-ops-tooling 24 | - elm-ops-tooling/with_retry.rb elm package install --yes 25 | # Faster compile on Travis. 26 | - | 27 | if [ ! -d sysconfcpus/bin ]; 28 | then 29 | git clone https://github.com/obmarg/libsysconfcpus.git; 30 | cd libsysconfcpus; 31 | ./configure --prefix=$TRAVIS_BUILD_DIR/sysconfcpus; 32 | make && make install; 33 | cd ..; 34 | fi 35 | 36 | # before_script: 37 | # - cd tests && $TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-make --yes Tests.elm && cd .. 38 | 39 | script: 40 | - $TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-test 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 4.1.0 2 | 3 | Added support of custom keywords: 4 | - Json.Schema.Builder.withCustomKeyword 5 | - Json.Schema.Definitions.getCustomKeywordValue 6 | 7 | ## 4.0.0 8 | 9 | - support full spec of draft-04 and draft-06 10 | - functionality validated by [official test suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) 11 | - dozen of fixes in validation 12 | - new type `ExclusiveBoundary(BoolBoundary, NumberBoundary)` (compatibility layer between drafts 4 and 6) 13 | 14 | ## 3.0.0 15 | 16 | - changed validation errors (added more info to facilitate error messages building) 17 | 18 | ## 2.0.0 19 | 20 | Multiple errors with details returned by validation: 21 | - added `Json.Schema.Validation` 22 | - changed validation output format from `Result String Bool` to `Result (List Json.Schema.Validation.Error) Value` 23 | 24 | ## 1.1.0 25 | 26 | Added `Json.Schema.Random` API to generate random values 27 | 28 | 29 | ## 1.0.0 30 | 31 | Initial capabilities: decode/encode, validate with a single string error 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Anatolii Chakkaev 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Schema decoder and validator for elm 2 | 3 | [![Build Status](https://travis-ci.org/json-tools/json-schema.svg?branch=master)](https://travis-ci.org/json-tools/json-schema) 4 | 5 | > JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. (http://json-schema.org/) 6 | 7 | This code is experimental, it doesn't cover json schema spec in full (yet), just allows to parse minimal subset of it in order to implement very basic proof of concept of "type as value" in elm. 8 | 9 | The end goal of this project is to cover json schema draft 6 spec in full, if you're interested - feel free to pick up some of the open issues and submit PR. 10 | 11 | ## When to use this library? 12 | 13 | ### ✍ form generation 14 | 15 | Sometimes it is not possible by design to come up with some static type definition, for example if you are building REST API test console, where each endpoint requires its own data. The simplest solution would be to allow user to enter data as json string, decode it into value to ensure that json is valid and send to a server as value, without looking inside this value to make sure it makes sense. But what if we want to enter data using form, perform some basic validation before sending it to a server? Then we have a valid use case for this library. 16 | 17 | ### ☝ documentation generation 18 | 19 | JSON Schema allows you to specify some meta data like title, description, examples, definitions and also some validation keywords like type, format, enum, and all sub-schemas (e.g. properties, items) which is a useful source of information for content generation if you want to document data structures. 20 | 21 | ### ✌ validation 22 | 23 | Instead of writing validation code as part of your frontend app you could describe it in a declarative way as JSON schema, so that you can focus on what your are validating rather than how. Combined with form generation this is a great time-saver while building interfaces. 24 | 25 | ## Current status of this project 26 | 27 | - [x] decode draft 6 of json-schema 28 | - [x] validate all the things 29 | - [x] schema builder api 30 | - [x] documentation 31 | - [x] random value generator 32 | - [x] demo: json editor 33 | - [x] multiple errors 34 | - [x] support draft-04 and draft-06 of JSON Schema 35 | - [ ] full `$ref` support 36 | - [ ] i18n 37 | - [ ] demo: docs generator 38 | - [ ] demo: schema builder 39 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "json-tools/json-schema", 4 | "summary": "JSON Schema for elm", 5 | "license": "BSD-3-Clause", 6 | "version": "1.0.2", 7 | "exposed-modules": [ 8 | "Json.Schema", 9 | "Json.Schema.Random", 10 | "Json.Schema.Builder", 11 | "Json.Schema.Validation", 12 | "Json.Schema.Definitions" 13 | ], 14 | "elm-version": "0.19.0 <= v < 0.20.0", 15 | "dependencies": { 16 | "NoRedInk/elm-json-decode-pipeline": "1.0.0 <= v < 2.0.0", 17 | "elm/core": "1.0.0 <= v < 2.0.0", 18 | "elm/json": "1.0.0 <= v < 2.0.0", 19 | "elm/random": "1.0.0 <= v < 2.0.0", 20 | "elm/regex": "1.0.0 <= v < 2.0.0", 21 | "zwilias/elm-utf-tools": "2.0.1 <= v < 3.0.0" 22 | }, 23 | "test-dependencies": {} 24 | } -------------------------------------------------------------------------------- /generate-tests.js: -------------------------------------------------------------------------------- 1 | const { readdirSync, readFileSync } = require('fs'); 2 | const { join } = require('path'); 3 | 4 | const namespace = process.argv[2]; 5 | 6 | if (namespace !== 'draft-4' && namespace !== 'draft-6') { 7 | console.error('Pleace specify namespace as a param. Avalable options: draft-4, draft-6.'); 8 | console.error('Usage example: node generate-tests.js draft-6'); 9 | process.exit(1); 10 | } 11 | 12 | const dirname = namespace.replace('-', ''); 13 | const moduleName = dirname.substr(0, 1).toUpperCase() + dirname.substr(1); 14 | 15 | const tests = load('./JSON-Schema-Test-Suite/tests/' + dirname); 16 | console.log(header(moduleName) + body(namespace, tests) + footer()); 17 | 18 | function load(path) { 19 | return readdirSync(path) 20 | .filter(x => x.endsWith('.json')) 21 | .filter(x => x !== 'refRemote.json') 22 | .map(filename => { 23 | return { filename, suite: JSON.parse(readFileSync(join(path, filename))) }; 24 | }); 25 | } 26 | 27 | function header(moduleName) { 28 | return `module ${moduleName} exposing (all) 29 | 30 | import Json.Encode as Encode exposing (Value) 31 | import Json.Decode as Decode exposing (decodeString, value) 32 | import Json.Schema.Definitions exposing (blankSchema, decoder) 33 | import Json.Schema exposing (validateValue) 34 | import Test exposing (Test, describe, test, only) 35 | import Expect 36 | 37 | 38 | all : Test 39 | all = 40 | `; 41 | 42 | } 43 | 44 | function footer() { 45 | return ` 46 | 47 | 48 | examine : String -> String -> Bool -> Expect.Expectation 49 | examine schemaSource dataSource outcome = 50 | let 51 | schema = 52 | schemaSource 53 | |> decodeString decoder 54 | |> Result.withDefault blankSchema 55 | 56 | data = 57 | dataSource 58 | |> decodeString value 59 | |> Result.withDefault Encode.null 60 | 61 | result = 62 | validateValue data schema 63 | |> Result.mapError toString 64 | |> Result.map (\\_ -> True) 65 | in 66 | if outcome then 67 | result 68 | |> Expect.equal (Ok True) 69 | else 70 | case result of 71 | Ok x -> 72 | Expect.fail "Unexpected success" 73 | 74 | Err _ -> 75 | Expect.pass`; 76 | } 77 | 78 | function body(name, tests) { 79 | return ` describe "${name}" 80 | [ ` + 81 | tests.map(({filename, suite}) => { 82 | return `describe "${filename}" 83 | [ ${printSuite(suite).join('\n , ')} 84 | ]` 85 | }).join('\n , ') 86 | + '\n ]'; 87 | } 88 | 89 | function printSuite(cases) { 90 | return cases.map(({description, schema, tests}) => { 91 | return `describe "suite: ${description}" 92 | [ ${printCases(schema, tests).join('\n , ')} 93 | ]` 94 | ; 95 | }); 96 | } 97 | 98 | 99 | function printCases(schema, collection) { 100 | return collection.map(({description, data, valid}) => { 101 | return `test "${description}" <| 102 | \\() -> 103 | examine 104 | """ 105 | ${JSON.stringify(schema, null, ' ').replace(/\n/g, '\n ')} 106 | """ 107 | """ 108 | ${JSON.stringify(data, null, ' ').replace(/\n/g, '\n ')} 109 | """ 110 | ${valid ? 'True' : 'False'}` 111 | }); 112 | } 113 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSON Schema Builder 5 | 6 | 7 | 8 | 9 | 10 | 19 | 20 | 21 | 22 | 23 | 161 | 162 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-schema", 3 | "private": false, 4 | "version": "3.0.0", 5 | "description": "", 6 | "author": "", 7 | "license": "ISC", 8 | "scripts": { 9 | "start": "elm-live src/Main.elm --output=elm.js --open", 10 | "test": "elm-test --watch", 11 | "deploy": "gh-pages -d dist -a", 12 | "generate-tests": "node ./generate-tests.js draft-6 > tests/Draft6.elm && node ./generate-tests.js draft-4 > tests/Draft4.elm" 13 | }, 14 | "devDependencies": { 15 | "elm": "^0.18.0", 16 | "elm-live": "^2.7.4", 17 | "elm-test": "^0.18.7" 18 | }, 19 | "dependencies": { 20 | "gh-pages": "^1.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Json/Schema.elm: -------------------------------------------------------------------------------- 1 | module Json.Schema exposing 2 | ( fromValue, fromString 3 | , validateValue, validateAt 4 | ) 5 | 6 | {-| This library provides bunch of utility methods to work with JSON values using 7 | schemas defined in [JSON Schema](http://json-schema.org/) format. 8 | 9 | Currently it allows to construct schemata ([draft-6](https://github.com/json-schema-org/json-schema-spec/blob/draft-06/schema.json)), validate values and generate random 10 | values based on schema (very experimental feature). 11 | It supports local references, but doesn't support remote references. 12 | 13 | 14 | # Decode schema 15 | 16 | Use `fromValue` or `fromString` methods if you receive schema from external source. If you want to construct schema from elm code you might want to use `Json.Schema.Builder`, or low-level API using definitions from `Json.Schema.Definitions` 17 | 18 | @docs fromValue, fromString 19 | 20 | 21 | # Validation 22 | 23 | @docs validateValue, validateAt 24 | 25 | -} 26 | 27 | import Json.Decode exposing (Value, decodeString, decodeValue) 28 | import Json.Schema.Definitions exposing (Schema, decoder) 29 | import Json.Schema.Helpers exposing (collectIds) 30 | import Json.Schema.Validation exposing (Error, JsonPointer, ValidationError(..), ValidationOptions, validate) 31 | import Json.Schemata 32 | import Ref exposing (SchemataPool, defaultPool) 33 | 34 | 35 | {-| Validate value against JSON Schema. Returns Result with updated value in case if validationOptions require so. 36 | 37 | schema 38 | |> Json.Schema.validateValue { applyDefaults = True } value 39 | 40 | -} 41 | validateValue : ValidationOptions -> Value -> Schema -> Result (List Error) Value 42 | validateValue validationOptions value schema = 43 | let 44 | ( pool, _ ) = 45 | collectIds schema defaultPool 46 | in 47 | validate validationOptions pool value schema schema 48 | 49 | 50 | {-| Validate value using subschema identified by URI. 51 | -} 52 | validateAt : ValidationOptions -> Value -> Schema -> String -> Result (List Error) Value 53 | validateAt validationOptions value schema uri = 54 | let 55 | ( pool, _ ) = 56 | collectIds schema defaultPool 57 | in 58 | case Ref.resolveReference "" pool schema uri of 59 | Just ( ns, resolvedSchema ) -> 60 | validate validationOptions pool value schema resolvedSchema 61 | 62 | Nothing -> 63 | Err [ Error (JsonPointer "" []) <| UnresolvableReference uri ] 64 | 65 | 66 | {-| Construct JSON Schema from JSON value 67 | -} 68 | fromValue : Value -> Result String Schema 69 | fromValue = 70 | decodeValue decoder 71 | >> Result.mapError Json.Decode.errorToString 72 | 73 | 74 | {-| Construct JSON Schema from string 75 | -} 76 | fromString : String -> Result String Schema 77 | fromString = 78 | decodeString decoder 79 | >> Result.mapError Json.Decode.errorToString 80 | -------------------------------------------------------------------------------- /src/Json/Schema/Builder.elm: -------------------------------------------------------------------------------- 1 | module Json.Schema.Builder exposing 2 | ( SchemaBuilder(..) 3 | , buildSchema, boolSchema, toSchema, encode 4 | , withType, withNullableType, withUnionType 5 | , withTitle, withDescription, withDefault, withExamples, withDefinitions 6 | , withId, withRef, withCustomKeyword 7 | , withMultipleOf, withMaximum, withMinimum, withExclusiveMaximum, withExclusiveMinimum 8 | , withMaxLength, withMinLength, withPattern, withFormat 9 | , withItems, withItem, withAdditionalItems, withMaxItems, withMinItems, withUniqueItems, withContains 10 | , withMaxProperties, withMinProperties, withRequired, withProperties, withPatternProperties, withAdditionalProperties, withSchemaDependency, withPropNamesDependency, withPropertyNames 11 | , withEnum, withConst, withAllOf, withAnyOf, withOneOf, withNot 12 | , validate 13 | -- type 14 | -- object 15 | -- encode 16 | -- numeric 17 | -- string 18 | -- array 19 | -- custom keyword 20 | -- schema 21 | -- generic 22 | -- meta 23 | ) 24 | 25 | {-| Convenience API to build a valid JSON schema 26 | 27 | 28 | # Definition 29 | 30 | @docs SchemaBuilder 31 | 32 | 33 | # Schema builder creation 34 | 35 | @docs buildSchema, boolSchema, toSchema, encode 36 | 37 | 38 | # Building up schema 39 | 40 | 41 | ## Type 42 | 43 | JSON Schema spec allows type to be string or array of strings. There are three 44 | groups of types produced: single types (e.g. `"string"`), nullable types (e.g. `["string", "null"]`) 45 | and union types (e.g. `["string", "object"]`) 46 | 47 | @docs withType, withNullableType, withUnionType 48 | 49 | 50 | ## Meta 51 | 52 | @docs withTitle, withDescription, withDefault, withExamples, withDefinitions 53 | 54 | 55 | ## JSON-Schema 56 | 57 | @docs withId, withRef, withCustomKeyword 58 | 59 | 60 | ## Numeric validations 61 | 62 | The following validations are only applicable to numeric values and 63 | will always succeed for any type other than `number` and `integer` 64 | 65 | @docs withMultipleOf, withMaximum, withMinimum, withExclusiveMaximum, withExclusiveMinimum 66 | 67 | 68 | ## String validations 69 | 70 | @docs withMaxLength, withMinLength, withPattern, withFormat 71 | 72 | 73 | ## Array validations 74 | 75 | @docs withItems, withItem, withAdditionalItems, withMaxItems, withMinItems, withUniqueItems, withContains 76 | 77 | 78 | ## Object validations 79 | 80 | @docs withMaxProperties, withMinProperties, withRequired, withProperties, withPatternProperties, withAdditionalProperties, withSchemaDependency, withPropNamesDependency, withPropertyNames 81 | 82 | 83 | ## Generic validations 84 | 85 | @docs withEnum, withConst, withAllOf, withAnyOf, withOneOf, withNot 86 | 87 | 88 | # Validation 89 | 90 | @docs validate 91 | 92 | -} 93 | 94 | import Json.Decode as Decode exposing (Value) 95 | import Json.Encode as Encode 96 | import Json.Schema.Definitions 97 | exposing 98 | ( Dependency(..) 99 | , ExclusiveBoundary(..) 100 | , Items(..) 101 | , Schema(..) 102 | , Schemata(..) 103 | , SingleType(..) 104 | , SubSchema 105 | , Type(..) 106 | , blankSubSchema 107 | , stringToType 108 | ) 109 | import Json.Schema.Validation as Validation exposing (Error) 110 | import Ref 111 | import Util exposing (foldResults) 112 | 113 | 114 | {-| Builder for JSON schema providing an API like this: 115 | 116 | buildSchema 117 | |> withTitle "My object" 118 | |> withProperties 119 | [ ( "foo" 120 | , buildSchema 121 | |> withType "string" 122 | ) 123 | , ( "bar" 124 | , buildSchema 125 | |> withType "integer" 126 | |> withMaximum 10 127 | ) 128 | ] 129 | 130 | -} 131 | type SchemaBuilder 132 | = SchemaBuilder { errors : List String, schema : Maybe SubSchema, bool : Maybe Bool } 133 | 134 | 135 | 136 | -- BASIC API 137 | 138 | 139 | {-| Create schema builder with blank schema 140 | -} 141 | buildSchema : SchemaBuilder 142 | buildSchema = 143 | SchemaBuilder { errors = [], schema = Just blankSubSchema, bool = Nothing } 144 | 145 | 146 | {-| Create boolean schema 147 | -} 148 | boolSchema : Bool -> SchemaBuilder 149 | boolSchema b = 150 | SchemaBuilder { errors = [], schema = Nothing, bool = Just b } 151 | 152 | 153 | {-| Extract JSON Schema from the builder 154 | -} 155 | toSchema : SchemaBuilder -> Result String Schema 156 | toSchema (SchemaBuilder sb) = 157 | if List.isEmpty sb.errors then 158 | case sb.bool of 159 | Just x -> 160 | Ok <| BooleanSchema x 161 | 162 | Nothing -> 163 | case sb.schema of 164 | Just ss -> 165 | Ok <| ObjectSchema { ss | source = Json.Schema.Definitions.encode (ObjectSchema ss) } 166 | 167 | Nothing -> 168 | Ok <| ObjectSchema blankSubSchema 169 | 170 | else 171 | Err <| String.join ", " sb.errors 172 | 173 | 174 | {-| Validate value using schema controlled by builder. 175 | -} 176 | validate : Validation.ValidationOptions -> Value -> SchemaBuilder -> Result (List Error) Value 177 | validate validationOptions val sb = 178 | case toSchema sb of 179 | Ok schema -> 180 | Validation.validate validationOptions Ref.defaultPool val schema schema 181 | 182 | Err s -> 183 | Ok val 184 | 185 | 186 | 187 | --Err <| "Schema is invalid: " ++ s 188 | -- TYPE 189 | 190 | 191 | {-| Set the `type` property of JSON schema to a specific type, accepts strings 192 | 193 | buildSchema 194 | |> withType "boolean" 195 | 196 | -} 197 | withType : String -> SchemaBuilder -> SchemaBuilder 198 | withType t sb = 199 | t 200 | |> stringToType 201 | |> Result.map (\x -> updateSchema (\s -> { s | type_ = SingleType x }) sb) 202 | |> (\r -> 203 | case r of 204 | Ok x -> 205 | x 206 | 207 | Err s -> 208 | appendError s sb 209 | ) 210 | 211 | 212 | {-| Set the `type` property of JSON schema to a nullable type. 213 | 214 | buildSchema 215 | |> withNullableType "string" 216 | 217 | -} 218 | withNullableType : String -> SchemaBuilder -> SchemaBuilder 219 | withNullableType t = 220 | case stringToType t of 221 | Ok NullType -> 222 | appendError "Nullable null is not allowed" 223 | 224 | Ok r -> 225 | updateSchema (\s -> { s | type_ = NullableType r }) 226 | 227 | Err s -> 228 | appendError s 229 | 230 | 231 | {-| Set the `type` property of JSON schema to an union type. 232 | 233 | buildSchema 234 | |> withUnionType [ "string", "object" ] 235 | 236 | -} 237 | withUnionType : List String -> SchemaBuilder -> SchemaBuilder 238 | withUnionType listTypes sb = 239 | listTypes 240 | |> List.sort 241 | |> List.map stringToType 242 | |> foldResults 243 | |> Result.map (\s -> updateSchema (\x -> { x | type_ = UnionType s }) sb) 244 | |> (\x -> 245 | case x of 246 | Err s -> 247 | appendError s sb 248 | 249 | Ok xLocal -> 250 | xLocal 251 | ) 252 | 253 | 254 | {-| Set the `contains` property of JSON schema to a sub-schema. 255 | 256 | buildSchema 257 | |> withContains 258 | (buildSchema 259 | |> withType "string" 260 | ) 261 | 262 | -} 263 | withContains : SchemaBuilder -> SchemaBuilder -> SchemaBuilder 264 | withContains = 265 | updateWithSubSchema (\sub s -> { s | contains = sub }) 266 | 267 | 268 | {-| -} 269 | withNot : SchemaBuilder -> SchemaBuilder -> SchemaBuilder 270 | withNot = 271 | updateWithSubSchema (\sub s -> { s | not = sub }) 272 | 273 | 274 | {-| -} 275 | withDefinitions : List ( String, SchemaBuilder ) -> SchemaBuilder -> SchemaBuilder 276 | withDefinitions = 277 | updateWithSchemata (\definitions s -> { s | definitions = definitions }) 278 | 279 | 280 | {-| -} 281 | withItems : List SchemaBuilder -> SchemaBuilder -> SchemaBuilder 282 | withItems listSchemas = 283 | case listSchemas |> toListOfSchemas of 284 | Ok items -> 285 | updateSchema (\s -> { s | items = ArrayOfItems items }) 286 | 287 | Err s -> 288 | appendError s 289 | 290 | 291 | {-| -} 292 | withItem : SchemaBuilder -> SchemaBuilder -> SchemaBuilder 293 | withItem item = 294 | case item |> toSchema of 295 | Ok itemSchema -> 296 | updateSchema (\s -> { s | items = ItemDefinition itemSchema }) 297 | 298 | Err s -> 299 | appendError s 300 | 301 | 302 | {-| -} 303 | withAdditionalItems : SchemaBuilder -> SchemaBuilder -> SchemaBuilder 304 | withAdditionalItems = 305 | updateWithSubSchema (\sub s -> { s | additionalItems = sub }) 306 | 307 | 308 | {-| -} 309 | withProperties : List ( String, SchemaBuilder ) -> SchemaBuilder -> SchemaBuilder 310 | withProperties = 311 | updateWithSchemata (\properties s -> { s | properties = properties }) 312 | 313 | 314 | {-| -} 315 | withPatternProperties : List ( String, SchemaBuilder ) -> SchemaBuilder -> SchemaBuilder 316 | withPatternProperties = 317 | updateWithSchemata (\patternProperties s -> { s | patternProperties = patternProperties }) 318 | 319 | 320 | {-| -} 321 | withAdditionalProperties : SchemaBuilder -> SchemaBuilder -> SchemaBuilder 322 | withAdditionalProperties = 323 | updateWithSubSchema (\sub s -> { s | additionalProperties = sub }) 324 | 325 | 326 | {-| -} 327 | withSchemaDependency : String -> SchemaBuilder -> SchemaBuilder -> SchemaBuilder 328 | withSchemaDependency name sd = 329 | case sd |> toSchema of 330 | Ok depSchema -> 331 | updateSchema (\s -> { s | dependencies = s.dependencies ++ [ ( name, PropSchema depSchema ) ] }) 332 | 333 | Err s -> 334 | appendError s 335 | 336 | 337 | {-| -} 338 | withPropNamesDependency : String -> List String -> SchemaBuilder -> SchemaBuilder 339 | withPropNamesDependency name pn = 340 | updateSchema (\schema -> { schema | dependencies = ( name, ArrayPropNames pn ) :: schema.dependencies }) 341 | 342 | 343 | {-| -} 344 | withPropertyNames : SchemaBuilder -> SchemaBuilder -> SchemaBuilder 345 | withPropertyNames = 346 | updateWithSubSchema (\propertyNames s -> { s | propertyNames = propertyNames }) 347 | 348 | 349 | {-| -} 350 | withAllOf : List SchemaBuilder -> SchemaBuilder -> SchemaBuilder 351 | withAllOf = 352 | updateWithListOfSchemas (\allOf s -> { s | allOf = allOf }) 353 | 354 | 355 | {-| -} 356 | withAnyOf : List SchemaBuilder -> SchemaBuilder -> SchemaBuilder 357 | withAnyOf = 358 | updateWithListOfSchemas (\anyOf s -> { s | anyOf = anyOf }) 359 | 360 | 361 | {-| -} 362 | withOneOf : List SchemaBuilder -> SchemaBuilder -> SchemaBuilder 363 | withOneOf = 364 | updateWithListOfSchemas (\oneOf s -> { s | oneOf = oneOf }) 365 | 366 | 367 | {-| -} 368 | withTitle : String -> SchemaBuilder -> SchemaBuilder 369 | withTitle x = 370 | updateSchema (\s -> { s | title = Just x }) 371 | 372 | 373 | {-| -} 374 | withDescription : String -> SchemaBuilder -> SchemaBuilder 375 | withDescription x = 376 | updateSchema (\s -> { s | description = Just x }) 377 | 378 | 379 | {-| -} 380 | withMultipleOf : Float -> SchemaBuilder -> SchemaBuilder 381 | withMultipleOf x = 382 | updateSchema (\s -> { s | multipleOf = Just x }) 383 | 384 | 385 | {-| -} 386 | withMaximum : Float -> SchemaBuilder -> SchemaBuilder 387 | withMaximum x = 388 | updateSchema (\s -> { s | maximum = Just x }) 389 | 390 | 391 | {-| -} 392 | withMinimum : Float -> SchemaBuilder -> SchemaBuilder 393 | withMinimum x = 394 | updateSchema (\s -> { s | minimum = Just x }) 395 | 396 | 397 | {-| -} 398 | withExclusiveMaximum : Float -> SchemaBuilder -> SchemaBuilder 399 | withExclusiveMaximum x = 400 | updateSchema (\s -> { s | exclusiveMaximum = Just (NumberBoundary x) }) 401 | 402 | 403 | {-| -} 404 | withExclusiveMinimum : Float -> SchemaBuilder -> SchemaBuilder 405 | withExclusiveMinimum x = 406 | updateSchema (\s -> { s | exclusiveMinimum = Just (NumberBoundary x) }) 407 | 408 | 409 | {-| -} 410 | withMaxLength : Int -> SchemaBuilder -> SchemaBuilder 411 | withMaxLength x = 412 | updateSchema (\s -> { s | maxLength = Just x }) 413 | 414 | 415 | {-| -} 416 | withMinLength : Int -> SchemaBuilder -> SchemaBuilder 417 | withMinLength x = 418 | updateSchema (\s -> { s | minLength = Just x }) 419 | 420 | 421 | {-| -} 422 | withMaxProperties : Int -> SchemaBuilder -> SchemaBuilder 423 | withMaxProperties n = 424 | updateSchema (\s -> { s | maxProperties = Just n }) 425 | 426 | 427 | {-| -} 428 | withMinProperties : Int -> SchemaBuilder -> SchemaBuilder 429 | withMinProperties n = 430 | updateSchema (\s -> { s | minProperties = Just n }) 431 | 432 | 433 | {-| -} 434 | withMaxItems : Int -> SchemaBuilder -> SchemaBuilder 435 | withMaxItems n = 436 | updateSchema (\s -> { s | maxItems = Just n }) 437 | 438 | 439 | {-| -} 440 | withMinItems : Int -> SchemaBuilder -> SchemaBuilder 441 | withMinItems n = 442 | updateSchema (\s -> { s | minItems = Just n }) 443 | 444 | 445 | {-| -} 446 | withUniqueItems : Bool -> SchemaBuilder -> SchemaBuilder 447 | withUniqueItems b = 448 | updateSchema (\s -> { s | uniqueItems = Just b }) 449 | 450 | 451 | {-| -} 452 | withPattern : String -> SchemaBuilder -> SchemaBuilder 453 | withPattern x = 454 | updateSchema (\s -> { s | pattern = Just x }) 455 | 456 | 457 | {-| -} 458 | withFormat : String -> SchemaBuilder -> SchemaBuilder 459 | withFormat x = 460 | updateSchema (\s -> { s | format = Just x }) 461 | 462 | 463 | {-| -} 464 | withEnum : List Value -> SchemaBuilder -> SchemaBuilder 465 | withEnum x = 466 | updateSchema (\s -> { s | enum = Just x }) 467 | 468 | 469 | {-| -} 470 | withRequired : List String -> SchemaBuilder -> SchemaBuilder 471 | withRequired x = 472 | updateSchema (\s -> { s | required = Just x }) 473 | 474 | 475 | {-| -} 476 | withConst : Value -> SchemaBuilder -> SchemaBuilder 477 | withConst v = 478 | updateSchema (\s -> { s | const = Just v }) 479 | 480 | 481 | {-| -} 482 | withRef : String -> SchemaBuilder -> SchemaBuilder 483 | withRef x = 484 | updateSchema (\s -> { s | ref = Just x }) 485 | 486 | 487 | {-| -} 488 | withExamples : List Value -> SchemaBuilder -> SchemaBuilder 489 | withExamples x = 490 | updateSchema (\s -> { s | examples = Just x }) 491 | 492 | 493 | {-| -} 494 | withDefault : Value -> SchemaBuilder -> SchemaBuilder 495 | withDefault x = 496 | updateSchema (\s -> { s | default = Just x }) 497 | 498 | 499 | {-| -} 500 | withId : String -> SchemaBuilder -> SchemaBuilder 501 | withId x = 502 | updateSchema (\s -> { s | id = Just x }) 503 | 504 | 505 | {-| -} 506 | withCustomKeyword : String -> Value -> SchemaBuilder -> SchemaBuilder 507 | withCustomKeyword key val = 508 | updateSchema 509 | (\s -> 510 | { s 511 | | source = 512 | s.source 513 | |> Decode.decodeValue (Decode.keyValuePairs Decode.value) 514 | |> Result.withDefault [] 515 | |> (::) ( key, val ) 516 | |> Encode.object 517 | } 518 | ) 519 | 520 | 521 | 522 | -- HELPERS 523 | 524 | 525 | updateSchema : (SubSchema -> SubSchema) -> SchemaBuilder -> SchemaBuilder 526 | updateSchema fn (SchemaBuilder sb) = 527 | case sb.schema of 528 | Just ss -> 529 | SchemaBuilder { sb | schema = Just <| fn ss } 530 | 531 | Nothing -> 532 | SchemaBuilder sb 533 | 534 | 535 | appendError : String -> SchemaBuilder -> SchemaBuilder 536 | appendError e (SchemaBuilder { errors, schema, bool }) = 537 | SchemaBuilder { errors = e :: errors, schema = schema, bool = bool } 538 | 539 | 540 | type alias SchemataBuilder = 541 | List ( String, SchemaBuilder ) 542 | 543 | 544 | toSchemata : SchemataBuilder -> Result String (List ( String, Schema )) 545 | toSchemata = 546 | List.foldl 547 | (\( key, builder ) -> 548 | Result.andThen 549 | (\schemas -> 550 | builder 551 | |> toSchema 552 | |> Result.map (\schema -> schemas ++ [ ( key, schema ) ]) 553 | ) 554 | ) 555 | (Ok []) 556 | 557 | 558 | toListOfSchemas : List SchemaBuilder -> Result String (List Schema) 559 | toListOfSchemas = 560 | List.foldl 561 | (\builder -> 562 | Result.andThen 563 | (\schemas -> 564 | builder 565 | |> toSchema 566 | |> Result.map (\schema -> schemas ++ [ schema ]) 567 | ) 568 | ) 569 | (Ok []) 570 | 571 | 572 | 573 | -- updateWithSubSchema (\sub s -> { s | contains = sub }) 574 | 575 | 576 | updateWithSubSchema : (Maybe Schema -> (SubSchema -> SubSchema)) -> SchemaBuilder -> SchemaBuilder -> SchemaBuilder 577 | updateWithSubSchema fn subSchemaBuilder = 578 | case subSchemaBuilder |> toSchema of 579 | Ok s -> 580 | fn (Just s) 581 | |> updateSchema 582 | 583 | Err err -> 584 | appendError err 585 | 586 | 587 | updateWithSchemata : (Maybe Schemata -> (SubSchema -> SubSchema)) -> SchemataBuilder -> SchemaBuilder -> SchemaBuilder 588 | updateWithSchemata fn schemataBuilder = 589 | case schemataBuilder |> toSchemata of 590 | Ok schemata -> 591 | updateSchema (fn (Just <| Schemata schemata)) 592 | 593 | Err s -> 594 | appendError s 595 | 596 | 597 | updateWithListOfSchemas : (Maybe (List Schema) -> (SubSchema -> SubSchema)) -> List SchemaBuilder -> SchemaBuilder -> SchemaBuilder 598 | updateWithListOfSchemas fn schemasBuilder = 599 | case schemasBuilder |> toListOfSchemas of 600 | Ok ls -> 601 | updateSchema (fn (Just ls)) 602 | 603 | Err s -> 604 | appendError s 605 | 606 | 607 | toString : String -> String 608 | toString = 609 | Encode.string >> Encode.encode 0 610 | 611 | 612 | exclusiveBoundaryToString : ExclusiveBoundary -> String 613 | exclusiveBoundaryToString eb = 614 | case eb of 615 | BoolBoundary b -> 616 | boolToString b 617 | 618 | NumberBoundary f -> 619 | String.fromFloat f 620 | 621 | 622 | boolToString : Bool -> String 623 | boolToString b = 624 | case b of 625 | True -> 626 | "true" 627 | 628 | False -> 629 | "false" 630 | 631 | 632 | {-| Encode schema into a builder code (elm) 633 | -} 634 | encode : Int -> Schema -> String 635 | encode level s = 636 | let 637 | indent : String 638 | indent = 639 | "\n" ++ String.repeat level " " 640 | 641 | pipe : String 642 | pipe = 643 | indent ++ "|> " 644 | 645 | comma : String 646 | comma = 647 | indent ++ ", " 648 | 649 | comma2 : String 650 | comma2 = 651 | indent ++ " , " 652 | 653 | comma4 : String 654 | comma4 = 655 | indent ++ " , " 656 | 657 | optionally : (a -> String) -> Maybe a -> String -> String -> String 658 | optionally fn val key res = 659 | case val of 660 | Just sLocal -> 661 | res ++ pipe ++ key ++ " " ++ fn sLocal 662 | 663 | Nothing -> 664 | res 665 | 666 | encodeItems : Items -> String -> String 667 | encodeItems items res = 668 | case items of 669 | ItemDefinition id -> 670 | res ++ pipe ++ "withItem " ++ (encode (level + 1) >> addParens) id 671 | 672 | ArrayOfItems aoi -> 673 | res ++ pipe ++ "withItem " ++ (aoi |> List.map (encode (level + 1)) |> String.join comma) 674 | 675 | NoItems -> 676 | res 677 | 678 | encodeDependency : String -> Dependency -> String 679 | encodeDependency key dep = 680 | case dep of 681 | PropSchema ps -> 682 | pipe ++ "withSchemaDependency \"" ++ key ++ "\" " ++ encode (level + 1) ps 683 | 684 | ArrayPropNames apn -> 685 | pipe 686 | ++ "withPropNamesDependency \"" 687 | ++ key 688 | ++ "\" [ " 689 | ++ (apn 690 | |> List.map (\sLocal -> "\"" ++ sLocal ++ "\"") 691 | |> String.join ", " 692 | ) 693 | ++ " ]" 694 | 695 | encodeDependencies : List ( String, Dependency ) -> String -> String 696 | encodeDependencies deps res = 697 | if List.isEmpty deps then 698 | res 699 | 700 | else 701 | res 702 | ++ pipe 703 | ++ "withDependencies" 704 | ++ (deps 705 | |> List.map (\( key, dep ) -> encodeDependency key dep) 706 | |> String.join pipe 707 | ) 708 | 709 | singleTypeToString : SingleType -> String 710 | singleTypeToString st = 711 | case st of 712 | StringType -> 713 | "string" 714 | 715 | IntegerType -> 716 | "integer" 717 | 718 | NumberType -> 719 | "number" 720 | 721 | BooleanType -> 722 | "boolean" 723 | 724 | ObjectType -> 725 | "object" 726 | 727 | ArrayType -> 728 | "array" 729 | 730 | NullType -> 731 | "null" 732 | 733 | encodeType : Type -> String -> String 734 | encodeType t res = 735 | case t of 736 | SingleType st -> 737 | res ++ pipe ++ "withType \"" ++ singleTypeToString st ++ "\"" 738 | 739 | NullableType st -> 740 | res ++ pipe ++ "withNullableType \"" ++ singleTypeToString st ++ "\"" 741 | 742 | UnionType ut -> 743 | res ++ pipe ++ "withUnionType [" ++ (ut |> List.map (singleTypeToString >> toString) |> String.join ", ") ++ "]" 744 | 745 | AnyType -> 746 | res 747 | 748 | encodeListSchemas : List Schema -> String 749 | encodeListSchemas l = 750 | l 751 | |> List.map (encode (level + 1)) 752 | |> String.join comma2 753 | |> (\sLocal -> indent ++ " [ " ++ sLocal ++ indent ++ " ]") 754 | 755 | encodeSchemata : Schemata -> String 756 | encodeSchemata (Schemata l) = 757 | l 758 | |> List.map (\( sLocal, x ) -> "( \"" ++ sLocal ++ "\"" ++ comma4 ++ encode (level + 2) x ++ indent ++ " )") 759 | |> String.join comma2 760 | |> (\sLocal -> indent ++ " [ " ++ sLocal ++ indent ++ " ]") 761 | 762 | addParens sLocal = 763 | "( " 764 | ++ sLocal 765 | ++ " )" 766 | in 767 | case s of 768 | BooleanSchema bs -> 769 | if bs then 770 | "boolSchema True" 771 | 772 | else 773 | "boolSchema False" 774 | 775 | ObjectSchema os -> 776 | [ encodeType os.type_ 777 | , optionally toString os.id "withId" 778 | , optionally toString os.ref "withRef" 779 | , optionally toString os.title "withTitle" 780 | , optionally toString os.description "withDescription" 781 | , optionally (\x -> x |> Encode.encode 0 |> toString |> (\xLocal -> "(" ++ xLocal ++ " |> Decode.decodeString Decode.value |> Result.withDefault Encode.null)")) os.default "withDefault" 782 | , optionally (\examples -> examples |> Encode.list identity |> Encode.encode 0) os.examples "withExamples" 783 | , optionally encodeSchemata os.definitions "withDefinitions" 784 | , optionally String.fromFloat os.multipleOf "withMultipleOf" 785 | , optionally String.fromFloat os.maximum "withMaximum" 786 | , optionally exclusiveBoundaryToString os.exclusiveMaximum "withExclusiveMaximum" 787 | , optionally String.fromFloat os.minimum "withMinimum" 788 | , optionally exclusiveBoundaryToString os.exclusiveMinimum "withExclusiveMinimum" 789 | , optionally String.fromInt os.maxLength "withMaxLength" 790 | , optionally String.fromInt os.minLength "withMinLength" 791 | , optionally toString os.pattern "withPattern" 792 | , optionally toString os.format "withFormat" 793 | , encodeItems os.items 794 | , optionally (encode (level + 1) >> addParens) os.additionalItems "withAdditionalItems" 795 | , optionally String.fromInt os.maxItems "withMaxItems" 796 | , optionally String.fromInt os.minItems "withMinItems" 797 | , optionally boolToString os.uniqueItems "withUniqueItems" 798 | , optionally (encode (level + 1) >> addParens) os.contains "withContains" 799 | , optionally String.fromInt os.maxProperties "withMaxProperties" 800 | , optionally String.fromInt os.minProperties "withMinProperties" 801 | , optionally (\sLocal -> sLocal |> List.map Encode.string |> Encode.list identity |> Encode.encode 0) os.required "withRequired" 802 | , optionally encodeSchemata os.properties "withProperties" 803 | , optionally encodeSchemata os.patternProperties "withPatternProperties" 804 | , optionally (encode (level + 1) >> addParens) os.additionalProperties "withAdditionalProperties" 805 | , encodeDependencies os.dependencies 806 | , optionally (encode (level + 1) >> addParens) os.propertyNames "withPropertyNames" 807 | , optionally (\examples -> examples |> Encode.list identity |> Encode.encode 0 |> (\x -> "( " ++ x ++ " |> List.map Encode.string )")) os.enum "withEnum" 808 | , optionally (Encode.encode 0 >> addParens) os.const "withConst" 809 | , optionally encodeListSchemas os.allOf "withAllOf" 810 | , optionally encodeListSchemas os.anyOf "withAnyOf" 811 | , optionally encodeListSchemas os.oneOf "withOneOf" 812 | , optionally (encode (level + 1) >> addParens) os.not "withNot" 813 | ] 814 | |> List.foldl identity "buildSchema" 815 | -------------------------------------------------------------------------------- /src/Json/Schema/Definitions.elm: -------------------------------------------------------------------------------- 1 | module Json.Schema.Definitions exposing 2 | ( Schema(..), SubSchema, Schemata(..), Items(..), Dependency(..), Type(..), SingleType(..), blankSchema, blankSubSchema, ExclusiveBoundary(..) 3 | , decoder, encode 4 | , stringToType, getCustomKeywordValue 5 | ) 6 | 7 | {-| This module contains low-level structures JSON Schema build from. 8 | Normally you wouldn't need to use any of those definitions. 9 | 10 | If you really need this low-level API you might need [JSON Schema spec](http://json-schema.org/documentation.html) as guidance. 11 | 12 | Feel free to open [issue](https://github.com/1602/json-schema) to describe your use-case, it will affect development roadmap of this library. 13 | 14 | 15 | # Definitions 16 | 17 | @docs Schema, SubSchema, Schemata, Items, Dependency, Type, SingleType, blankSchema, blankSubSchema, ExclusiveBoundary 18 | 19 | 20 | # Decoding / encoding 21 | 22 | @docs decoder, encode 23 | 24 | 25 | # Misc 26 | 27 | @docs stringToType, getCustomKeywordValue 28 | 29 | -} 30 | 31 | import Json.Decode as Decode exposing (Decoder, Value, andThen, bool, fail, field, float, int, lazy, list, nullable, string, succeed, value) 32 | import Json.Decode.Pipeline as DecodePipeline exposing (optional, optionalAt, required, requiredAt) 33 | import Json.Encode as Encode 34 | import Util exposing (foldResults, isInt, resultToDecoder) 35 | 36 | 37 | {-| Schema can be either boolean or actual object containing validation and meta properties 38 | -} 39 | type Schema 40 | = BooleanSchema Bool 41 | | ObjectSchema SubSchema 42 | 43 | 44 | {-| This object holds all draft-6 schema properties 45 | -} 46 | type alias SubSchema = 47 | { type_ : Type 48 | , id : Maybe String 49 | , ref : 50 | Maybe String 51 | 52 | -- meta 53 | , title : Maybe String 54 | , description : Maybe String 55 | , default : Maybe Value 56 | , examples : Maybe (List Value) 57 | , definitions : 58 | Maybe Schemata 59 | 60 | -- numeric validations 61 | , multipleOf : Maybe Float 62 | , maximum : Maybe Float 63 | , exclusiveMaximum : Maybe ExclusiveBoundary 64 | , minimum : Maybe Float 65 | , exclusiveMinimum : 66 | Maybe ExclusiveBoundary 67 | 68 | -- string validations 69 | , maxLength : Maybe Int 70 | , minLength : Maybe Int 71 | , pattern : Maybe String 72 | , format : 73 | Maybe String 74 | 75 | -- array validations 76 | , items : Items 77 | , additionalItems : Maybe Schema 78 | , maxItems : Maybe Int 79 | , minItems : Maybe Int 80 | , uniqueItems : Maybe Bool 81 | , contains : 82 | Maybe Schema 83 | 84 | -- object validations 85 | , maxProperties : Maybe Int 86 | , minProperties : Maybe Int 87 | , required : Maybe (List String) 88 | , properties : Maybe Schemata 89 | , patternProperties : Maybe Schemata 90 | , additionalProperties : Maybe Schema 91 | , dependencies : List ( String, Dependency ) 92 | , propertyNames : 93 | Maybe Schema 94 | 95 | -- misc validations 96 | , enum : Maybe (List Value) 97 | , const : Maybe Value 98 | , allOf : Maybe (List Schema) 99 | , anyOf : Maybe (List Schema) 100 | , oneOf : Maybe (List Schema) 101 | , not : Maybe Schema 102 | , source : Value 103 | } 104 | 105 | 106 | {-| List of schema-properties used in properties, definitions and patternProperties 107 | -} 108 | type Schemata 109 | = Schemata (List ( String, Schema )) 110 | 111 | 112 | {-| Items definition. 113 | -} 114 | type Items 115 | = NoItems 116 | | ItemDefinition Schema 117 | | ArrayOfItems (List Schema) 118 | 119 | 120 | {-| Dependency definition. 121 | -} 122 | type Dependency 123 | = ArrayPropNames (List String) 124 | | PropSchema Schema 125 | 126 | 127 | {-| Exclusive boundaries. Compatibility layer between draft-04 and draft-06 (keywords `exclusiveMinimum` and `exclusiveMaximum` has been changed from a boolean to a number to be consistent with the principle of keyword independence). Since we currently keep both draft-4 and draft-6 as same type definition, we have a union of `Bool` and `Float` here. It might be not a bad idea to separate type definitions for different drafts of JSON Schema, current API decision will be reconsidered when future versions of JSON Schema will arrive. 128 | -} 129 | type ExclusiveBoundary 130 | = BoolBoundary Bool 131 | | NumberBoundary Float 132 | 133 | 134 | {-| Create blank JSON Schema `{}`. 135 | -} 136 | blankSchema : Schema 137 | blankSchema = 138 | ObjectSchema blankSubSchema 139 | 140 | 141 | {-| -} 142 | blankSubSchema : SubSchema 143 | blankSubSchema = 144 | { type_ = AnyType 145 | , id = Nothing 146 | , ref = Nothing 147 | , title = Nothing 148 | , description = Nothing 149 | , default = Nothing 150 | , examples = Nothing 151 | , definitions = Nothing 152 | , multipleOf = Nothing 153 | , maximum = Nothing 154 | , exclusiveMaximum = Nothing 155 | , minimum = Nothing 156 | , exclusiveMinimum = Nothing 157 | , maxLength = Nothing 158 | , minLength = Nothing 159 | , pattern = Nothing 160 | , format = Nothing 161 | , items = NoItems 162 | , additionalItems = Nothing 163 | , maxItems = Nothing 164 | , minItems = Nothing 165 | , uniqueItems = Nothing 166 | , contains = Nothing 167 | , maxProperties = Nothing 168 | , minProperties = Nothing 169 | , required = Nothing 170 | , properties = Nothing 171 | , patternProperties = Nothing 172 | , additionalProperties = Nothing 173 | , dependencies = [] 174 | , propertyNames = Nothing 175 | , enum = Nothing 176 | , const = Nothing 177 | , allOf = Nothing 178 | , anyOf = Nothing 179 | , oneOf = Nothing 180 | , not = Nothing 181 | , source = Encode.object [] 182 | } 183 | 184 | 185 | type RowEncoder a 186 | = RowEncoder (Maybe a) String (a -> Value) 187 | 188 | 189 | {-| -} 190 | encode : Schema -> Value 191 | encode s = 192 | let 193 | optionally : (a -> Value) -> Maybe a -> String -> List ( String, Value ) -> List ( String, Value ) 194 | optionally fn val key res = 195 | let 196 | result = 197 | res 198 | |> List.filter (\( k, _ ) -> k /= key) 199 | in 200 | case val of 201 | Just schema -> 202 | ( key, fn schema ) :: result 203 | 204 | Nothing -> 205 | result 206 | 207 | encodeItems : Items -> List ( String, Value ) -> List ( String, Value ) 208 | encodeItems items res = 209 | case items of 210 | ItemDefinition id -> 211 | ( "items", encode id ) :: res 212 | 213 | ArrayOfItems aoi -> 214 | ( "items", aoi |> Encode.list encode ) :: res 215 | 216 | NoItems -> 217 | res 218 | 219 | encodeDependency : Dependency -> Value 220 | encodeDependency dep = 221 | case dep of 222 | PropSchema ps -> 223 | encode ps 224 | 225 | ArrayPropNames apn -> 226 | apn |> Encode.list Encode.string 227 | 228 | encodeDependencies : List ( String, Dependency ) -> List ( String, Value ) -> List ( String, Value ) 229 | encodeDependencies deps res = 230 | if List.isEmpty deps then 231 | res 232 | 233 | else 234 | ( "dependencies", deps |> List.map (\( key, dep ) -> ( key, encodeDependency dep )) |> Encode.object ) :: res 235 | 236 | singleTypeToString : SingleType -> String 237 | singleTypeToString st = 238 | case st of 239 | StringType -> 240 | "string" 241 | 242 | IntegerType -> 243 | "integer" 244 | 245 | NumberType -> 246 | "number" 247 | 248 | BooleanType -> 249 | "boolean" 250 | 251 | ObjectType -> 252 | "object" 253 | 254 | ArrayType -> 255 | "array" 256 | 257 | NullType -> 258 | "null" 259 | 260 | encodeType : Type -> List ( String, Value ) -> List ( String, Value ) 261 | encodeType t res = 262 | case t of 263 | SingleType st -> 264 | ( "type", st |> singleTypeToString |> Encode.string ) :: res 265 | 266 | NullableType st -> 267 | ( "type", [ "null" |> Encode.string, st |> singleTypeToString |> Encode.string ] |> Encode.list identity ) :: res 268 | 269 | UnionType ut -> 270 | ( "type", ut |> Encode.list (singleTypeToString >> Encode.string) ) :: res 271 | 272 | AnyType -> 273 | res 274 | 275 | encodeListSchemas : List Schema -> Value 276 | encodeListSchemas l = 277 | l 278 | |> Encode.list encode 279 | 280 | encodeSchemata : Schemata -> Value 281 | encodeSchemata (Schemata listSchemas) = 282 | listSchemas 283 | |> List.map (\( key, schema ) -> ( key, encode schema )) 284 | |> Encode.object 285 | 286 | encodeExclusiveBoundary : ExclusiveBoundary -> Value 287 | encodeExclusiveBoundary eb = 288 | case eb of 289 | BoolBoundary b -> 290 | Encode.bool b 291 | 292 | NumberBoundary f -> 293 | Encode.float f 294 | 295 | source : SubSchema -> List ( String, Value ) 296 | source os = 297 | os.source 298 | |> Decode.decodeValue (Decode.keyValuePairs Decode.value) 299 | |> Result.withDefault [] 300 | in 301 | case s of 302 | BooleanSchema bs -> 303 | Encode.bool bs 304 | 305 | ObjectSchema os -> 306 | [ encodeType os.type_ 307 | , optionally Encode.string os.id "$id" 308 | , optionally Encode.string os.ref "$ref" 309 | , optionally Encode.string os.title "title" 310 | , optionally Encode.string os.description "description" 311 | , optionally identity os.default "default" 312 | , optionally (Encode.list identity) os.examples "examples" 313 | , optionally encodeSchemata os.definitions "definitions" 314 | , optionally Encode.float os.multipleOf "multipleOf" 315 | , optionally Encode.float os.maximum "maximum" 316 | , optionally encodeExclusiveBoundary os.exclusiveMaximum "exclusiveMaximum" 317 | , optionally Encode.float os.minimum "minimum" 318 | , optionally encodeExclusiveBoundary os.exclusiveMinimum "exclusiveMinimum" 319 | , optionally Encode.int os.maxLength "maxLength" 320 | , optionally Encode.int os.minLength "minLength" 321 | , optionally Encode.string os.pattern "pattern" 322 | , optionally Encode.string os.format "format" 323 | , encodeItems os.items 324 | , optionally encode os.additionalItems "additionalItems" 325 | , optionally Encode.int os.maxItems "maxItems" 326 | , optionally Encode.int os.minItems "minItems" 327 | , optionally Encode.bool os.uniqueItems "uniqueItems" 328 | , optionally encode os.contains "contains" 329 | , optionally Encode.int os.maxProperties "maxProperties" 330 | , optionally Encode.int os.minProperties "minProperties" 331 | , optionally (\list -> list |> Encode.list Encode.string) os.required "required" 332 | , optionally encodeSchemata os.properties "properties" 333 | , optionally encodeSchemata os.patternProperties "patternProperties" 334 | , optionally encode os.additionalProperties "additionalProperties" 335 | , encodeDependencies os.dependencies 336 | , optionally encode os.propertyNames "propertyNames" 337 | , optionally (Encode.list identity) os.enum "enum" 338 | , optionally identity os.const "const" 339 | , optionally encodeListSchemas os.allOf "allOf" 340 | , optionally encodeListSchemas os.anyOf "anyOf" 341 | , optionally encodeListSchemas os.oneOf "oneOf" 342 | , optionally encode os.not "not" 343 | ] 344 | |> List.foldl identity (source os) 345 | |> List.reverse 346 | |> Encode.object 347 | 348 | 349 | {-| -} 350 | decoder : Decoder Schema 351 | decoder = 352 | let 353 | singleType = 354 | string 355 | |> andThen singleTypeDecoder 356 | 357 | multipleTypes = 358 | string 359 | |> list 360 | |> andThen multipleTypesDecoder 361 | 362 | booleanSchemaDecoder = 363 | Decode.bool 364 | |> Decode.andThen 365 | (\b -> 366 | if b then 367 | succeed (BooleanSchema True) 368 | 369 | else 370 | succeed (BooleanSchema False) 371 | ) 372 | 373 | exclusiveBoundaryDecoder = 374 | Decode.oneOf [ Decode.bool |> Decode.map BoolBoundary, Decode.float |> Decode.map NumberBoundary ] 375 | 376 | objectSchemaDecoder = 377 | Decode.succeed SubSchema 378 | |> optional "type" 379 | (Decode.oneOf [ multipleTypes, Decode.map SingleType singleType ]) 380 | AnyType 381 | |> DecodePipeline.custom 382 | (Decode.map2 383 | (\a b -> 384 | if a == Nothing then 385 | b 386 | 387 | else 388 | a 389 | ) 390 | (field "$id" string |> Decode.maybe) 391 | (field "id" string |> Decode.maybe) 392 | ) 393 | |> optional "$ref" (nullable string) Nothing 394 | -- meta 395 | |> optional "title" (nullable string) Nothing 396 | |> optional "description" (nullable string) Nothing 397 | |> optional "default" (value |> Decode.map Just) Nothing 398 | |> optional "examples" (nullable <| list value) Nothing 399 | |> optional "definitions" (nullable <| lazy <| \_ -> schemataDecoder) Nothing 400 | -- number 401 | |> optional "multipleOf" (nullable float) Nothing 402 | |> optional "maximum" (nullable float) Nothing 403 | |> optional "exclusiveMaximum" (nullable exclusiveBoundaryDecoder) Nothing 404 | |> optional "minimum" (nullable float) Nothing 405 | |> optional "exclusiveMinimum" (nullable exclusiveBoundaryDecoder) Nothing 406 | -- string 407 | |> optional "maxLength" (nullable nonNegativeInt) Nothing 408 | |> optional "minLength" (nullable nonNegativeInt) Nothing 409 | |> optional "pattern" (nullable string) Nothing 410 | |> optional "format" (nullable string) Nothing 411 | -- array 412 | |> optional "items" (lazy (\_ -> itemsDecoder)) NoItems 413 | |> optional "additionalItems" (nullable <| lazy (\_ -> decoder)) Nothing 414 | |> optional "maxItems" (nullable nonNegativeInt) Nothing 415 | |> optional "minItems" (nullable nonNegativeInt) Nothing 416 | |> optional "uniqueItems" (nullable bool) Nothing 417 | |> optional "contains" (nullable <| lazy (\_ -> decoder)) Nothing 418 | |> optional "maxProperties" (nullable nonNegativeInt) Nothing 419 | |> optional "minProperties" (nullable nonNegativeInt) Nothing 420 | |> optional "required" (nullable (list string)) Nothing 421 | |> optional "properties" (nullable (lazy (\_ -> schemataDecoder))) Nothing 422 | |> optional "patternProperties" (nullable (lazy (\_ -> schemataDecoder))) Nothing 423 | |> optional "additionalProperties" (nullable <| lazy (\_ -> decoder)) Nothing 424 | |> optional "dependencies" (lazy (\_ -> dependenciesDecoder)) [] 425 | |> optional "propertyNames" (nullable <| lazy (\_ -> decoder)) Nothing 426 | |> optional "enum" (nullable nonEmptyUniqueArrayOfValuesDecoder) Nothing 427 | |> optional "const" (value |> Decode.map Just) Nothing 428 | |> optional "allOf" (nullable (lazy (\_ -> nonEmptyListOfSchemas))) Nothing 429 | |> optional "anyOf" (nullable (lazy (\_ -> nonEmptyListOfSchemas))) Nothing 430 | |> optional "oneOf" (nullable (lazy (\_ -> nonEmptyListOfSchemas))) Nothing 431 | |> optional "not" (nullable <| lazy (\_ -> decoder)) Nothing 432 | |> requiredAt [] Decode.value 433 | in 434 | Decode.oneOf 435 | [ booleanSchemaDecoder 436 | , objectSchemaDecoder 437 | |> Decode.andThen 438 | (\b -> 439 | succeed (ObjectSchema b) 440 | ) 441 | ] 442 | 443 | 444 | nonEmptyListOfSchemas : Decoder (List Schema) 445 | nonEmptyListOfSchemas = 446 | list (lazy (\_ -> decoder)) 447 | |> andThen failIfEmpty 448 | 449 | 450 | nonEmptyUniqueArrayOfValuesDecoder : Decoder (List Value) 451 | nonEmptyUniqueArrayOfValuesDecoder = 452 | list value 453 | |> andThen failIfValuesAreNotUnique 454 | |> andThen failIfEmpty 455 | 456 | 457 | failIfValuesAreNotUnique : List Value -> Decoder (List Value) 458 | failIfValuesAreNotUnique l = 459 | succeed l 460 | 461 | 462 | failIfEmpty : List a -> Decoder (List a) 463 | failIfEmpty l = 464 | if List.isEmpty l then 465 | fail "List is empty" 466 | 467 | else 468 | succeed l 469 | 470 | 471 | itemsDecoder : Decoder Items 472 | itemsDecoder = 473 | Decode.oneOf 474 | [ Decode.map ArrayOfItems <| list decoder 475 | , Decode.map ItemDefinition decoder 476 | ] 477 | 478 | 479 | dependenciesDecoder : Decoder (List ( String, Dependency )) 480 | dependenciesDecoder = 481 | Decode.oneOf 482 | [ Decode.map ArrayPropNames (list string) 483 | , Decode.map PropSchema decoder 484 | ] 485 | |> Decode.keyValuePairs 486 | 487 | 488 | nonNegativeInt : Decoder Int 489 | nonNegativeInt = 490 | int 491 | |> andThen 492 | (\x -> 493 | if x >= 0 then 494 | succeed x 495 | 496 | else 497 | fail "Expected non-negative int" 498 | ) 499 | 500 | 501 | {-| Type property in json schema can be a single type or array of them, this type definition wraps up this complexity, also it introduces concept of nullable type, which is array of "null" type and a single type speaking JSON schema language, but also a useful concept to treat it separately from list of types. 502 | -} 503 | type Type 504 | = AnyType 505 | | SingleType SingleType 506 | | NullableType SingleType 507 | | UnionType (List SingleType) 508 | 509 | 510 | {-| -} 511 | type SingleType 512 | = IntegerType 513 | | NumberType 514 | | StringType 515 | | BooleanType 516 | | ArrayType 517 | | ObjectType 518 | | NullType 519 | 520 | 521 | multipleTypesDecoder : List String -> Decoder Type 522 | multipleTypesDecoder lst = 523 | case lst of 524 | [ x, "null" ] -> 525 | Decode.map NullableType <| singleTypeDecoder x 526 | 527 | [ "null", x ] -> 528 | Decode.map NullableType <| singleTypeDecoder x 529 | 530 | [ x ] -> 531 | Decode.map SingleType <| singleTypeDecoder x 532 | 533 | otherList -> 534 | otherList 535 | |> List.sort 536 | |> List.map stringToType 537 | |> foldResults 538 | |> Result.andThen (Ok << UnionType) 539 | |> resultToDecoder 540 | 541 | 542 | {-| Attempt to parse string into a single type, it recognises the following list of types: 543 | 544 | - integer 545 | - number 546 | - string 547 | - boolean 548 | - array 549 | - object 550 | - null 551 | 552 | -} 553 | stringToType : String -> Result String SingleType 554 | stringToType s = 555 | case s of 556 | "integer" -> 557 | Ok IntegerType 558 | 559 | "number" -> 560 | Ok NumberType 561 | 562 | "string" -> 563 | Ok StringType 564 | 565 | "boolean" -> 566 | Ok BooleanType 567 | 568 | "array" -> 569 | Ok ArrayType 570 | 571 | "object" -> 572 | Ok ObjectType 573 | 574 | "null" -> 575 | Ok NullType 576 | 577 | _ -> 578 | Err ("Unknown type: " ++ s) 579 | 580 | 581 | singleTypeDecoder : String -> Decoder SingleType 582 | singleTypeDecoder s = 583 | case stringToType s of 584 | Ok st -> 585 | succeed st 586 | 587 | Err msg -> 588 | fail msg 589 | 590 | 591 | schemataDecoder : Decoder Schemata 592 | schemataDecoder = 593 | Decode.keyValuePairs (lazy (\_ -> decoder)) 594 | |> Decode.map Schemata 595 | 596 | 597 | {-| Return custom keyword value by its name, useful when dealing with additional meta information added along with standard JSON Schema keywords. 598 | -} 599 | getCustomKeywordValue : String -> Schema -> Maybe Value 600 | getCustomKeywordValue key schema = 601 | case schema of 602 | ObjectSchema os -> 603 | os.source 604 | |> Decode.decodeValue (Decode.keyValuePairs Decode.value) 605 | |> Result.withDefault [] 606 | |> List.filterMap 607 | (\( k, v ) -> 608 | if k == key then 609 | Just v 610 | 611 | else 612 | Nothing 613 | ) 614 | |> List.head 615 | 616 | _ -> 617 | Nothing 618 | -------------------------------------------------------------------------------- /src/Json/Schema/Examples.elm: -------------------------------------------------------------------------------- 1 | module Json.Schema.Examples exposing (bookingSchema, coreSchemaDraft6) 2 | 3 | import Json.Decode 4 | import Json.Encode as Encode 5 | import Json.Schema.Builder exposing (..) 6 | import Json.Schema.Definitions exposing (Schema, blankSchema) 7 | 8 | 9 | coreSchemaDraft6 : Schema 10 | coreSchemaDraft6 = 11 | buildSchema 12 | |> withUnionType [ "boolean", "object" ] 13 | |> withTitle "Core schema meta-schema" 14 | |> withDefault (Encode.object []) 15 | |> withDefinitions 16 | [ ( "schemaArray" 17 | , buildSchema 18 | |> withType "array" 19 | |> withItem (buildSchema |> withRef "#") 20 | |> withMinItems 1 21 | ) 22 | , ( "nonNegativeInteger" 23 | , buildSchema 24 | |> withType "integer" 25 | |> withMinimum 0 26 | ) 27 | , ( "nonNegativeIntegerDefault0" 28 | , buildSchema 29 | |> withAllOf 30 | [ buildSchema 31 | |> withRef "#/definitions/nonNegativeInteger" 32 | , buildSchema 33 | |> withDefault (Encode.int 0) 34 | ] 35 | ) 36 | , ( "simpleTypes" 37 | , buildSchema 38 | |> withEnum 39 | ([ "array" 40 | , "boolean" 41 | , "integer" 42 | , "null" 43 | , "number" 44 | , "object" 45 | , "string" 46 | ] 47 | |> List.map Encode.string 48 | ) 49 | ) 50 | , ( "stringArray" 51 | , buildSchema 52 | |> withType "array" 53 | |> withDefault ([] |> Encode.list) 54 | |> withItem (buildSchema |> withType "string") 55 | ) 56 | ] 57 | |> withProperties 58 | [ ( "$id" 59 | , buildSchema 60 | |> withType "string" 61 | |> withFormat "uri-reference" 62 | ) 63 | , ( "$schema" 64 | , buildSchema 65 | |> withType "string" 66 | |> withFormat "uri" 67 | ) 68 | , ( "$ref" 69 | , buildSchema 70 | |> withType "string" 71 | |> withFormat "uri-reference" 72 | ) 73 | , ( "title" 74 | , buildSchema |> withType "string" 75 | ) 76 | , ( "description" 77 | , buildSchema |> withType "string" 78 | ) 79 | , ( "default" 80 | , buildSchema 81 | ) 82 | , ( "multipleOf" 83 | , buildSchema 84 | |> withType "number" 85 | |> withExclusiveMinimum 0 86 | ) 87 | , ( "maximum" 88 | , buildSchema |> withType "number" 89 | ) 90 | , ( "exclusiveMaximum" 91 | , buildSchema |> withType "number" 92 | ) 93 | , ( "minimum" 94 | , buildSchema |> withType "number" 95 | ) 96 | , ( "exclusiveMinimum" 97 | , buildSchema |> withType "number" 98 | ) 99 | , ( "maxLength" 100 | , buildSchema |> withRef "#/definitions/nonNegativeInteger" 101 | ) 102 | , ( "minLength" 103 | , buildSchema |> withRef "#/definitions/nonNegativeIntegerDefault0" 104 | ) 105 | , ( "pattern" 106 | , buildSchema 107 | |> withType "string" 108 | |> withFormat "regex" 109 | ) 110 | , ( "additionalItems" 111 | , buildSchema |> withRef "#" 112 | ) 113 | , ( "items" 114 | , buildSchema 115 | |> withDefault (Encode.object []) 116 | |> withAnyOf 117 | [ buildSchema |> withRef "#" 118 | , buildSchema |> withRef "#/definitions/schemaArray" 119 | ] 120 | ) 121 | , ( "maxItems" 122 | , buildSchema |> withRef "#/definitions/nonNegativeInteger" 123 | ) 124 | , ( "minItems" 125 | , buildSchema |> withRef "#/definitions/nonNegativeIntegerDefault0" 126 | ) 127 | , ( "uniqueItems" 128 | , buildSchema 129 | |> withType "boolean" 130 | |> withDefault (Encode.bool False) 131 | ) 132 | , ( "contains" 133 | , buildSchema |> withRef "#" 134 | ) 135 | , ( "maxProperties" 136 | , buildSchema |> withRef "#/definitions/nonNegativeInteger" 137 | ) 138 | , ( "minProperties" 139 | , buildSchema |> withRef "#/definitions/nonNegativeIntegerDefault0" 140 | ) 141 | , ( "required" 142 | , buildSchema |> withRef "#/definitions/stringArray" 143 | ) 144 | , ( "additionalProperties" 145 | , buildSchema |> withRef "#" 146 | ) 147 | , ( "definitions" 148 | , buildSchema 149 | |> withType "object" 150 | |> withDefault (Encode.object []) 151 | |> withAdditionalProperties (buildSchema |> withRef "#") 152 | ) 153 | , ( "properties" 154 | , buildSchema 155 | |> withType "object" 156 | |> withDefault (Encode.object []) 157 | |> withAdditionalProperties (buildSchema |> withRef "#") 158 | ) 159 | , ( "patternProperties" 160 | , buildSchema 161 | |> withType "object" 162 | |> withDefault (Encode.object []) 163 | |> withAdditionalProperties (buildSchema |> withRef "#") 164 | -- |> withPropertyNames (buildSchema |> withFormat "regex") 165 | ) 166 | , ( "dependencies" 167 | , buildSchema 168 | |> withType "object" 169 | |> withAdditionalProperties 170 | (buildSchema 171 | |> withAnyOf 172 | [ buildSchema |> withRef "#" 173 | , buildSchema |> withRef "#/definitions/stringArray" 174 | ] 175 | ) 176 | ) 177 | , ( "propertyNames" 178 | , buildSchema |> withRef "#" 179 | ) 180 | , ( "const" 181 | , buildSchema 182 | ) 183 | , ( "enum" 184 | , buildSchema 185 | |> withType "array" 186 | |> withMinItems 1 187 | |> withUniqueItems True 188 | ) 189 | , ( "type" 190 | , buildSchema 191 | |> withAnyOf 192 | [ buildSchema 193 | |> withRef "#/definitions/simpleTypes" 194 | , buildSchema 195 | |> withType "array" 196 | |> withItem (buildSchema |> withRef "#/definitions/simpleTypes") 197 | |> withMinItems 1 198 | |> withUniqueItems True 199 | ] 200 | ) 201 | , ( "format" 202 | , buildSchema |> withType "string" 203 | ) 204 | , ( "allOf" 205 | , buildSchema |> withRef "#/definitions/schemaArray" 206 | ) 207 | , ( "anyOf" 208 | , buildSchema |> withRef "#/definitions/schemaArray" 209 | ) 210 | , ( "oneOf" 211 | , buildSchema |> withRef "#/definitions/schemaArray" 212 | ) 213 | , ( "not" 214 | , buildSchema |> withRef "#" 215 | ) 216 | ] 217 | |> toSchema 218 | |> Result.withDefault blankSchema 219 | 220 | 221 | bookingSchema : Schema 222 | bookingSchema = 223 | buildSchema 224 | |> withType "object" 225 | |> withDefinitions 226 | [ ( "basicPerson" 227 | , buildSchema 228 | |> withType "object" 229 | |> withTitle "Basic Person" 230 | |> withDescription "Minimal information representing a person" 231 | |> withRequired [ "title", "firstName", "lastName" ] 232 | |> withProperties 233 | [ ( "title" 234 | , buildSchema 235 | |> withEnum ([ "mr", "miss", "ms", "mrs" ] |> List.map Encode.string) 236 | ) 237 | , ( "firstName" 238 | , buildSchema 239 | |> withType "string" 240 | ) 241 | , ( "lastName" 242 | , buildSchema 243 | |> withType "string" 244 | ) 245 | ] 246 | ) 247 | , ( "personAddress" 248 | , buildSchema 249 | |> withTitle "Person+Address" 250 | |> withDescription "Weird combination of a person with an address, early days [Selective breeding](https://en.wikipedia.org/wiki/Selective_breeding) experiment." 251 | |> withAllOf 252 | [ buildSchema 253 | |> withRef "#/definitions/basicPerson" 254 | , buildSchema 255 | |> withRef "#/definitions/address" 256 | ] 257 | ) 258 | , ( "phone" 259 | , buildSchema 260 | |> withType "object" 261 | |> withTitle "Phone number" 262 | |> withRequired [ "countryCode", "number" ] 263 | |> withProperties 264 | [ ( "countryCode" 265 | , buildSchema 266 | |> withRef "#/definitions/countryCode" 267 | ) 268 | , ( "number" 269 | , buildSchema 270 | |> withType "string" 271 | |> withDescription "Mobile phone number (numbers only, excluding country code)" 272 | |> withMinLength 10 273 | ) 274 | ] 275 | ) 276 | , ( "price" 277 | , buildSchema 278 | |> withType "object" 279 | |> withRequired [ "currencyCode", "value" ] 280 | |> withProperties 281 | [ ( "currencyCode" 282 | , buildSchema 283 | |> withRef "#/definitions/currencyCode" 284 | ) 285 | , ( "value" 286 | , buildSchema 287 | |> withType "integer" 288 | |> withDescription "A positive integer in the smallest currency unit (that is, 100 pence for £1.00)" 289 | |> withMinimum 0 290 | ) 291 | ] 292 | |> withAdditionalProperties (boolSchema False) 293 | ) 294 | , ( "paymentCard" 295 | , buildSchema 296 | |> withType "object" 297 | |> withTitle "Payment Card" 298 | |> withDescription "Note that instead of card number `panToken` must be supplied because of PCI DSS Compliance limitations" 299 | |> withRequired [ "type", "brand", "expirationDate", "name", "cvv" ] 300 | |> withProperties 301 | [ ( "type" 302 | , buildSchema 303 | |> withType "string" 304 | |> withEnum ([ "debit", "credit" ] |> List.map Encode.string) 305 | ) 306 | , ( "brand" 307 | , buildSchema 308 | |> withType "string" 309 | |> withEnum ([ "visa", "mastercard", "amex" ] |> List.map Encode.string) 310 | ) 311 | , ( "panToken" 312 | , buildSchema 313 | |> withType "string" 314 | |> withMinLength 20 315 | ) 316 | , ( "expirationDate" 317 | , buildSchema 318 | |> withType "string" 319 | |> withMaxLength 7 320 | |> withMinLength 7 321 | |> withPattern "^20[0-9]{2}-(?:0[1-9]|1[0-2])$" 322 | ) 323 | , ( "name" 324 | , buildSchema 325 | |> withType "string" 326 | ) 327 | , ( "cvv" 328 | , buildSchema 329 | |> withType "string" 330 | |> withMaxLength 4 331 | |> withMinLength 3 332 | ) 333 | ] 334 | ) 335 | , ( "address" 336 | , buildSchema 337 | |> withType "object" 338 | |> withRequired [ "line1", "city", "postcode", "countryCode" ] 339 | |> withProperties 340 | [ ( "line1" 341 | , buildSchema 342 | |> withType "string" 343 | |> withTitle "Address line 1" 344 | |> withDescription "Street name with house number" 345 | ) 346 | , ( "line2" 347 | , buildSchema 348 | |> withType "string" 349 | |> withTitle "Address line 2" 350 | |> withDescription "Additional address info" 351 | ) 352 | , ( "city" 353 | , buildSchema 354 | |> withType "string" 355 | ) 356 | , ( "postcode" 357 | , buildSchema 358 | |> withType "string" 359 | ) 360 | , ( "countryCode" 361 | , buildSchema 362 | |> withRef "#/definitions/countryCode" 363 | ) 364 | ] 365 | ) 366 | , ( "datePlace" 367 | , buildSchema 368 | |> withType "object" 369 | |> withRequired [ "dateTime", "airportCode" ] 370 | |> withProperties 371 | [ ( "countryCode" 372 | , buildSchema 373 | |> withRef "#/definitions/countryCode" 374 | ) 375 | , ( "dateTime" 376 | , buildSchema 377 | |> withType "string" 378 | |> withTitle "Date-Time" 379 | |> withDescription "Date and time of flight (airport local time)" 380 | |> withPattern "^20[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-3][0-9]) [012][0-9]:[0-5][0-9]$" 381 | ) 382 | , ( "airportCode" 383 | , buildSchema 384 | |> withType "string" 385 | |> withTitle "Airport Code" 386 | |> withDescription "International Air Transport Association airport code" 387 | |> withMaxLength 3 388 | |> withMinLength 3 389 | |> withPattern "^[A-Z]{3}$" 390 | ) 391 | ] 392 | |> withPropertyNames buildSchema 393 | |> withEnum ([ "dateTime", "airportCode", "countryCode" ] |> List.map Encode.string) 394 | ) 395 | , ( "currencyCode" 396 | , buildSchema 397 | |> withType "string" 398 | |> withDescription "3-letter ISO code representing the currency. __Lowercase__." 399 | |> withMaxLength 3 400 | |> withMinLength 3 401 | |> withEnum ([ "all", "afn", "ars", "awg", "aud", "azn", "bsd", "bbd", "byn", "bzd", "bmd", "bob", "bam", "bwp", "bgn", "brl", "bnd", "khr", "cad", "kyd", "clp", "cny", "cop", "crc", "hrk", "cup", "czk", "dkk", "dop", "xcd", "egp", "svc", "eur", "fkp", "fjd", "ghs", "gip", "gtq", "ggp", "gyd", "hnl", "hkd", "huf", "isk", "inr", "idr", "irr", "imp", "ils", "jmd", "jpy", "jep", "kzt", "kpw", "krw", "kgs", "lak", "lbp", "lrd", "mkd", "myr", "mur", "mxn", "mnt", "mzn", "nad", "npr", "ang", "nzd", "nio", "ngn", "nok", "omr", "pkr", "pab", "pyg", "pen", "php", "pln", "qar", "ron", "rub", "shp", "sar", "rsd", "scr", "sgd", "sbd", "sos", "zar", "lkr", "sek", "chf", "srd", "syp", "twd", "thb", "ttd", "try", "tvd", "uah", "gbp", "usd", "uyu", "uzs", "vef", "vnd", "yer", "zwd" ] |> List.map Encode.string) 402 | ) 403 | , ( "countryCode" 404 | , buildSchema 405 | |> withType "string" 406 | |> withTitle "ISO code representing the country" 407 | |> withDescription "2-letter ISO code representing the country. United Kingdom is officially assigned the alpha-2 code `gb` rather than `uk`. __Lowercase__." 408 | |> withMaxLength 2 409 | |> withMinLength 2 410 | |> withEnum ([ "af", "ax", "al", "dz", "as", "ad", "ao", "ai", "aq", "ag", "ar", "am", "aw", "au", "at", "az", "bs", "bh", "bd", "bb", "by", "be", "bz", "bj", "bm", "bt", "bo", "bq", "ba", "bw", "bv", "br", "io", "bn", "bg", "bf", "bi", "kh", "cm", "ca", "cv", "ky", "cf", "td", "cl", "cn", "cx", "cc", "co", "km", "cg", "cd", "ck", "cr", "ci", "hr", "cu", "cw", "cy", "cz", "dk", "dj", "dm", "do", "ec", "eg", "sv", "gq", "er", "ee", "et", "fk", "fo", "fj", "fi", "fr", "gf", "pf", "tf", "ga", "gm", "ge", "de", "gh", "gi", "gr", "gl", "gd", "gp", "gu", "gt", "gg", "gn", "gw", "gy", "ht", "hm", "va", "hn", "hk", "hu", "is", "in", "id", "ir", "iq", "ie", "im", "il", "it", "jm", "jp", "je", "jo", "kz", "ke", "ki", "kp", "kr", "kw", "kg", "la", "lv", "lb", "ls", "lr", "ly", "li", "lt", "lu", "mo", "mk", "mg", "mw", "my", "mv", "ml", "mt", "mh", "mq", "mr", "mu", "yt", "mx", "fm", "md", "mc", "mn", "me", "ms", "ma", "mz", "mm", "na", "nr", "np", "nl", "nc", "nz", "ni", "ne", "ng", "nu", "nf", "mp", "no", "om", "pk", "pw", "ps", "pa", "pg", "py", "pe", "ph", "pn", "pl", "pt", "pr", "qa", "re", "ro", "ru", "rw", "bl", "sh", "kn", "lc", "mf", "pm", "vc", "ws", "sm", "st", "sa", "sn", "rs", "sc", "sl", "sg", "sx", "sk", "si", "sb", "so", "za", "gs", "ss", "es", "lk", "sd", "sr", "sj", "sz", "se", "ch", "sy", "tw", "tj", "tz", "th", "tl", "tg", "tk", "to", "tt", "tn", "tr", "tm", "tc", "tv", "ug", "ua", "ae", "gb", "us", "um", "uy", "uz", "vu", "ve", "vn", "vg", "vi", "wf", "eh", "ye", "zm", "zw" ] |> List.map Encode.string) 411 | ) 412 | ] 413 | |> withRequired [ "url", "account", "passengers", "payment", "flight" ] 414 | |> withProperties 415 | [ ( "url" 416 | , buildSchema 417 | |> withType "string" 418 | |> withFormat "uri" 419 | ) 420 | , ( "account" 421 | , buildSchema 422 | |> withType "object" 423 | |> withRequired [ "email", "phone", "isExisting" ] 424 | |> withProperties 425 | [ ( "email" 426 | , buildSchema 427 | |> withType "string" 428 | |> withFormat "email" 429 | ) 430 | , ( "password" 431 | , buildSchema 432 | |> withType "string" 433 | |> withFormat "string" 434 | ) 435 | , ( "phone" 436 | , buildSchema 437 | |> withRef "#/definitions/phone" 438 | ) 439 | , ( "isExisting" 440 | , buildSchema 441 | |> withType "boolean" 442 | |> withEnum ([ True, False ] |> List.map Encode.bool) 443 | ) 444 | ] 445 | ) 446 | , ( "flight" 447 | , buildSchema 448 | |> withType "object" 449 | |> withRequired [ "cabinClass", "from", "to", "price" ] 450 | |> withProperties 451 | [ ( "from" 452 | , buildSchema 453 | |> withRef "#/definitions/datePlace" 454 | ) 455 | , ( "to" 456 | , buildSchema 457 | |> withRef "#/definitions/datePlace" 458 | ) 459 | , ( "return" 460 | , buildSchema 461 | |> withType "object" 462 | |> withRequired [ "from", "to" ] 463 | |> withProperties 464 | [ ( "from" 465 | , buildSchema 466 | |> withRef "#/definitions/datePlace" 467 | ) 468 | , ( "to" 469 | , buildSchema 470 | |> withRef "#/definitions/datePlace" 471 | ) 472 | ] 473 | ) 474 | , ( "price" 475 | , buildSchema 476 | |> withRef "#/definitions/price" 477 | ) 478 | , ( "cabinClass" 479 | , buildSchema 480 | |> withType "string" 481 | |> withEnum ([ "economy", "economy premium", "business", "first" ] |> List.map Encode.string) 482 | ) 483 | ] 484 | |> withAdditionalProperties (boolSchema True) 485 | ) 486 | , ( "passengers" 487 | , buildSchema 488 | |> withType "array" 489 | |> withItem 490 | (buildSchema 491 | |> withType "object" 492 | |> withRequired [ "title", "firstName", "lastName", "dateOfBirth", "hasHoldLuggage" ] 493 | |> withProperties 494 | [ ( "title" 495 | , buildSchema 496 | |> withEnum ([ "mr", "miss", "ms", "mrs" ] |> List.map Encode.string) 497 | ) 498 | , ( "firstName" 499 | , buildSchema 500 | |> withType "string" 501 | ) 502 | , ( "lastName" 503 | , buildSchema 504 | |> withType "string" 505 | ) 506 | , ( "dateOfBirth" 507 | , buildSchema 508 | |> withType "string" 509 | |> withFormat "date" 510 | ) 511 | , ( "hasHoldLuggage" 512 | , buildSchema 513 | |> withType "boolean" 514 | |> withEnum ([ True, False ] |> List.map Encode.bool) 515 | ) 516 | , ( "id" 517 | , buildSchema 518 | |> withType "object" 519 | |> withProperties 520 | [ ( "type" 521 | , buildSchema 522 | |> withType "string" 523 | ) 524 | , ( "number" 525 | , buildSchema 526 | |> withType "string" 527 | ) 528 | , ( "expDate" 529 | , buildSchema 530 | |> withType "string" 531 | |> withFormat "date" 532 | ) 533 | , ( "countryCode" 534 | , buildSchema 535 | |> withRef "#/definitions/countryCode" 536 | ) 537 | ] 538 | ) 539 | ] 540 | ) 541 | |> withMaxItems 1 542 | |> withMinItems 1 543 | ) 544 | , ( "payment" 545 | , buildSchema 546 | |> withType "object" 547 | |> withRequired [ "card", "address" ] 548 | |> withProperties 549 | [ ( "card" 550 | , buildSchema 551 | |> withRef "#/definitions/paymentCard" 552 | ) 553 | , ( "address" 554 | , buildSchema 555 | |> withRef "#/definitions/personAddress" 556 | ) 557 | ] 558 | ) 559 | ] 560 | |> withAdditionalProperties (boolSchema False) 561 | |> toSchema 562 | |> Result.withDefault blankSchema 563 | -------------------------------------------------------------------------------- /src/Json/Schema/Helpers.elm: -------------------------------------------------------------------------------- 1 | module Json.Schema.Helpers exposing 2 | ( ImpliedType 3 | , collectIds 4 | , typeToList 5 | --, implyType 6 | --, for 7 | 8 | , typeToString 9 | , whenObjectSchema 10 | --, makeJsonPointer 11 | -- , resolve 12 | --, resolveReference 13 | --, calcSubSchemaType 14 | 15 | ) 16 | 17 | import Dict exposing (Dict) 18 | import Json.Decode as Decode exposing (Value, decodeString, decodeValue) 19 | import Json.Encode as Encode 20 | import Json.Schema.Definitions as Schema 21 | exposing 22 | ( Items(..) 23 | , Schema(..) 24 | , Schemata(..) 25 | , SingleType(..) 26 | , SubSchema 27 | , Type(..) 28 | , blankSchema 29 | , blankSubSchema 30 | ) 31 | import Ref exposing (SchemataPool, parseJsonPointer, resolveReference) 32 | 33 | 34 | type alias ImpliedType = 35 | { type_ : Type 36 | , schema : SubSchema 37 | , error : Maybe String 38 | } 39 | 40 | 41 | singleTypeToString : SingleType -> String 42 | singleTypeToString st = 43 | case st of 44 | StringType -> 45 | "string" 46 | 47 | IntegerType -> 48 | "integer" 49 | 50 | NumberType -> 51 | "number" 52 | 53 | BooleanType -> 54 | "boolean" 55 | 56 | ObjectType -> 57 | "object" 58 | 59 | ArrayType -> 60 | "array" 61 | 62 | NullType -> 63 | "null" 64 | 65 | 66 | typeToString : Type -> String 67 | typeToString t = 68 | case t of 69 | NullableType NullType -> 70 | "null" 71 | 72 | NullableType st -> 73 | "nullable " ++ singleTypeToString st 74 | 75 | SingleType s -> 76 | singleTypeToString s 77 | 78 | UnionType l -> 79 | l 80 | |> List.map singleTypeToString 81 | |> String.join ", " 82 | 83 | AnyType -> 84 | "any" 85 | 86 | 87 | typeToList : Type -> List String 88 | typeToList t = 89 | case t of 90 | NullableType NullType -> 91 | [ "null" ] 92 | 93 | NullableType st -> 94 | [ "nullable " ++ singleTypeToString st ] 95 | 96 | SingleType s -> 97 | [ singleTypeToString s ] 98 | 99 | UnionType l -> 100 | l 101 | |> List.map singleTypeToString 102 | 103 | AnyType -> 104 | [] 105 | 106 | 107 | whenObjectSchema : Schema -> Maybe SubSchema 108 | whenObjectSchema schema = 109 | case schema of 110 | ObjectSchema os -> 111 | Just os 112 | 113 | BooleanSchema _ -> 114 | Nothing 115 | 116 | 117 | makeJsonPointer : ( Bool, String, List String ) -> String 118 | makeJsonPointer ( isPointer, ns, path ) = 119 | if isPointer then 120 | ("#" :: path) 121 | |> String.join "/" 122 | |> (++) ns 123 | 124 | else if List.isEmpty path then 125 | ns 126 | 127 | else 128 | path 129 | |> String.join "/" 130 | |> (++) (ns ++ "#") 131 | 132 | 133 | 134 | {- 135 | for : String -> Schema -> Maybe Schema 136 | for jsonPointer schema = 137 | jsonPointer 138 | |> parseJsonPointer 139 | |> List.foldl (weNeedToGoDeeper schema) (Just schema) 140 | 141 | 142 | implyType : Value -> Schema -> String -> ImpliedType 143 | implyType val schema subpath = 144 | let 145 | path = 146 | parseJsonPointer subpath 147 | 148 | actualValue = 149 | val 150 | |> Decode.decodeValue (Decode.at path Decode.value) 151 | |> Result.toMaybe 152 | in 153 | path 154 | |> List.foldl (weNeedToGoDeeper schema) (Just schema) 155 | |> Maybe.andThen whenObjectSchema 156 | |> Maybe.andThen (calcSubSchemaType actualValue schema) 157 | |> \x -> 158 | case x of 159 | Nothing -> 160 | { type_ = AnyType 161 | , schema = blankSubSchema 162 | , error = Just <| "Can't imply type: " ++ subpath 163 | } 164 | 165 | Just ( t, os ) -> 166 | { type_ = t 167 | , schema = os 168 | , error = Nothing 169 | } 170 | 171 | -} 172 | 173 | 174 | getListItem : Int -> List a -> Maybe a 175 | getListItem index list = 176 | let 177 | ( _, result ) = 178 | List.foldl 179 | (\item ( i, resultLocal ) -> 180 | if index == i then 181 | ( i + 1, Just item ) 182 | 183 | else 184 | ( i + 1, resultLocal ) 185 | ) 186 | ( 0, Nothing ) 187 | list 188 | in 189 | result 190 | 191 | 192 | setListItem : Int -> a -> List a -> List a 193 | setListItem index a list = 194 | List.indexedMap 195 | (\i item -> 196 | if index == i then 197 | a 198 | 199 | else 200 | item 201 | ) 202 | list 203 | 204 | 205 | 206 | {- 207 | calcSubSchemaType : Maybe Value -> Schema -> SubSchema -> Maybe ( Type, SubSchema ) 208 | calcSubSchemaType actualValue schema os = 209 | (case os.ref of 210 | Just ref -> 211 | ref 212 | |> resolveReference schema 213 | |> Maybe.andThen whenObjectSchema 214 | 215 | Nothing -> 216 | Just os 217 | ) 218 | |> Maybe.andThen 219 | (\os -> 220 | case os.type_ of 221 | AnyType -> 222 | [ os.anyOf 223 | , os.allOf 224 | , os.oneOf 225 | ] 226 | |> List.map (Maybe.withDefault []) 227 | |> List.concat 228 | |> tryAllSchemas actualValue schema 229 | |> \res -> 230 | if res == Nothing then 231 | if os.properties /= Nothing || os.additionalProperties /= Nothing then 232 | Just ( SingleType ObjectType, os ) 233 | else if os.enum /= Nothing then 234 | os.enum 235 | |> deriveTypeFromEnum 236 | |> \t -> Just ( t, os ) 237 | else if os == blankSubSchema then 238 | Just ( AnyType, os ) 239 | else 240 | Nothing 241 | else 242 | res 243 | 244 | UnionType ut -> 245 | if ut == [ BooleanType, ObjectType ] || ut == [ ObjectType, BooleanType ] then 246 | Just ( SingleType ObjectType, os ) 247 | else 248 | Just ( os.type_, os ) 249 | 250 | x -> 251 | Just ( x, os ) 252 | ) 253 | 254 | 255 | deriveTypeFromValue : Value -> Maybe Type 256 | deriveTypeFromValue val = 257 | case Decode.decodeValue Decode.string val of 258 | Ok _ -> 259 | Just <| SingleType StringType 260 | 261 | Err _ -> 262 | Nothing 263 | 264 | 265 | deriveTypeFromEnum : Maybe (List Value) -> Type 266 | deriveTypeFromEnum enum = 267 | enum 268 | |> Maybe.andThen List.head 269 | |> Maybe.andThen deriveTypeFromValue 270 | |> Maybe.withDefault AnyType 271 | -} 272 | {- 273 | resolve : Schema -> Schema -> Schema 274 | resolve rootSchema schema = 275 | schema 276 | |> whenObjectSchema 277 | |> Maybe.andThen 278 | (\os -> 279 | os.ref 280 | |> Maybe.andThen (resolveReference "" rootSchema) 281 | ) 282 | |> Maybe.withDefault schema 283 | -} 284 | {- 285 | weNeedToGoDeeper : Schema -> String -> Maybe Schema -> Maybe Schema 286 | weNeedToGoDeeper rootSchema key schema = 287 | schema 288 | |> Maybe.andThen whenObjectSchema 289 | |> Maybe.andThen 290 | (\os -> 291 | case os.ref of 292 | Just r -> 293 | resolveReference rootSchema r 294 | 295 | Nothing -> 296 | schema 297 | ) 298 | |> Maybe.andThen (findProperty key rootSchema) 299 | |> Maybe.map (resolve rootSchema) 300 | 301 | 302 | 303 | findProperty : String -> Schema -> Schema -> Maybe Schema 304 | findProperty name rootSchema schema = 305 | let 306 | os = 307 | whenObjectSchema schema 308 | in 309 | os 310 | |> Maybe.andThen .properties 311 | |> Maybe.andThen 312 | (\(Schemata pp) -> 313 | pp 314 | |> List.foldl 315 | (\( key, s ) res -> 316 | if res /= Nothing || key /= name then 317 | res 318 | else 319 | Just s 320 | ) 321 | Nothing 322 | ) 323 | |> (\r -> 324 | if r == Nothing then 325 | os 326 | |> Maybe.andThen .additionalProperties 327 | else 328 | r 329 | ) 330 | |> (\r -> 331 | if r == Nothing then 332 | os 333 | |> Maybe.andThen .anyOf 334 | |> Maybe.andThen 335 | (\anyOf -> 336 | anyOf 337 | |> List.foldl 338 | (\s r -> 339 | if r == Nothing then 340 | s 341 | |> resolve rootSchema 342 | |> findProperty name rootSchema 343 | else 344 | r 345 | ) 346 | Nothing 347 | ) 348 | else 349 | r 350 | ) 351 | |> \r -> 352 | if r == Nothing then 353 | Just blankSchema 354 | else 355 | r 356 | 357 | 358 | findDefinition : String -> Schemata -> Maybe SubSchema 359 | findDefinition ref (Schemata defs) = 360 | defs 361 | |> List.foldl 362 | (\( key, def ) res -> 363 | if res == Nothing && ("#/definitions/" ++ key) == ref then 364 | whenObjectSchema def 365 | else 366 | res 367 | ) 368 | Nothing 369 | 370 | 371 | tryAllSchemas : Maybe Value -> Schema -> List Schema -> Maybe ( Type, SubSchema ) 372 | tryAllSchemas actualValue rootSchema listSchemas = 373 | listSchemas 374 | |> List.map (resolve rootSchema) 375 | |> List.foldl 376 | (\schema res -> 377 | if res == Nothing then 378 | case actualValue of 379 | Just av -> 380 | case Validation.validate Ref.defaultPool av rootSchema schema of 381 | Ok _ -> 382 | schema 383 | |> whenObjectSchema 384 | |> Maybe.andThen (calcSubSchemaType actualValue rootSchema) 385 | 386 | Err _ -> 387 | Nothing 388 | 389 | Nothing -> 390 | schema 391 | |> whenObjectSchema 392 | |> Maybe.andThen (calcSubSchemaType actualValue rootSchema) 393 | else 394 | res 395 | ) 396 | Nothing 397 | -} 398 | 399 | 400 | encodeDict : Dict String Value -> Value 401 | encodeDict dict = 402 | Encode.object (Dict.toList dict) 403 | 404 | 405 | decodeDict : Value -> Dict String Value 406 | decodeDict val = 407 | Decode.decodeValue (Decode.dict Decode.value) val 408 | |> Result.withDefault Dict.empty 409 | 410 | 411 | decodeList : Value -> List Value 412 | decodeList val = 413 | Decode.decodeValue (Decode.list Decode.value) val 414 | |> Result.withDefault [] 415 | 416 | 417 | 418 | {- 419 | getDefinition : Maybe Schemata -> String -> Maybe Schema 420 | getDefinition defs name = 421 | defs 422 | |> Maybe.andThen 423 | (\(Schemata x) -> 424 | List.foldl 425 | (\( key, prop ) result -> 426 | if name == key then 427 | Just prop 428 | else 429 | result 430 | ) 431 | Nothing 432 | x 433 | ) 434 | -} 435 | {- 436 | debugSchema : String -> Maybe Schema -> Maybe Schema 437 | debugSchema msg schema = 438 | let 439 | a = 440 | case schema of 441 | Just s -> 442 | s 443 | |> Schema.encode 444 | |> Encode.encode 4 445 | |> Debug.log 446 | |> (\f -> f msg) 447 | 448 | Nothing -> 449 | Debug.log msg "Nothing" 450 | in 451 | schema 452 | 453 | 454 | debugSubSchema : String -> Maybe SubSchema -> Maybe SubSchema 455 | debugSubSchema msg schema = 456 | let 457 | a = 458 | case schema of 459 | Just s -> 460 | ObjectSchema s 461 | |> Schema.encode 462 | |> Encode.encode 4 463 | |> Debug.log 464 | |> (\f -> f msg) 465 | 466 | Nothing -> 467 | Debug.log msg "Nothing" 468 | in 469 | schema 470 | -} 471 | 472 | 473 | collectIds : Schema -> SchemataPool -> ( SchemataPool, String ) 474 | collectIds schema pool = 475 | let 476 | getNs : Maybe String -> String 477 | getNs uri = 478 | case uri of 479 | Just s -> 480 | let 481 | ( isPointer, ns, _ ) = 482 | parseJsonPointer s "" 483 | in 484 | ns 485 | 486 | Nothing -> 487 | "" 488 | 489 | manageId : String -> Value -> SchemataPool -> List ( String, Value ) -> ( List ( String, Value ), ( SchemataPool, String ) ) 490 | manageId ns source poolLocal obj = 491 | case List.filter (\( name, _ ) -> name == "id" || name == "$id") obj of 492 | ( _, val ) :: _ -> 493 | val 494 | |> Decode.decodeValue Decode.string 495 | |> Result.map 496 | (\id -> 497 | let 498 | ( isPointer, newNs, path ) = 499 | parseJsonPointer id ns 500 | in 501 | case Decode.decodeValue Schema.decoder source of 502 | Ok schemaLocal -> 503 | ( obj, ( Dict.insert (makeJsonPointer ( isPointer, newNs, path )) schemaLocal poolLocal, newNs ) ) 504 | 505 | Err _ -> 506 | ( obj, ( poolLocal, ns ) ) 507 | ) 508 | |> Result.withDefault ( obj, ( poolLocal, ns ) ) 509 | 510 | _ -> 511 | ( obj, ( poolLocal, ns ) ) 512 | 513 | walkValue source ( poolLocal, ns ) = 514 | source 515 | |> Decode.decodeValue (Decode.keyValuePairs Decode.value) 516 | |> Result.withDefault [] 517 | |> manageId ns source poolLocal 518 | |> (\( list, res ) -> List.foldl (\( key, val ) -> walkValue val) res list) 519 | in 520 | case schema of 521 | ObjectSchema { id, source } -> 522 | walkValue source ( pool, getNs id ) 523 | 524 | _ -> 525 | ( pool, "" ) 526 | -------------------------------------------------------------------------------- /src/Json/Schema/Random.elm: -------------------------------------------------------------------------------- 1 | module Json.Schema.Random exposing 2 | ( value, valueAt 3 | , GeneratorSettings, defaultSettings 4 | ) 5 | 6 | {-| Generate random values based on JSON Schema. 7 | 8 | Experimental module. 9 | 10 | 11 | # Generator 12 | 13 | @docs value, valueAt 14 | 15 | 16 | # Settings 17 | 18 | @docs GeneratorSettings, defaultSettings 19 | 20 | -} 21 | 22 | import Char 23 | import Dict 24 | import Json.Encode as Encode exposing (Value) 25 | import Json.Schema.Definitions 26 | exposing 27 | ( Items(..) 28 | , Schema(..) 29 | , Schemata(..) 30 | , SingleType(..) 31 | , Type(..) 32 | ) 33 | import Json.Schema.Helpers exposing (collectIds) 34 | import Random exposing (Generator, Seed) 35 | import Ref exposing (defaultPool) 36 | import Util exposing (getAt, uncons) 37 | 38 | 39 | {-| Customize generator behaviour using following parameters: 40 | 41 | - optionalPropertyProbability : float from 0 to 1, which affects used while generating object with optional property, default 0.5 42 | - degradationMultiplier : used in nested objects to affect probability of optional property appearance (must have for recursive objects), default 0.2 43 | - defaultListLengthLimit : how many items in array to generate when limit is not set by a schema, default 100 44 | - defaultStringLengthLimit : how many characters in random string to generate when limit is not set by a schema, default 100 45 | 46 | -} 47 | type alias GeneratorSettings = 48 | { optionalPropertyProbability : Float 49 | , degradationMultiplier : Float 50 | , defaultListLengthLimit : Int 51 | , defaultStringLengthLimit : Int 52 | } 53 | 54 | 55 | {-| Defaults for GeneratorSettings 56 | -} 57 | defaultSettings : GeneratorSettings 58 | defaultSettings = 59 | GeneratorSettings 60 | -- optionalPropertyProbability 61 | 0.5 62 | -- degradationMultiplier 63 | 0.2 64 | -- defaultListLengthLimit 65 | 100 66 | -- defaultStringLengthLimit 67 | 100 68 | 69 | 70 | randomString : Int -> Int -> Maybe String -> Generator String 71 | randomString minLength maxLength format = 72 | case format of 73 | Just "url" -> 74 | randomBool 75 | |> Random.map 76 | (\x -> 77 | if x then 78 | "http://example.com/" 79 | 80 | else 81 | "https://github.com" 82 | ) 83 | 84 | Just "uri" -> 85 | randomBool 86 | |> Random.map 87 | (\x -> 88 | if x then 89 | "http://example.com/" 90 | 91 | else 92 | "https://github.com" 93 | ) 94 | 95 | Just "email" -> 96 | Random.int 1000 9999 97 | |> Random.map 98 | (\x -> "rcp" ++ (x |> String.fromInt) ++ "@receipt.to") 99 | 100 | Just "host-name" -> 101 | randomBool 102 | |> Random.map 103 | (\x -> 104 | if x then 105 | "example.com" 106 | 107 | else 108 | "github.com" 109 | ) 110 | 111 | Just "date-time" -> 112 | randomBool 113 | |> Random.map (\_ -> "2018-01-01T09:00:00Z") 114 | 115 | Just "time" -> 116 | randomBool 117 | |> Random.map (\_ -> "09:00:00") 118 | 119 | Just "date" -> 120 | randomBool 121 | |> Random.map (\_ -> "2018-01-01") 122 | 123 | _ -> 124 | Random.int minLength maxLength 125 | |> Random.andThen (\a -> Random.list a lowercaseLetter) 126 | |> Random.map String.fromList 127 | 128 | 129 | lowercaseLetter : Generator Char 130 | lowercaseLetter = 131 | Random.map (\n -> Char.fromCode (n + 97)) (Random.int 0 25) 132 | 133 | 134 | randomItemFromList : ( a, List a ) -> Generator a 135 | randomItemFromList ( head, tail ) = 136 | let 137 | list = 138 | head :: tail 139 | in 140 | list 141 | |> List.length 142 | |> (+) -1 143 | |> Random.int 0 144 | |> Random.map ((\a -> getAt a list) >> Maybe.withDefault head) 145 | 146 | 147 | nullGenerator : Generator Value 148 | nullGenerator = 149 | randomBool |> Random.map (\_ -> Encode.null) 150 | 151 | 152 | upgradeSettings : GeneratorSettings -> GeneratorSettings 153 | upgradeSettings settings = 154 | { settings 155 | | optionalPropertyProbability = 156 | settings.optionalPropertyProbability * settings.degradationMultiplier 157 | } 158 | 159 | 160 | randomObject : GeneratorSettings -> String -> Ref.SchemataPool -> List ( String, Schema ) -> List String -> Generator Value 161 | randomObject settings ns pool props required = 162 | props 163 | |> List.foldl 164 | (\( k, v ) res -> 165 | if List.member k required then 166 | v 167 | |> valueGenerator (upgradeSettings settings) ns pool 168 | |> Random.andThen (\x -> res |> Random.map ((::) ( k, x ))) 169 | 170 | else 171 | Random.float 0 1 172 | |> Random.andThen 173 | (\isRequired -> 174 | if isRequired < settings.optionalPropertyProbability then 175 | v 176 | |> valueGenerator (upgradeSettings settings) ns pool 177 | |> Random.andThen (\x -> res |> Random.map ((::) ( k, x ))) 178 | 179 | else 180 | res 181 | ) 182 | ) 183 | (randomBool |> Random.map (\_ -> [])) 184 | |> Random.map (List.reverse >> Encode.object) 185 | 186 | 187 | randomList : GeneratorSettings -> String -> Ref.SchemataPool -> Int -> Int -> Schema -> Generator Value 188 | randomList settings ns pool minItems maxItems schema = 189 | Random.int minItems maxItems 190 | |> Random.andThen (\a -> Random.list a (valueGenerator (upgradeSettings settings) ns pool schema)) 191 | |> Random.map (Encode.list identity) 192 | 193 | 194 | {-| Random value generator. 195 | 196 | buildSchema 197 | |> withProperties 198 | [ ( "foo", buildSchema |> withType "integer" ) ] 199 | |> toSchema 200 | |> Result.withDefault blankSchema 201 | |> value defaultSettings 202 | |> (\a -> Random.step a (Random.initialSeed 2)) 203 | |> (\( v, _ ) -> 204 | Expect.equal v (Encode.object [ ( "foo", Encode.int 688281600 ) ]) 205 | ) 206 | 207 | See tests for more examples. 208 | 209 | -} 210 | value : GeneratorSettings -> Schema -> Generator Value 211 | value settings s = 212 | let 213 | ( pool, ns ) = 214 | collectIds s defaultPool 215 | in 216 | valueGenerator settings ns pool s 217 | 218 | 219 | {-| Random value generator at path. 220 | -} 221 | valueAt : GeneratorSettings -> Schema -> String -> Generator Value 222 | valueAt settings s ref = 223 | let 224 | ( pool, ns ) = 225 | collectIds s defaultPool 226 | 227 | --|> Debug.log "pool is" 228 | a = 229 | pool 230 | |> Dict.keys 231 | 232 | -- |> Debug.log "pool keys are" 233 | in 234 | case Ref.resolveReference ns pool s ref of 235 | Just ( nsLocal, ss ) -> 236 | valueGenerator settings nsLocal pool ss 237 | 238 | Nothing -> 239 | nullGenerator 240 | 241 | 242 | resolve : String -> Ref.SchemataPool -> Schema -> Maybe ( String, Schema ) 243 | resolve ns pool schema = 244 | case schema of 245 | BooleanSchema _ -> 246 | Just ( ns, schema ) 247 | 248 | ObjectSchema os -> 249 | case os.ref of 250 | Just ref -> 251 | Ref.resolveReference ns pool schema ref 252 | 253 | -- |> Debug.log ("resolving this :( " ++ ref ++ " " ++ ns) 254 | Nothing -> 255 | Just ( ns, schema ) 256 | 257 | 258 | randomBool : Generator Bool 259 | randomBool = 260 | Random.float 0 1 261 | |> Random.map (\x -> x > 0.5) 262 | 263 | 264 | valueGenerator : GeneratorSettings -> String -> Ref.SchemataPool -> Schema -> Generator Value 265 | valueGenerator settings ns pool schema = 266 | case schema |> resolve ns pool of 267 | Nothing -> 268 | nullGenerator 269 | 270 | Just ( nsLocal, BooleanSchema b ) -> 271 | if b then 272 | randomBool |> Random.map (\_ -> Encode.object []) 273 | 274 | else 275 | randomBool |> Random.map (\_ -> Encode.null) 276 | 277 | Just ( nsLocal, ObjectSchema os ) -> 278 | [ Maybe.andThen uncons os.examples 279 | |> Maybe.map randomItemFromList 280 | , Maybe.andThen uncons os.enum 281 | |> Maybe.map randomItemFromList 282 | , case os.type_ of 283 | SingleType NumberType -> 284 | Random.float 285 | (os.minimum |> Maybe.withDefault (toFloat Random.minInt)) 286 | (os.maximum |> Maybe.withDefault (toFloat Random.maxInt)) 287 | |> Random.map Encode.float 288 | |> Just 289 | 290 | SingleType IntegerType -> 291 | Random.int 292 | (os.minimum |> Maybe.map round |> Maybe.withDefault Random.minInt) 293 | (os.maximum |> Maybe.map round |> Maybe.withDefault Random.maxInt) 294 | |> Random.map Encode.int 295 | |> Just 296 | 297 | SingleType BooleanType -> 298 | randomBool 299 | |> Random.map Encode.bool 300 | |> Just 301 | 302 | SingleType StringType -> 303 | randomString 304 | (os.minLength |> Maybe.withDefault 0) 305 | (os.maxLength |> Maybe.withDefault settings.defaultStringLengthLimit) 306 | os.format 307 | |> Random.map Encode.string 308 | |> Just 309 | 310 | _ -> 311 | Nothing 312 | , os.properties 313 | |> Maybe.map (\(Schemata props) -> randomObject settings ns pool props (os.required |> Maybe.withDefault [])) 314 | , case os.items of 315 | ItemDefinition schemaLocal -> 316 | randomList settings 317 | ns 318 | pool 319 | (os.minItems |> Maybe.withDefault 0) 320 | (os.maxItems |> Maybe.withDefault settings.defaultListLengthLimit) 321 | schemaLocal 322 | |> Just 323 | 324 | --NoItems -> 325 | _ -> 326 | Nothing 327 | ] 328 | |> List.foldl 329 | (\maybeGenerator res -> 330 | if res == Nothing then 331 | maybeGenerator 332 | 333 | else 334 | res 335 | ) 336 | Nothing 337 | |> Maybe.withDefault nullGenerator 338 | -------------------------------------------------------------------------------- /src/Json/Schemata.elm: -------------------------------------------------------------------------------- 1 | module Json.Schemata exposing (draft4, draft6) 2 | 3 | import Json.Decode as Decode 4 | import Json.Schema.Definitions exposing (Schema, blankSchema, decoder) 5 | 6 | 7 | draft4 : Schema 8 | draft4 = 9 | """ 10 | { 11 | "id": "http://json-schema.org/draft-04/schema#", 12 | "$schema": "http://json-schema.org/draft-04/schema#", 13 | "description": "Core schema meta-schema", 14 | "definitions": { 15 | "schemaArray": { 16 | "type": "array", 17 | "minItems": 1, 18 | "items": { "$ref": "#" } 19 | }, 20 | "positiveInteger": { 21 | "type": "integer", 22 | "minimum": 0 23 | }, 24 | "positiveIntegerDefault0": { 25 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 26 | }, 27 | "simpleTypes": { 28 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 29 | }, 30 | "stringArray": { 31 | "type": "array", 32 | "items": { "type": "string" }, 33 | "minItems": 1, 34 | "uniqueItems": true 35 | } 36 | }, 37 | "type": "object", 38 | "properties": { 39 | "id": { 40 | "type": "string", 41 | "format": "uri", 42 | "description": "Identifier of schema" 43 | }, 44 | "$schema": { 45 | "type": "string", 46 | "format": "uri", 47 | "description": "Link to a schema which validates this object" 48 | }, 49 | "title": { 50 | "type": "string" 51 | }, 52 | "description": { 53 | "type": "string" 54 | }, 55 | "default": {}, 56 | "multipleOf": { 57 | "type": "number", 58 | "minimum": 0, 59 | "exclusiveMinimum": true 60 | }, 61 | "maximum": { 62 | "type": "number" 63 | }, 64 | "exclusiveMaximum": { 65 | "type": "boolean", 66 | "default": false 67 | }, 68 | "minimum": { 69 | "type": "number" 70 | }, 71 | "exclusiveMinimum": { 72 | "type": "boolean", 73 | "default": false 74 | }, 75 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 76 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 77 | "pattern": { 78 | "type": "string", 79 | "format": "regex" 80 | }, 81 | "additionalItems": { 82 | "anyOf": [ 83 | { "type": "boolean" }, 84 | { "$ref": "#" } 85 | ], 86 | "default": {} 87 | }, 88 | "items": { 89 | "anyOf": [ 90 | { "$ref": "#" }, 91 | { "$ref": "#/definitions/schemaArray" } 92 | ], 93 | "default": {} 94 | }, 95 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 96 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 97 | "uniqueItems": { 98 | "type": "boolean", 99 | "default": false 100 | }, 101 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 102 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 103 | "required": { "$ref": "#/definitions/stringArray" }, 104 | "additionalProperties": { 105 | "anyOf": [ 106 | { "type": "boolean" }, 107 | { "$ref": "#" } 108 | ], 109 | "default": {} 110 | }, 111 | "definitions": { 112 | "type": "object", 113 | "additionalProperties": { "$ref": "#" }, 114 | "default": {} 115 | }, 116 | "properties": { 117 | "type": "object", 118 | "additionalProperties": { "$ref": "#" }, 119 | "default": {} 120 | }, 121 | "patternProperties": { 122 | "type": "object", 123 | "additionalProperties": { "$ref": "#" }, 124 | "default": {} 125 | }, 126 | "dependencies": { 127 | "type": "object", 128 | "additionalProperties": { 129 | "anyOf": [ 130 | { "$ref": "#" }, 131 | { "$ref": "#/definitions/stringArray" } 132 | ] 133 | } 134 | }, 135 | "enum": { 136 | "type": "array", 137 | "minItems": 1, 138 | "uniqueItems": true 139 | }, 140 | "type": { 141 | "anyOf": [ 142 | { "$ref": "#/definitions/simpleTypes" }, 143 | { 144 | "type": "array", 145 | "items": { "$ref": "#/definitions/simpleTypes" }, 146 | "minItems": 1, 147 | "uniqueItems": true 148 | } 149 | ] 150 | }, 151 | "allOf": { "$ref": "#/definitions/schemaArray" }, 152 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 153 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 154 | "not": { "$ref": "#" } 155 | }, 156 | "dependencies": { 157 | "exclusiveMaximum": [ "maximum" ], 158 | "exclusiveMinimum": [ "minimum" ] 159 | }, 160 | "default": {} 161 | } 162 | """ 163 | |> decodeUnsafe 164 | 165 | 166 | draft6 : Schema 167 | draft6 = 168 | """ 169 | { 170 | "$schema": "http://json-schema.org/draft-06/schema#", 171 | "$id": "http://json-schema.org/draft-06/schema#", 172 | "title": "Core schema meta-schema", 173 | "definitions": { 174 | "schemaArray": { 175 | "type": "array", 176 | "minItems": 1, 177 | "items": { "$ref": "#" } 178 | }, 179 | "nonNegativeInteger": { 180 | "type": "integer", 181 | "minimum": 0 182 | }, 183 | "nonNegativeIntegerDefault0": { 184 | "allOf": [ 185 | { "$ref": "#/definitions/nonNegativeInteger" }, 186 | { "default": 0 } 187 | ] 188 | }, 189 | "simpleTypes": { 190 | "enum": [ 191 | "array", 192 | "boolean", 193 | "integer", 194 | "null", 195 | "number", 196 | "object", 197 | "string" 198 | ] 199 | }, 200 | "stringArray": { 201 | "type": "array", 202 | "items": { "type": "string" }, 203 | "uniqueItems": true, 204 | "default": [] 205 | } 206 | }, 207 | "type": ["object", "boolean"], 208 | "properties": { 209 | "$id": { 210 | "type": "string", 211 | "format": "uri-reference", 212 | "description": "Identifier of schema" 213 | }, 214 | "$schema": { 215 | "type": "string", 216 | "format": "uri", 217 | "description": "Link to a schema which validates this object" 218 | }, 219 | "$ref": { 220 | "type": "string", 221 | "format": "uri-reference" 222 | }, 223 | "title": { 224 | "type": "string" 225 | }, 226 | "description": { 227 | "type": "string" 228 | }, 229 | "default": {}, 230 | "multipleOf": { 231 | "type": "number", 232 | "exclusiveMinimum": 0 233 | }, 234 | "maximum": { 235 | "type": "number" 236 | }, 237 | "exclusiveMaximum": { 238 | "type": "number" 239 | }, 240 | "minimum": { 241 | "type": "number" 242 | }, 243 | "exclusiveMinimum": { 244 | "type": "number" 245 | }, 246 | "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, 247 | "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 248 | "pattern": { 249 | "type": "string", 250 | "format": "regex" 251 | }, 252 | "additionalItems": { "$ref": "#" }, 253 | "items": { 254 | "anyOf": [ 255 | { "$ref": "#" }, 256 | { "$ref": "#/definitions/schemaArray" } 257 | ], 258 | "default": {} 259 | }, 260 | "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, 261 | "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 262 | "uniqueItems": { 263 | "type": "boolean", 264 | "default": false 265 | }, 266 | "contains": { "$ref": "#" }, 267 | "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, 268 | "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, 269 | "required": { "$ref": "#/definitions/stringArray" }, 270 | "additionalProperties": { "$ref": "#" }, 271 | "definitions": { 272 | "type": "object", 273 | "additionalProperties": { "$ref": "#" }, 274 | "default": {} 275 | }, 276 | "properties": { 277 | "type": "object", 278 | "additionalProperties": { "$ref": "#" }, 279 | "default": {} 280 | }, 281 | "patternProperties": { 282 | "type": "object", 283 | "additionalProperties": { "$ref": "#" }, 284 | "default": {} 285 | }, 286 | "dependencies": { 287 | "type": "object", 288 | "additionalProperties": { 289 | "anyOf": [ 290 | { "$ref": "#" }, 291 | { "$ref": "#/definitions/stringArray" } 292 | ] 293 | } 294 | }, 295 | "propertyNames": { "$ref": "#" }, 296 | "const": {}, 297 | "enum": { 298 | "type": "array", 299 | "minItems": 1, 300 | "uniqueItems": true 301 | }, 302 | "type": { 303 | "anyOf": [ 304 | { "$ref": "#/definitions/simpleTypes" }, 305 | { 306 | "type": "array", 307 | "items": { "$ref": "#/definitions/simpleTypes" }, 308 | "minItems": 1, 309 | "uniqueItems": true 310 | } 311 | ] 312 | }, 313 | "format": { "type": "string" }, 314 | "allOf": { "$ref": "#/definitions/schemaArray" }, 315 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 316 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 317 | "not": { "$ref": "#" } 318 | }, 319 | "default": {} 320 | } 321 | """ 322 | |> decodeUnsafe 323 | 324 | 325 | decodeUnsafe : String -> Schema 326 | decodeUnsafe = 327 | Decode.decodeString decoder >> Result.withDefault blankSchema 328 | -------------------------------------------------------------------------------- /src/Ref.elm: -------------------------------------------------------------------------------- 1 | module Ref exposing (SchemataPool, defaultPool, parseJsonPointer, resolveReference) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Decode as Decode 5 | import Json.Schema.Definitions exposing (Schema(..), Schemata(..), SubSchema, decoder) 6 | import Json.Schemata as Schemata 7 | import Regex exposing (fromString) 8 | 9 | 10 | {-| Pool of schemata used in refs lookup by id 11 | -} 12 | type alias SchemataPool = 13 | Dict String Schema 14 | 15 | 16 | {-| Default schemata pool containing schemata draft-04 and draft-06 17 | -} 18 | defaultPool : SchemataPool 19 | defaultPool = 20 | Dict.empty 21 | |> Dict.insert "http://json-schema.org/draft-06/schema" Schemata.draft6 22 | |> Dict.insert "http://json-schema.org/draft-06/schema#" Schemata.draft6 23 | |> Dict.insert "http://json-schema.org/draft-04/schema" Schemata.draft4 24 | 25 | 26 | parseJsonPointer : String -> String -> ( Bool, String, List String ) 27 | parseJsonPointer pointer currentNamespace = 28 | let 29 | merge base relative = 30 | if isAbsolute base && hasFragments base then 31 | base |> Regex.replace lastFragment (\_ -> "/" ++ relative) 32 | 33 | else 34 | relative 35 | 36 | hasFragments = 37 | Regex.contains lastFragment 38 | 39 | isAbsolute = 40 | Regex.contains absoluteUri 41 | 42 | ( ns, hash ) = 43 | case String.split "#" pointer of 44 | [] -> 45 | ( currentNamespace, "" ) 46 | 47 | a :: [] -> 48 | if a == "" then 49 | ( currentNamespace, "" ) 50 | 51 | else if isAbsolute a then 52 | ( a, "" ) 53 | 54 | else 55 | ( merge currentNamespace a, "" ) 56 | 57 | a :: b :: _ -> 58 | if a == "" then 59 | ( currentNamespace, b ) 60 | --|> Debug.log "case 3.1" 61 | 62 | else if isAbsolute a then 63 | ( a, b ) 64 | --|> Debug.log "case 3.2" 65 | 66 | else 67 | ( merge currentNamespace a, b ) 68 | 69 | --|> Debug.log "case 3.4" 70 | isPointer = 71 | hasFragments hash 72 | in 73 | ( isPointer 74 | , ns 75 | , if isPointer then 76 | hash 77 | |> String.split "/" 78 | |> List.drop 1 79 | |> List.map unescapeJsonPathFragment 80 | 81 | else if hash /= "" then 82 | [ hash ] 83 | 84 | else 85 | [] 86 | ) 87 | 88 | 89 | absoluteUri : Regex.Regex 90 | absoluteUri = 91 | fromString "\\/\\/|^\\/" |> Maybe.withDefault Regex.never 92 | 93 | 94 | lastFragment : Regex.Regex 95 | lastFragment = 96 | fromString "\\/[^\\/]*$" |> Maybe.withDefault Regex.never 97 | 98 | 99 | tilde : Regex.Regex 100 | tilde = 101 | fromString "~0" |> Maybe.withDefault Regex.never 102 | 103 | 104 | slash : Regex.Regex 105 | slash = 106 | fromString "~1" |> Maybe.withDefault Regex.never 107 | 108 | 109 | percent : Regex.Regex 110 | percent = 111 | fromString "%25" |> Maybe.withDefault Regex.never 112 | 113 | 114 | unescapeJsonPathFragment : String -> String 115 | unescapeJsonPathFragment s = 116 | s 117 | |> Regex.replace tilde (\_ -> "~") 118 | |> Regex.replace slash (\_ -> "/") 119 | |> Regex.replace percent (\_ -> "%") 120 | 121 | 122 | makeJsonPointer : ( Bool, String, List String ) -> String 123 | makeJsonPointer ( isPointer, ns, path ) = 124 | if isPointer then 125 | ("#" :: path) 126 | |> String.join "/" 127 | |> (++) ns 128 | 129 | else if List.isEmpty path then 130 | ns 131 | -- |> Debug.log "path was empty" 132 | 133 | else 134 | path 135 | |> String.join "/" 136 | |> (++) (ns ++ "#") 137 | 138 | 139 | removeTrailingSlash : String -> String 140 | removeTrailingSlash s = 141 | if String.endsWith "#" s then 142 | String.dropRight 1 s 143 | 144 | else 145 | s 146 | 147 | 148 | resolveReference : String -> SchemataPool -> Schema -> String -> Maybe ( String, Schema ) 149 | resolveReference ns pool schema ref = 150 | let 151 | rootNs = 152 | schema 153 | |> whenObjectSchema 154 | |> Maybe.andThen .id 155 | |> Maybe.map removeTrailingSlash 156 | |> Maybe.withDefault ns 157 | 158 | resolveRecursively namespace limit localSchema localRef = 159 | let 160 | ( isPointer, localNs, path ) = 161 | parseJsonPointer {- Debug.log "resolving ref" -} localRef namespace 162 | 163 | --|> Debug.log "new json pointer (parsed)" 164 | --|> Debug.log ("parse " ++ (toString localRef) ++ " within ns " ++ (toString namespace)) 165 | newJsonPointer = 166 | makeJsonPointer ( isPointer, localNs, path ) 167 | 168 | --|> Debug.log "new json pointer (combined)" 169 | a = 170 | pool 171 | |> Dict.keys 172 | 173 | --|> Debug.log "pool keys" 174 | in 175 | if limit > 0 then 176 | if isPointer then 177 | (if localNs == "" then 178 | Just localSchema 179 | 180 | else 181 | pool 182 | |> Dict.get localNs 183 | ) 184 | |> Maybe.andThen whenObjectSchema 185 | |> Maybe.andThen 186 | (\os -> 187 | os.source 188 | |> Decode.decodeValue (Decode.at path decoder) 189 | |> Result.toMaybe 190 | |> Maybe.andThen 191 | (\def -> 192 | case def of 193 | ObjectSchema oss -> 194 | case oss.ref of 195 | Just r -> 196 | resolveRecursively localNs (limit - 1) localSchema r 197 | 198 | Nothing -> 199 | Just ( localNs, def ) 200 | 201 | BooleanSchema _ -> 202 | Just ( localNs, def ) 203 | ) 204 | ) 205 | 206 | else if newJsonPointer == "" then 207 | Just ( "", localSchema ) 208 | 209 | else 210 | pool 211 | |> Dict.get newJsonPointer 212 | |> Maybe.map (\x -> ( localNs, x )) 213 | 214 | else 215 | Just ( localNs, localSchema ) 216 | in 217 | resolveRecursively rootNs 10 schema ref 218 | 219 | 220 | 221 | --|> Debug.log ("resolution result for " ++ ref ++ " " ++ rootNs) 222 | 223 | 224 | whenObjectSchema : Schema -> Maybe SubSchema 225 | whenObjectSchema schema = 226 | case schema of 227 | ObjectSchema os -> 228 | Just os 229 | 230 | BooleanSchema _ -> 231 | Nothing 232 | -------------------------------------------------------------------------------- /src/Util.elm: -------------------------------------------------------------------------------- 1 | module Util exposing (foldResults, getAt, indexOfFirstDuplicate, isInt, isUnique, resultToDecoder, uncons) 2 | 3 | import Json.Decode exposing (Decoder, fail, succeed) 4 | 5 | 6 | foldResults : List (Result x y) -> Result x (List y) 7 | foldResults results = 8 | results 9 | |> List.foldl 10 | (\t -> Result.andThen (\r -> t |> Result.map (\a -> (::) a r))) 11 | (Ok []) 12 | |> Result.map List.reverse 13 | 14 | 15 | resultToDecoder : Result String a -> Decoder a 16 | resultToDecoder res = 17 | case res of 18 | Ok a -> 19 | succeed a 20 | 21 | Err e -> 22 | fail e 23 | 24 | 25 | isInt : Float -> Bool 26 | isInt x = 27 | x == (round >> toFloat) x 28 | 29 | 30 | uncons : List a -> Maybe ( a, List a ) 31 | uncons l = 32 | case l of 33 | head :: tail -> 34 | Just ( head, tail ) 35 | 36 | _ -> 37 | Nothing 38 | 39 | 40 | getAt : Int -> List a -> Maybe a 41 | getAt index = 42 | List.drop index >> List.head 43 | 44 | 45 | isUnique : List comparable -> Bool 46 | isUnique list = 47 | indexOfFirstDuplicate list == -1 48 | 49 | 50 | indexOfFirstDuplicate : List comparable -> Int 51 | indexOfFirstDuplicate list = 52 | list 53 | |> List.foldl 54 | (\x ( index, res, sublist ) -> 55 | ( index + 1 56 | , if res > -1 then 57 | res 58 | 59 | else if List.member x sublist then 60 | index 61 | 62 | else 63 | -1 64 | , sublist |> List.drop 1 65 | ) 66 | ) 67 | ( 0, -1, list |> List.drop 1 ) 68 | |> (\( _, r, _ ) -> r) 69 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | repl-temp-* 3 | elm-stuff 4 | dist 5 | .DS_Store 6 | npm-debug.log 7 | -------------------------------------------------------------------------------- /tests/Decoding.elm: -------------------------------------------------------------------------------- 1 | module Decoding exposing (all) 2 | 3 | import Test exposing (Test, describe, test, only, skip) 4 | import Json.Schema.Builder 5 | exposing 6 | ( SchemaBuilder 7 | , buildSchema 8 | , boolSchema 9 | , toSchema 10 | , withType 11 | , withNullableType 12 | , withUnionType 13 | , withContains 14 | , withDefinitions 15 | , withItems 16 | , withItem 17 | , withAdditionalItems 18 | , withProperties 19 | , withPatternProperties 20 | , withAdditionalProperties 21 | , withSchemaDependency 22 | , withPropNamesDependency 23 | , withPropertyNames 24 | , withAllOf 25 | , withAnyOf 26 | , withOneOf 27 | , withTitle 28 | , withNot 29 | ) 30 | import Json.Schema.Definitions as Schema exposing (Schema, decoder) 31 | import Expect 32 | import Json.Encode as Encode exposing (Value) 33 | import Json.Decode as Decode exposing (decodeValue) 34 | 35 | 36 | all : Test 37 | all = 38 | describe "decoding of JSON Schema" 39 | [ test "type=integer" <| 40 | \() -> 41 | [ ( "type", Encode.string "integer" ) ] 42 | |> decodesInto 43 | (buildSchema 44 | |> withType "integer" 45 | ) 46 | , test "type=number" <| 47 | \() -> 48 | [ ( "type", Encode.string "number" ) ] 49 | |> decodesInto 50 | (buildSchema 51 | |> withType "number" 52 | ) 53 | , test "type=string" <| 54 | \() -> 55 | [ ( "type", Encode.string "string" ) ] 56 | |> decodesInto 57 | (buildSchema 58 | |> withType "string" 59 | ) 60 | , test "type=object" <| 61 | \() -> 62 | [ ( "type", Encode.string "object" ) ] 63 | |> decodesInto 64 | (buildSchema 65 | |> withType "object" 66 | ) 67 | , test "type=array" <| 68 | \() -> 69 | [ ( "type", Encode.string "array" ) ] 70 | |> decodesInto 71 | (buildSchema 72 | |> withType "array" 73 | ) 74 | , test "type=null" <| 75 | \() -> 76 | [ ( "type", Encode.string "null" ) ] 77 | |> decodesInto 78 | (buildSchema 79 | |> withType "null" 80 | ) 81 | , test "type=[null,integer]" <| 82 | \() -> 83 | [ ( "type" 84 | , Encode.list 85 | [ Encode.string "null" 86 | , Encode.string "integer" 87 | ] 88 | ) 89 | ] 90 | |> decodesInto 91 | (buildSchema 92 | |> withNullableType "integer" 93 | ) 94 | , test "type=[string,integer]" <| 95 | \() -> 96 | [ ( "type" 97 | , Encode.list 98 | [ Encode.string "integer" 99 | , Encode.string "string" 100 | ] 101 | ) 102 | ] 103 | |> decodesInto 104 | (buildSchema 105 | |> withUnionType [ "string", "integer" ] 106 | ) 107 | , test "title=smth" <| 108 | \() -> 109 | [ ( "title", Encode.string "smth" ) ] 110 | |> decodesInto 111 | (buildSchema 112 | |> withTitle "smth" 113 | ) 114 | , test "definitions={foo=blankSchema}" <| 115 | \() -> 116 | [ ( "definitions", Encode.object [ ( "foo", Encode.object [] ) ] ) ] 117 | |> decodesInto 118 | (buildSchema 119 | |> withDefinitions [ ( "foo", buildSchema ) ] 120 | ) 121 | , test "items=[blankSchema]" <| 122 | \() -> 123 | [ ( "items", Encode.list <| [ Encode.object [] ] ) ] 124 | |> decodesInto 125 | (buildSchema 126 | |> withItems [ buildSchema ] 127 | ) 128 | , test "items=blankSchema" <| 129 | \() -> 130 | [ ( "items", Encode.object [] ) ] 131 | |> decodesInto 132 | (buildSchema 133 | |> withItem buildSchema 134 | ) 135 | , test "additionalItems=blankSchema" <| 136 | \() -> 137 | [ ( "additionalItems", Encode.object [] ) ] 138 | |> decodesInto 139 | (buildSchema 140 | |> withAdditionalItems buildSchema 141 | ) 142 | , test "contains={}" <| 143 | \() -> 144 | [ ( "contains", Encode.object [] ) ] 145 | |> decodesInto 146 | (buildSchema 147 | |> withContains buildSchema 148 | ) 149 | , test "properties={foo=blankSchema}" <| 150 | \() -> 151 | [ ( "properties", Encode.object [ ( "foo", Encode.object [] ) ] ) ] 152 | |> decodesInto 153 | (buildSchema 154 | |> withProperties [ ( "foo", buildSchema ) ] 155 | ) 156 | , test "patternProperties={foo=blankSchema}" <| 157 | \() -> 158 | [ ( "patternProperties", Encode.object [ ( "foo", Encode.object [] ) ] ) ] 159 | |> decodesInto 160 | (buildSchema 161 | |> withPatternProperties [ ( "foo", buildSchema ) ] 162 | ) 163 | , test "additionalProperties=blankSchema" <| 164 | \() -> 165 | [ ( "additionalProperties", Encode.object [] ) ] 166 | |> decodesInto 167 | (buildSchema 168 | |> withAdditionalProperties buildSchema 169 | ) 170 | , test "dependencies={foo=blankSchema}" <| 171 | \() -> 172 | [ ( "dependencies", Encode.object [ ( "foo", Encode.object [] ) ] ) ] 173 | |> decodesInto 174 | (buildSchema 175 | |> withSchemaDependency "foo" buildSchema 176 | ) 177 | , test "dependencies={foo=[bar]}" <| 178 | \() -> 179 | [ ( "dependencies", Encode.object [ ( "foo", Encode.list [ Encode.string "bar" ] ) ] ) ] 180 | |> decodesInto 181 | (buildSchema 182 | |> withPropNamesDependency "foo" [ "bar" ] 183 | ) 184 | , test "propertyNames={}" <| 185 | \() -> 186 | [ ( "propertyNames", Encode.object [ ( "type", Encode.string "string" ) ] ) ] 187 | |> decodesInto 188 | (buildSchema 189 | |> withPropertyNames (buildSchema |> withType "string") 190 | ) 191 | , test "enum=[]" <| 192 | \() -> 193 | [ ( "enum", Encode.list [] ) ] 194 | |> decodeSchema 195 | |> Expect.err 196 | , test "allOf=[]" <| 197 | \() -> 198 | [ ( "allOf", Encode.list [] ) ] 199 | |> decodeSchema 200 | |> Expect.err 201 | , test "allOf=[blankSchema]" <| 202 | \() -> 203 | [ ( "allOf", Encode.list [ Encode.object [] ] ) ] 204 | |> decodesInto 205 | (buildSchema 206 | |> withAllOf [ buildSchema ] 207 | ) 208 | , test "oneOf=[blankSchema]" <| 209 | \() -> 210 | [ ( "oneOf", Encode.list [ Encode.object [] ] ) ] 211 | |> decodesInto 212 | (buildSchema 213 | |> withOneOf [ buildSchema ] 214 | ) 215 | , test "anyOf=[blankSchema]" <| 216 | \() -> 217 | [ ( "anyOf", Encode.list [ Encode.object [] ] ) ] 218 | |> decodesInto 219 | (buildSchema 220 | |> withAnyOf [ buildSchema ] 221 | ) 222 | , describe "boolean schema" 223 | [ test "true always validates any value" <| 224 | \() -> 225 | Encode.bool True 226 | |> decodeValue Schema.decoder 227 | |> Expect.equal (boolSchema True |> toSchema) 228 | , test "false always fails validation" <| 229 | \() -> 230 | Encode.bool False 231 | |> decodeValue Schema.decoder 232 | |> Expect.equal (boolSchema False |> toSchema) 233 | ] 234 | ] 235 | 236 | 237 | shouldResultWithSchema : Schema -> Result x Schema -> Expect.Expectation 238 | shouldResultWithSchema s = 239 | s 240 | |> Ok 241 | |> Expect.equal 242 | 243 | 244 | decodeSchema : List ( String, Value ) -> Result String Schema 245 | decodeSchema list = 246 | list 247 | |> Encode.object 248 | |> decodeValue Schema.decoder 249 | 250 | 251 | decodesInto : SchemaBuilder -> List ( String, Value ) -> Expect.Expectation 252 | decodesInto sb list = 253 | list 254 | |> Encode.object 255 | |> decodeValue Schema.decoder 256 | |> Expect.equal (sb |> toSchema) 257 | -------------------------------------------------------------------------------- /tests/Generator.elm: -------------------------------------------------------------------------------- 1 | module Generator exposing (all) 2 | 3 | import Test exposing (Test, describe, test, only) 4 | import Json.Schema.Builder 5 | exposing 6 | ( SchemaBuilder 7 | , buildSchema 8 | , boolSchema 9 | , toSchema 10 | , withType 11 | , withNullableType 12 | , withUnionType 13 | , withMinimum 14 | , withMaximum 15 | , withContains 16 | , withDefinitions 17 | , withItems 18 | , withItem 19 | , withMaxItems 20 | , withMinItems 21 | , withAdditionalItems 22 | , withProperties 23 | , withPatternProperties 24 | , withAdditionalProperties 25 | , withSchemaDependency 26 | , withPropNamesDependency 27 | , withPropertyNames 28 | , withAllOf 29 | , withAnyOf 30 | , withOneOf 31 | , withTitle 32 | , withNot 33 | , withExamples 34 | , withEnum 35 | ) 36 | import Json.Schema.Definitions as Schema exposing (Schema, decoder, blankSchema) 37 | import Json.Schema.Random as JSR 38 | import Expect 39 | import Json.Encode as Encode exposing (Value) 40 | import Random exposing (initialSeed, step) 41 | 42 | 43 | all : Test 44 | all = 45 | describe "random value generator" 46 | [ describe "generate by example" 47 | [ test "enough examples" <| 48 | \() -> 49 | buildSchema 50 | |> withExamples 51 | [ Encode.string "dime" 52 | , Encode.int 2 53 | , Encode.int 3 54 | , Encode.int 4 55 | , Encode.int 5 56 | ] 57 | |> toSchema 58 | |> Result.withDefault (blankSchema) 59 | |> JSR.value JSR.defaultSettings 60 | |> flip step (initialSeed 178) 61 | |> (\( v, _ ) -> Expect.equal v (Encode.string "dime")) 62 | , test "not enough examples" <| 63 | \() -> 64 | buildSchema 65 | |> withExamples [] 66 | |> toSchema 67 | |> Result.withDefault (blankSchema) 68 | |> JSR.value JSR.defaultSettings 69 | |> flip step (initialSeed 178) 70 | |> (\( v, _ ) -> Expect.equal v Encode.null) 71 | ] 72 | , describe "generate by enum" 73 | [ test "enough enums" <| 74 | \() -> 75 | buildSchema 76 | |> withEnum 77 | [ Encode.string "dime" 78 | , Encode.int 2 79 | , Encode.int 3 80 | , Encode.int 4 81 | , Encode.int 5 82 | ] 83 | |> toSchema 84 | |> Result.withDefault (blankSchema) 85 | |> JSR.value JSR.defaultSettings 86 | |> flip step (initialSeed 178) 87 | |> (\( v, _ ) -> Expect.equal v (Encode.string "dime")) 88 | , test "not enough examples" <| 89 | \() -> 90 | buildSchema 91 | |> withEnum [] 92 | |> toSchema 93 | |> Result.withDefault (blankSchema) 94 | |> JSR.value JSR.defaultSettings 95 | |> flip step (initialSeed 178) 96 | |> (\( v, _ ) -> Expect.equal v Encode.null) 97 | ] 98 | , describe "random object generation" 99 | [ test "object with required fields" <| 100 | \() -> 101 | buildSchema 102 | |> withProperties 103 | [ ( "foo", buildSchema |> withType "integer" ) ] 104 | |> toSchema 105 | |> Result.withDefault (blankSchema) 106 | |> JSR.value JSR.defaultSettings 107 | |> flip step (initialSeed 2) 108 | |> (\( v, _ ) -> Expect.equal v (Encode.object [ ( "foo", Encode.int 688281600 ) ])) 109 | ] 110 | , describe "random array generation" 111 | [ test "list of similar items" <| 112 | \() -> 113 | buildSchema 114 | |> withItem (buildSchema |> withType "integer" |> withMinimum 0 |> withMaximum 10) 115 | |> withMaxItems 10 116 | |> toSchema 117 | |> Result.withDefault (blankSchema) 118 | |> JSR.value JSR.defaultSettings 119 | |> flip step (initialSeed 1) 120 | |> (\( v, _ ) -> 121 | [ 3, 9, 7 ] 122 | |> List.map Encode.int 123 | |> Encode.list 124 | |> Expect.equal v 125 | ) 126 | ] 127 | ] 128 | -------------------------------------------------------------------------------- /tests/Type.elm.rm: -------------------------------------------------------------------------------- 1 | module Type exposing (all) 2 | 3 | import Json.Schema as JS exposing (empty) 4 | import Test exposing (Test, describe, test, only) 5 | import Data.Schema 6 | exposing 7 | ( Validation 8 | ( IntegerSchema 9 | , FloatSchema 10 | , StringSchema 11 | , Undefined 12 | ) 13 | , Meta 14 | , Schema 15 | ) 16 | import Data.NumberValidations exposing (NumberValidations) 17 | import Data.StringValidations exposing (StringValidations) 18 | import Expect 19 | import Json.Encode as Encode exposing (Value) 20 | 21 | 22 | all : Test 23 | all = describe "deprecated" [] 24 | 25 | 26 | deprecated : Test 27 | deprecated = 28 | describe "schema.type" 29 | [ test "integer schema" <| 30 | \() -> 31 | [ ( "type", Encode.string "integer" ) ] 32 | |> decodeSchema 33 | |> shouldResultWithSchema 34 | (Schema 35 | withEmptyMeta 36 | (IntegerSchema 37 | (NumberValidations 38 | Nothing 39 | Nothing 40 | Nothing 41 | Nothing 42 | Nothing 43 | ) 44 | ) 45 | Nothing 46 | ) 47 | , test "integer schema with validations" <| 48 | \() -> 49 | [ ( "type", Encode.string "integer" ) 50 | , ( "multipleOf", Encode.float 2.0 ) 51 | , ( "maximum", Encode.float 2.0 ) 52 | , ( "exclusiveMaximum", Encode.float 2.0 ) 53 | , ( "minimum", Encode.float 1.0 ) 54 | , ( "exclusiveMinimum", Encode.float 1.0 ) 55 | ] 56 | |> decodeSchema 57 | |> shouldResultWithSchema 58 | (Schema 59 | withEmptyMeta 60 | (IntegerSchema 61 | (NumberValidations 62 | (Just 2.0) 63 | (Just 2.0) 64 | (Just 2.0) 65 | (Just 1.0) 66 | (Just 1.0) 67 | ) 68 | ) 69 | Nothing 70 | ) 71 | , test "number schema" <| 72 | \() -> 73 | [ ( "type", Encode.string "number" ) ] 74 | |> decodeSchema 75 | |> shouldResultWithSchema 76 | (Schema 77 | withEmptyMeta 78 | (FloatSchema 79 | (NumberValidations 80 | Nothing 81 | Nothing 82 | Nothing 83 | Nothing 84 | Nothing 85 | ) 86 | ) 87 | Nothing 88 | ) 89 | , test "string schema" <| 90 | \() -> 91 | [ ( "type", Encode.string "string" ) ] 92 | |> decodeSchema 93 | |> shouldResultWithSchema 94 | (Schema 95 | withEmptyMeta 96 | (StringSchema 97 | (StringValidations 98 | Nothing 99 | Nothing 100 | Nothing 101 | ) 102 | ) 103 | Nothing 104 | ) 105 | , test "undefined schema" <| 106 | \() -> 107 | [] 108 | |> decodeSchema 109 | |> shouldResultWithSchema 110 | (Schema 111 | withEmptyMeta 112 | (Undefined 113 | (NumberValidations 114 | Nothing 115 | Nothing 116 | Nothing 117 | Nothing 118 | Nothing 119 | ) 120 | (StringValidations 121 | Nothing 122 | Nothing 123 | Nothing 124 | ) 125 | ) 126 | Nothing 127 | ) 128 | , test "list of one" <| 129 | \() -> 130 | [ ( "type" 131 | , Encode.list [ Encode.string "string" ] 132 | ) 133 | ] 134 | |> decodeSchema 135 | |> shouldResultWithSchema 136 | (Schema 137 | withEmptyMeta 138 | (StringSchema 139 | (StringValidations 140 | Nothing 141 | Nothing 142 | Nothing 143 | ) 144 | ) 145 | Nothing 146 | ) 147 | , test "nullable type" <| 148 | \() -> 149 | [ ( "type" 150 | , [ "string", "null" ] 151 | |> List.map Encode.string 152 | |> Encode.list 153 | ) 154 | ] 155 | |> decodeSchema 156 | |> shouldResultWithSchema 157 | (Schema 158 | withEmptyMeta 159 | (Undefined 160 | (NumberValidations 161 | Nothing 162 | Nothing 163 | Nothing 164 | Nothing 165 | Nothing 166 | ) 167 | (StringValidations 168 | Nothing 169 | Nothing 170 | Nothing 171 | ) 172 | ) 173 | Nothing 174 | ) 175 | ] 176 | 177 | withEmptyMeta : Meta 178 | withEmptyMeta = 179 | Meta 180 | Nothing 181 | Nothing 182 | Nothing 183 | Nothing 184 | 185 | shouldResultWithSchema : Schema -> Result x Schema -> Expect.Expectation 186 | shouldResultWithSchema s = 187 | s 188 | |> Ok 189 | |> Expect.equal 190 | 191 | 192 | decodeSchema : List ( String, Value ) -> Result String Schema 193 | decodeSchema list = 194 | list 195 | |> Encode.object 196 | |> JS.fromValue 197 | -------------------------------------------------------------------------------- /tests/Validations.elm: -------------------------------------------------------------------------------- 1 | module Validations exposing (all) 2 | 3 | import Json.Schema.Builder as JSB 4 | exposing 5 | ( buildSchema 6 | , boolSchema 7 | , withItem 8 | , withItems 9 | , withAdditionalItems 10 | , withContains 11 | , withProperties 12 | , withPatternProperties 13 | , withAdditionalProperties 14 | , withSchemaDependency 15 | , withPropNamesDependency 16 | , withPropertyNames 17 | , withType 18 | , withNullableType 19 | , withUnionType 20 | , withAllOf 21 | , withAnyOf 22 | , withOneOf 23 | , withMultipleOf 24 | , withMaximum 25 | , withMinimum 26 | , withExclusiveMaximum 27 | , withExclusiveMinimum 28 | , withPattern 29 | , withEnum 30 | , withRequired 31 | , withMaxLength 32 | , withMinLength 33 | , withMaxProperties 34 | , withMinProperties 35 | , withMaxItems 36 | , withMinItems 37 | , withUniqueItems 38 | , withConst 39 | , validate 40 | ) 41 | import Json.Encode as Encode exposing (int) 42 | import Json.Decode as Decode exposing (decodeValue) 43 | import Json.Schema.Validation as Validation exposing (Error, defaultOptions, JsonPointer, ValidationError(..)) 44 | import Json.Schema.Definitions exposing (blankSchema) 45 | import Test exposing (Test, describe, test, only) 46 | import Ref 47 | import Expect 48 | 49 | 50 | all : Test 51 | all = 52 | describe "validations" 53 | [ describe "multipleOf" 54 | [ test "success with int" <| 55 | \() -> 56 | buildSchema 57 | |> withMultipleOf 2 58 | |> JSB.validate defaultOptions (Encode.int 4) 59 | |> expectOk 60 | , test "success with float" <| 61 | \() -> 62 | buildSchema 63 | |> withMultipleOf 2.1 64 | |> JSB.validate defaultOptions (Encode.float 4.2) 65 | |> expectOk 66 | , test "success with periodic float" <| 67 | \() -> 68 | buildSchema 69 | |> withMultipleOf (1 / 3) 70 | |> JSB.validate defaultOptions (Encode.float (2 / 3)) 71 | |> expectOk 72 | , test "failure" <| 73 | \() -> 74 | buildSchema 75 | |> withMultipleOf 3 76 | |> JSB.validate defaultOptions (Encode.float (2 / 7)) 77 | |> Expect.equal (Err [ error [] <| MultipleOf 3 (2 / 7) ]) 78 | ] 79 | , describe "maximum" 80 | [ test "success" <| 81 | \() -> 82 | buildSchema 83 | |> withMaximum 2 84 | |> JSB.validate defaultOptions (Encode.int 2) 85 | |> expectOk 86 | , test "failure" <| 87 | \() -> 88 | buildSchema 89 | |> withMaximum 2 90 | |> JSB.validate defaultOptions (Encode.float 2.1) 91 | |> Expect.equal (Err [ error [] <| Maximum 2.0 2.1 ]) 92 | ] 93 | , describe "minimum" 94 | [ test "success" <| 95 | \() -> 96 | buildSchema 97 | |> withMinimum 2 98 | |> JSB.validate defaultOptions (Encode.int 2) 99 | |> expectOk 100 | , test "failure" <| 101 | \() -> 102 | buildSchema 103 | |> withMinimum 2 104 | |> JSB.validate defaultOptions (Encode.float 1.9) 105 | |> Expect.equal (Err [ error [] <| Minimum 2.0 1.9 ]) 106 | ] 107 | , describe "exclusiveMaximum" 108 | [ test "success" <| 109 | \() -> 110 | buildSchema 111 | |> withExclusiveMaximum 2 112 | |> JSB.validate defaultOptions (Encode.float 1.9) 113 | |> expectOk 114 | , test "failure" <| 115 | \() -> 116 | buildSchema 117 | |> withExclusiveMaximum 2 118 | |> JSB.validate defaultOptions (Encode.float 2) 119 | |> Expect.equal (Err [ error [] <| ExclusiveMaximum 2 2 ]) 120 | ] 121 | , describe "exclusiveMinimum" 122 | [ test "success" <| 123 | \() -> 124 | buildSchema 125 | |> withExclusiveMinimum 2 126 | |> JSB.validate defaultOptions (Encode.float 2.1) 127 | |> expectOk 128 | , test "failure" <| 129 | \() -> 130 | buildSchema 131 | |> withExclusiveMinimum 2 132 | |> JSB.validate defaultOptions (Encode.float 2) 133 | |> Expect.equal (Err [ error [] <| ExclusiveMinimum 2 2 ]) 134 | ] 135 | , describe "maxLength" 136 | [ test "success" <| 137 | \() -> 138 | buildSchema 139 | |> withMaxLength 3 140 | |> JSB.validate defaultOptions (Encode.string "foo") 141 | |> expectOk 142 | , test "success for non-strings" <| 143 | \() -> 144 | buildSchema 145 | |> withMaxLength 3 146 | |> JSB.validate defaultOptions (Encode.int 10000) 147 | |> expectOk 148 | , test "failure" <| 149 | \() -> 150 | buildSchema 151 | |> withMaxLength 2 152 | |> validate defaultOptions (Encode.string "foo") 153 | |> Expect.equal (Err [ error [] <| MaxLength 2 3 ]) 154 | ] 155 | , describe "minLength" 156 | [ test "success" <| 157 | \() -> 158 | buildSchema 159 | |> withMinLength 3 160 | |> validate defaultOptions (Encode.string "foo") 161 | |> expectOk 162 | , test "failure" <| 163 | \() -> 164 | buildSchema 165 | |> withMinLength 4 166 | |> validate defaultOptions (Encode.string "foo") 167 | |> Expect.equal (Err [ error [] <| MinLength 4 3 ]) 168 | ] 169 | , describe "pattern" 170 | [ test "success" <| 171 | \() -> 172 | buildSchema 173 | |> withPattern "o{2}" 174 | |> JSB.validate defaultOptions (Encode.string "foo") 175 | |> expectOk 176 | , test "failure" <| 177 | \() -> 178 | buildSchema 179 | |> withPattern "o{3}" 180 | |> JSB.validate defaultOptions (Encode.string "foo") 181 | |> Expect.equal (Err [ error [] <| Pattern "o{3}" "foo" ]) 182 | ] 183 | , describe "items: schema" 184 | [ test "success" <| 185 | \() -> 186 | buildSchema 187 | |> withItem (buildSchema |> withMaximum 10) 188 | |> JSB.validate defaultOptions (Encode.list [ int 1 ]) 189 | |> expectOk 190 | , test "failure" <| 191 | \() -> 192 | buildSchema 193 | |> withItem (buildSchema |> withMaximum 10) 194 | |> JSB.validate defaultOptions (Encode.list [ int 1, int 11 ]) 195 | |> Expect.equal (Err [ error [ "1" ] <| Maximum 10 11 ]) 196 | ] 197 | , describe "items: array of schema" 198 | [ test "success" <| 199 | \() -> 200 | buildSchema 201 | |> withItems 202 | [ buildSchema 203 | |> withMaximum 10 204 | , buildSchema 205 | |> withMaximum 100 206 | ] 207 | |> JSB.validate defaultOptions (Encode.list [ int 1, int 20 ]) 208 | |> expectOk 209 | , test "failure" <| 210 | \() -> 211 | buildSchema 212 | |> withItems 213 | [ buildSchema 214 | |> withMaximum 11 215 | , buildSchema 216 | |> withMaximum 100 217 | ] 218 | |> JSB.validate defaultOptions (Encode.list [ int 100, int 2 ]) 219 | |> Expect.equal (Err [ error [ "0" ] <| Maximum 11 100 ]) 220 | ] 221 | , describe "items: array of schema with additional items" 222 | [ test "success" <| 223 | \() -> 224 | buildSchema 225 | |> withItems 226 | [ buildSchema 227 | |> withMaximum 10 228 | , buildSchema 229 | |> withMaximum 100 230 | ] 231 | |> withAdditionalItems (buildSchema |> withMaximum 1) 232 | |> JSB.validate defaultOptions (Encode.list [ int 1, int 20, int 1 ]) 233 | |> expectOk 234 | , test "failure" <| 235 | \() -> 236 | buildSchema 237 | |> withItems 238 | [ buildSchema 239 | |> withMaximum 11 240 | , buildSchema 241 | |> withMaximum 100 242 | ] 243 | |> withAdditionalItems (buildSchema |> withMaximum 1) 244 | |> JSB.validate defaultOptions (Encode.list [ int 2, int 2, int 100 ]) 245 | |> Expect.equal (Err [ error [ "2" ] <| Maximum 1 100 ]) 246 | ] 247 | , describe "maxItems" 248 | [ test "success" <| 249 | \() -> 250 | buildSchema 251 | |> withMaxItems 3 252 | |> validate defaultOptions (Encode.list [ int 1, int 2 ]) 253 | |> expectOk 254 | , test "failure" <| 255 | \() -> 256 | buildSchema 257 | |> withMaxItems 2 258 | |> validate defaultOptions (Encode.list [ int 1, int 2, int 3 ]) 259 | |> Expect.equal (Err [ error [] <| MaxItems 2 3 ]) 260 | ] 261 | , describe "minItems" 262 | [ test "success" <| 263 | \() -> 264 | buildSchema 265 | |> withMinItems 2 266 | |> validate defaultOptions (Encode.list [ int 1, int 2, int 3 ]) 267 | |> expectOk 268 | , test "failure" <| 269 | \() -> 270 | buildSchema 271 | |> withMinItems 3 272 | |> validate defaultOptions (Encode.list [ int 1, int 2 ]) 273 | |> Expect.equal (Err [ error [] <| MinItems 3 2 ]) 274 | ] 275 | , describe "uniqueItems" 276 | [ test "success" <| 277 | \() -> 278 | buildSchema 279 | |> withUniqueItems True 280 | |> validate defaultOptions (Encode.list [ int 1, int 2, int 3 ]) 281 | |> expectOk 282 | , test "failure" <| 283 | \() -> 284 | buildSchema 285 | |> withUniqueItems True 286 | |> validate defaultOptions (Encode.list [ int 1, int 1 ]) 287 | |> Expect.equal (Err [ error [] <| UniqueItems (int 1) ]) 288 | ] 289 | , describe "contains" 290 | [ test "success" <| 291 | \() -> 292 | buildSchema 293 | |> withContains (buildSchema |> withMaximum 1) 294 | |> JSB.validate defaultOptions (Encode.list [ int 10, int 20, int 1 ]) 295 | |> expectOk 296 | , test "failure" <| 297 | \() -> 298 | buildSchema 299 | |> withContains (buildSchema |> withMaximum 1) 300 | |> JSB.validate defaultOptions (Encode.list [ int 10, int 20 ]) 301 | |> Expect.equal (Err [ error [] Contains ]) 302 | ] 303 | , describe "maxProperties" 304 | [ test "success" <| 305 | \() -> 306 | buildSchema 307 | |> withMaxProperties 3 308 | |> validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 309 | |> expectOk 310 | , test "failure" <| 311 | \() -> 312 | buildSchema 313 | |> withMaxProperties 1 314 | |> validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 315 | |> Expect.equal (Err [ error [] <| MaxProperties 1 2 ]) 316 | ] 317 | , describe "minProperties" 318 | [ test "success" <| 319 | \() -> 320 | buildSchema 321 | |> withMinProperties 1 322 | |> validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 323 | |> expectOk 324 | , test "failure" <| 325 | \() -> 326 | buildSchema 327 | |> withMinProperties 3 328 | |> validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 329 | |> Expect.equal (Err [ error [] <| MinProperties 3 2 ]) 330 | ] 331 | , describe "required" 332 | [ test "success" <| 333 | \() -> 334 | buildSchema 335 | |> withRequired [ "foo", "bar" ] 336 | |> validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 337 | |> expectOk 338 | , test "failure" <| 339 | \() -> 340 | buildSchema 341 | |> withRequired [ "foo", "bar" ] 342 | |> validate defaultOptions (Encode.object [ ( "foo", int 1 ) ]) 343 | |> Expect.equal (Err [ error [] <| Required [ "bar" ], error [ "bar" ] RequiredProperty ]) 344 | ] 345 | , describe "properties" 346 | [ test "success" <| 347 | \() -> 348 | buildSchema 349 | |> withProperties 350 | [ ( "foo", buildSchema |> withMaximum 10 ) 351 | , ( "bar", buildSchema |> withMaximum 20 ) 352 | ] 353 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 354 | |> expectOk 355 | , test "failure" <| 356 | \() -> 357 | buildSchema 358 | |> withProperties 359 | [ ( "foo", buildSchema |> withMaximum 10 ) 360 | , ( "bar", buildSchema |> withMaximum 20 ) 361 | ] 362 | |> JSB.validate defaultOptions (Encode.object [ ( "bar", int 28 ) ]) 363 | |> Expect.equal (Err [ error [ "bar" ] <| Maximum 20 28 ]) 364 | ] 365 | , describe "patternProperties" 366 | [ test "success" <| 367 | \() -> 368 | buildSchema 369 | |> withPatternProperties 370 | [ ( "o{2}", buildSchema |> withMaximum 10 ) 371 | , ( "a", buildSchema |> withMaximum 20 ) 372 | ] 373 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 374 | |> expectOk 375 | , test "failure" <| 376 | \() -> 377 | buildSchema 378 | |> withPatternProperties 379 | [ ( "o{2}", buildSchema |> withMaximum 10 ) 380 | , ( "a", buildSchema |> withMaximum 20 ) 381 | ] 382 | |> JSB.validate defaultOptions (Encode.object [ ( "bar", int 28 ) ]) 383 | |> Expect.equal (Err [ error [ "bar" ] <| Maximum 20 28 ]) 384 | ] 385 | , describe "additionalProperties" 386 | [ test "success: pattern" <| 387 | \() -> 388 | buildSchema 389 | |> withPatternProperties 390 | [ ( "o{2}", buildSchema |> withMaximum 100 ) 391 | ] 392 | |> withAdditionalProperties (buildSchema |> withMaximum 20) 393 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 100 ), ( "bar", int 2 ) ]) 394 | |> expectOk 395 | , test "success: props" <| 396 | \() -> 397 | buildSchema 398 | |> withProperties 399 | [ ( "foo", buildSchema |> withMaximum 100 ) 400 | ] 401 | |> withAdditionalProperties (buildSchema |> withMaximum 20) 402 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 100 ), ( "bar", int 2 ) ]) 403 | |> expectOk 404 | , test "success: boolean true" <| 405 | \() -> 406 | buildSchema 407 | |> withProperties 408 | [ ( "foo", buildSchema |> withMaximum 100 ) 409 | ] 410 | |> withAdditionalProperties (boolSchema True) 411 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 100 ), ( "bar", int 2 ) ]) 412 | |> expectOk 413 | , test "failure" <| 414 | \() -> 415 | buildSchema 416 | |> withPatternProperties 417 | [ ( "o{2}", buildSchema |> withMaximum 100 ) 418 | ] 419 | |> withAdditionalProperties (buildSchema |> withMaximum 20) 420 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 100 ), ( "bar", int 200 ) ]) 421 | |> Expect.equal (Err [ error [ "bar" ] <| Maximum 20 200 ]) 422 | , test "success: boolean false" <| 423 | \() -> 424 | buildSchema 425 | |> withPatternProperties 426 | [ ( "o{2}", buildSchema |> withMaximum 100 ) 427 | ] 428 | |> withAdditionalProperties (boolSchema False) 429 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 100 ) ]) 430 | |> expectOk 431 | , test "failure: boolean false" <| 432 | \() -> 433 | buildSchema 434 | |> withPatternProperties 435 | [ ( "o{2}", buildSchema |> withMaximum 100 ) 436 | ] 437 | |> withAdditionalProperties (boolSchema False) 438 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 100 ), ( "bar", int 200 ) ]) 439 | |> Expect.equal (Err [ error [] <| AdditionalPropertiesDisallowed [ "bar" ], error [ "bar" ] AdditionalPropertyDisallowed ]) 440 | ] 441 | , describe "dependencies" 442 | [ test "success" <| 443 | \() -> 444 | buildSchema 445 | |> withSchemaDependency 446 | "foo" 447 | (buildSchema |> withRequired [ "bar" ]) 448 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 449 | |> expectOk 450 | , test "failure when dependency is a schema" <| 451 | \() -> 452 | buildSchema 453 | |> withSchemaDependency 454 | "foo" 455 | (buildSchema |> withRequired [ "bar" ]) 456 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 1 ) ]) 457 | |> Expect.equal (Err [ error [] <| Required [ "bar" ], error [ "bar" ] RequiredProperty ]) 458 | --|> Expect.equal (Err "Required property 'bar' is missing") 459 | , test "failure when dependency is array of strings" <| 460 | \() -> 461 | buildSchema 462 | |> withPropNamesDependency "foo" [ "bar" ] 463 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 1 ) ]) 464 | |> Expect.equal (Err [ error [] <| Required [ "bar" ], error [ "bar" ] RequiredProperty ]) 465 | ] 466 | , describe "propertyNames" 467 | [ test "success" <| 468 | \() -> 469 | buildSchema 470 | |> withPropertyNames (buildSchema |> withPattern "^ba") 471 | |> JSB.validate defaultOptions (Encode.object [ ( "baz", int 1 ), ( "bar", int 2 ) ]) 472 | |> expectOk 473 | , test "failure" <| 474 | \() -> 475 | buildSchema 476 | |> withPropertyNames (buildSchema |> withPattern "^ba") 477 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 1 ), ( "bar", int 2 ) ]) 478 | |> Expect.equal (Err [ error [] <| InvalidPropertyName [ error [ "foo" ] <| Pattern "^ba" "foo" ] ]) 479 | ] 480 | , describe "enum" 481 | [ test "success" <| 482 | \() -> 483 | buildSchema 484 | |> withEnum [ int 1, int 2 ] 485 | |> validate defaultOptions (Encode.int 2) 486 | |> expectOk 487 | , test "failure" <| 488 | \() -> 489 | buildSchema 490 | |> withEnum [ int 1, int 2 ] 491 | |> validate defaultOptions (Encode.int 3) 492 | |> Expect.equal (Err [ error [] Enum ]) 493 | ] 494 | , describe "const" 495 | [ test "success" <| 496 | \() -> 497 | buildSchema 498 | |> withConst (int 1) 499 | |> validate defaultOptions (Encode.int 1) 500 | |> expectOk 501 | , test "failure" <| 502 | \() -> 503 | buildSchema 504 | |> withConst (int 1) 505 | |> validate defaultOptions (Encode.int 2) 506 | |> Expect.equal (Err [ error [] Const ]) 507 | ] 508 | , describe "type=string" 509 | [ test "success" <| 510 | \() -> 511 | buildSchema 512 | |> withType "string" 513 | |> JSB.validate defaultOptions (Encode.string "foo") 514 | |> expectOk 515 | , test "failure" <| 516 | \() -> 517 | buildSchema 518 | |> withType "string" 519 | |> JSB.validate defaultOptions (Encode.int 1) 520 | |> Expect.equal (Err [ error [] <| InvalidType "Expecting a String but instead got: 1" ]) 521 | ] 522 | , describe "type=number" 523 | [ test "success" <| 524 | \() -> 525 | buildSchema 526 | |> withType "number" 527 | |> JSB.validate defaultOptions (Encode.int 1) 528 | |> expectOk 529 | , test "failure" <| 530 | \() -> 531 | buildSchema 532 | |> withType "number" 533 | |> JSB.validate defaultOptions (Encode.string "bar") 534 | |> Expect.equal (Err [ error [] <| InvalidType "Expecting a Float but instead got: \"bar\"" ]) 535 | , test "failure with null" <| 536 | \() -> 537 | buildSchema 538 | |> withType "number" 539 | |> JSB.validate defaultOptions Encode.null 540 | |> Expect.equal (Err [ error [] <| InvalidType "Expecting a Float but instead got: null" ]) 541 | ] 542 | , describe "type=null,number" 543 | [ test "success" <| 544 | \() -> 545 | buildSchema 546 | |> withNullableType "number" 547 | |> JSB.validate defaultOptions (Encode.int 1) 548 | |> expectOk 549 | , test "success with null" <| 550 | \() -> 551 | buildSchema 552 | |> withNullableType "number" 553 | |> JSB.validate defaultOptions Encode.null 554 | |> expectOk 555 | , test "failure" <| 556 | \() -> 557 | buildSchema 558 | |> withNullableType "number" 559 | |> JSB.validate defaultOptions (Encode.string "bar") 560 | |> Expect.equal (Err [ error [] <| InvalidType "Expecting a Float but instead got: \"bar\"" ]) 561 | ] 562 | , describe "type=number,string" 563 | [ test "success for number" <| 564 | \() -> 565 | buildSchema 566 | |> withUnionType [ "number", "string" ] 567 | |> JSB.validate defaultOptions (Encode.int 1) 568 | |> expectOk 569 | , test "success for string" <| 570 | \() -> 571 | buildSchema 572 | |> withUnionType [ "number", "string" ] 573 | |> JSB.validate defaultOptions (Encode.string "str") 574 | |> expectOk 575 | , test "failure for object" <| 576 | \() -> 577 | buildSchema 578 | |> withUnionType [ "number", "string" ] 579 | |> JSB.validate defaultOptions (Encode.object []) 580 | |> Expect.equal (Err [ error [] <| InvalidType "None of desired types match" ]) 581 | ] 582 | , describe "allOf" 583 | [ test "success" <| 584 | \() -> 585 | buildSchema 586 | |> withAllOf 587 | [ buildSchema |> withMinimum 0 588 | , buildSchema |> withMaximum 1 589 | ] 590 | |> JSB.validate defaultOptions (Encode.int 1) 591 | |> expectOk 592 | , test "failure because of minimum" <| 593 | \() -> 594 | buildSchema 595 | |> withAllOf 596 | [ buildSchema |> withMinimum 0 597 | , buildSchema |> withMaximum 1 598 | ] 599 | |> JSB.validate defaultOptions (Encode.int -1) 600 | |> Expect.equal (Err [ error [] <| Minimum 0 -1 ]) 601 | , test "failure because of maximum" <| 602 | \() -> 603 | buildSchema 604 | |> withAllOf 605 | [ buildSchema |> withMinimum 0 606 | , buildSchema |> withMaximum 1 607 | ] 608 | |> JSB.validate defaultOptions (Encode.int 2) 609 | |> Expect.equal (Err [ error [] <| Maximum 1 2 ]) 610 | ] 611 | , describe "anyOf" 612 | [ test "success for enum" <| 613 | \() -> 614 | buildSchema 615 | |> withAllOf 616 | [ buildSchema |> withMinimum 0 617 | , buildSchema |> withEnum [ int 1 ] 618 | ] 619 | |> JSB.validate defaultOptions (Encode.int 1) 620 | |> expectOk 621 | , test "success for minimum" <| 622 | \() -> 623 | buildSchema 624 | |> withAnyOf 625 | [ buildSchema |> withMinimum 0 626 | , buildSchema |> withEnum [ int 1 ] 627 | ] 628 | |> JSB.validate defaultOptions (Encode.float 0.5) 629 | |> expectOk 630 | , test "failure" <| 631 | \() -> 632 | buildSchema 633 | |> withAnyOf 634 | [ buildSchema |> withMinimum 0 635 | , buildSchema |> withEnum [ int 1 ] 636 | ] 637 | |> JSB.validate defaultOptions (Encode.int -1) 638 | |> Expect.equal 639 | (Err 640 | [ error [] <| Minimum 0 -1 641 | , error [] <| Enum 642 | ] 643 | ) 644 | ] 645 | , describe "oneOf" 646 | [ test "success for enum" <| 647 | \() -> 648 | buildSchema 649 | |> withOneOf 650 | [ buildSchema |> withMinimum 10 651 | , buildSchema |> withEnum [ int 1 ] 652 | ] 653 | |> JSB.validate defaultOptions (Encode.int 1) 654 | |> expectOk 655 | , test "success for minimum" <| 656 | \() -> 657 | buildSchema 658 | |> withOneOf 659 | [ buildSchema |> withMinimum 0 660 | , buildSchema |> withEnum [ int 1 ] 661 | ] 662 | |> JSB.validate defaultOptions (Encode.int 0) 663 | |> expectOk 664 | , test "failure for all" <| 665 | \() -> 666 | buildSchema 667 | |> withOneOf 668 | [ buildSchema |> withMinimum 0 669 | , buildSchema |> withEnum [ int 1 ] 670 | ] 671 | |> JSB.validate defaultOptions (Encode.int -1) 672 | |> Expect.equal (Err [ error [] OneOfNoneSucceed ]) 673 | , test "failure because of success for both" <| 674 | \() -> 675 | buildSchema 676 | |> withOneOf 677 | [ buildSchema |> withMinimum 0 678 | , buildSchema |> withEnum [ int 1 ] 679 | ] 680 | |> JSB.validate defaultOptions (Encode.int 1) 681 | |> Expect.equal (Err [ error [] <| OneOfManySucceed 2 ]) 682 | ] 683 | , describe "boolean schema" 684 | [ test "true always validates any value" <| 685 | \() -> 686 | Encode.bool True 687 | |> decodeValue Json.Schema.Definitions.decoder 688 | |> Result.withDefault blankSchema 689 | |> (\s -> Validation.validate defaultOptions Ref.defaultPool (int 1) s s) 690 | |> expectOk 691 | , test "false always fails validation" <| 692 | \() -> 693 | Encode.bool False 694 | |> decodeValue Json.Schema.Definitions.decoder 695 | |> Result.withDefault blankSchema 696 | |> (\s -> Validation.validate defaultOptions Ref.defaultPool (int 1) s s) 697 | |> Expect.equal (Err [ error [] AlwaysFail ]) 698 | ] 699 | , describe "multiple errors" 700 | [ test "validation should return multiple errors" <| 701 | \() -> 702 | buildSchema 703 | |> withProperties 704 | [ ( "foo", buildSchema |> withMaximum 1 ) 705 | , ( "bar", buildSchema |> withMaximum 2 ) 706 | ] 707 | |> JSB.validate defaultOptions (Encode.object [ ( "foo", int 7 ), ( "bar", int 28 ) ]) 708 | |> Expect.equal 709 | (Err 710 | [ error [ "foo" ] <| Maximum 1 7 711 | , error [ "bar" ] <| Maximum 2 28 712 | ] 713 | ) 714 | ] 715 | , describe "defaults" 716 | [ test "apply default value" <| 717 | \() -> 718 | buildSchema 719 | |> withProperties 720 | [ ( "key", buildSchema 721 | |> withType "string" 722 | |> JSB.withDefault (Encode.string "def") 723 | ) ] 724 | |> JSB.validate { applyDefaults = True } (Encode.object []) 725 | |> Expect.equal 726 | (Ok <| Encode.object [ ("key", Encode.string "def" ) ]) 727 | , test "apply default value in nested object" <| 728 | \() -> 729 | buildSchema 730 | |> withProperties 731 | [ ( "obj", buildSchema 732 | |> withProperties 733 | [ ( "key", buildSchema 734 | |> withType "string" 735 | |> JSB.withDefault (Encode.string "def") 736 | ) ] 737 | ) ] 738 | |> JSB.validate { applyDefaults = True } (Encode.object []) 739 | |> Result.map (Encode.encode 0) 740 | |> Expect.equal 741 | (Ok """{"obj":{"key":"def"}}""") 742 | ] 743 | ] 744 | 745 | 746 | error : List String -> ValidationError -> Error 747 | error path = 748 | Error (JsonPointer "" path) 749 | 750 | 751 | expectOk : Result x a -> Expect.Expectation 752 | expectOk e = 753 | case e of 754 | Err x -> 755 | Expect.fail <| "Unexpected error: " ++ (toString x) 756 | 757 | Ok _ -> 758 | Expect.pass 759 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Test Suites", 4 | "repository": "https://github.com/1602/elm-json-schema.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "../src", 8 | "." 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 13 | "elm-community/elm-test": "4.0.0 <= v < 5.0.0", 14 | "elm-community/json-extra": "2.0.0 <= v < 3.0.0", 15 | "elm-community/list-extra": "6.1.0 <= v < 7.0.0", 16 | "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", 17 | "zwilias/elm-utf-tools": "1.0.1 <= v < 2.0.0" 18 | }, 19 | "elm-version": "0.18.0 <= v < 0.19.0" 20 | } 21 | --------------------------------------------------------------------------------