├── .gitmodules ├── LICENSE ├── README.md ├── bench.lua ├── jsonschema ├── init.lua └── store.lua ├── ljsonschema-scm-1.rockspec └── spec ├── TODO ├── extra ├── dependencies.json ├── empty.json ├── function.lua ├── ref.json ├── sanity.json └── table.json ├── jsonschema.json └── suite.lua /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/JSON-Schema-Test-Suite"] 2 | path = spec/JSON-Schema-Test-Suite 3 | url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Julien Desgats 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lsjonschema: JSON schema validator 2 | ================================== 3 | 4 | This library provides a JSON schema draft 4 validator for Lua. Note that 5 | even though it uses the JSON Schema semantics, it is neither bound or limited 6 | to JSON. It can be used to validate saner key/value data formats as well (Lua 7 | tables, msgpack, bencode, ...). 8 | 9 | It has been designed to validate incoming data for HTTP APIs so it is decently 10 | fast: it works by transforming the given schema into a pure Lua function 11 | on-the-fly. Work is currently in progress to make it as JIT-friendly as 12 | possible. 13 | 14 | Installation 15 | ------------ 16 | 17 | This module is pure Lua and does not depend on any particular JSON library 18 | (`cjson.null` will be used for `null` tokens, but you can override that if 19 | necessary, see *Advanced usage*) 20 | 21 | The preferred way to install this library is to use Luarocks: 22 | 23 | luarocks install ljsonschema 24 | 25 | Running the tests also requires the [`cjson`][cjson] library and the Telescope 26 | test runner: 27 | 28 | git submodule update --init --recrusive 29 | luarocks install net-url 30 | luarocks install lua-cjson 31 | luarocks install https://raw.githubusercontent.com/jdesgats/telescope/master/rockspecs/telescope-scm-1.rockspec 32 | tsc ./spec/suite.lua 33 | 34 | [cjson]: https://luarocks.org/modules/luarocks/lua-cjson 35 | 36 | Usage 37 | ----- 38 | 39 | ### Getting started 40 | 41 | ```lua 42 | local jsonschema = require 'jsonschema' 43 | 44 | -- Note: do cache the result of schema compilation as this is a quite 45 | -- expensive process 46 | local myvalidator = jsonschema.generate_validator { 47 | type = 'object', 48 | properties = { 49 | foo = { type = 'string' }, 50 | bar = { type = 'number' }, 51 | }, 52 | } 53 | 54 | print(myvalidator{ foo='hello', bar=42 }) 55 | ``` 56 | 57 | ### Advanced usage 58 | 59 | Some advanced features of JSON Schema are not possible to implement using the 60 | standard library and require third party libraries to be work. 61 | 62 | In order to not force one particular library, and not bloat this library for 63 | the simple schemas, extension points are provided: the `generate_validator` 64 | takes a second table argument that can be used to customise the generated 65 | parser. 66 | 67 | ```lua 68 | local v = jsonschema.generate_validator(schema, { 69 | -- a value used to check null elements in the validated documents 70 | -- defaults to `cjson.null` (if available) or `nil` 71 | null = null_token, 72 | 73 | -- function called to match patterns, defaults to string.find. 74 | -- The JSON schema specification mentions that the validator should obey 75 | -- the ECMA-262 specification but Lua pattern matching library is much more 76 | -- primitive than that. Users might want to use PCRE or other more powerful 77 | -- libraries here 78 | match_pattern = function(string, patt) 79 | return ... -- boolean value 80 | end, 81 | 82 | -- function called to resolve external schemas. It is called with the full 83 | -- url to fetch (without the fragment part) and must return the 84 | -- corresponding schema as a Lua table. 85 | -- There is no default implementation: this function must be provided if 86 | -- resolving external schemas is required. 87 | external_resolver = function(url) 88 | return ... -- Lua table 89 | end, 90 | 91 | -- name when generating the validator function, it might ease debugging as 92 | -- as it will appear in stack traces. 93 | name = "myschema", 94 | }) 95 | ``` 96 | 97 | Differences with JSONSchema 98 | --------------------------- 99 | 100 | Due to the nature of the Lua language, the full JSON schema support is 101 | difficult to reach. Some of the limitations can be solved using the advanced 102 | options detailed previously, but some features are not supported (correctly) 103 | at this time: 104 | 105 | * Empty tables and empty arrays are the same from Lua point of view 106 | * Unicode strings are considered as a stream of bytes (so length checks might 107 | not behave as expected) 108 | 109 | 110 | On the other hand, some extra features are supported: 111 | 112 | * The type `table` can be used to match arrays or objects, it is also much 113 | faster than `array` or `object` as it does not involve walking the table to 114 | find out if it's a sequence or a hash 115 | * The type `function` can be used to check for functions 116 | 117 | -------------------------------------------------------------------------------- /bench.lua: -------------------------------------------------------------------------------- 1 | -- JSON schema validator benchmarking tool 2 | 3 | local json = require 'cjson' 4 | local time = require 'posix.time' 5 | local jsonschema = require 'jsonschema-jit' 6 | 7 | local mrandom = math.random 8 | local clock_gettime = time.clock_gettime 9 | local CLOCK_PROCESS_CPUTIME_ID = time.CLOCK_PROCESS_CPUTIME_ID 10 | local function timer() 11 | local start = clock_gettime(CLOCK_PROCESS_CPUTIME_ID) 12 | return function() 13 | local cur = clock_gettime(CLOCK_PROCESS_CPUTIME_ID) 14 | return (cur.tv_sec + cur.tv_nsec/1e9) - (start.tv_sec + start.tv_nsec/1e9) 15 | end 16 | end 17 | 18 | local blacklist = { 19 | -- edge cases, not supported features 20 | ['object type matches objects'] = { 21 | ['an array is not an object'] = true, -- empty object/array confusion 22 | }, 23 | ['array type matches arrays'] = { 24 | ['an object is not an array'] = true, -- empty object/array confusion 25 | }, 26 | ['regexes are not anchored by default and are case sensitive'] = { 27 | ['recognized members are accounted for'] = true, -- uses a unsupported pattern construct 28 | }, 29 | ['minLength validation'] = { 30 | ['one supplementary Unicode code point is not long enough'] = true, -- unicode handling 31 | }, 32 | ['maxLength validation'] = { 33 | ['two supplementary Unicode code points is long enough'] = true, -- unicode handling 34 | }, 35 | } 36 | 37 | local supported = { 38 | 'spec/extra/sanity.json', 39 | 'spec/extra/empty.json', 40 | 'spec/JSON-Schema-Test-Suite/tests/draft4/type.json', 41 | -- objects 42 | 'spec/JSON-Schema-Test-Suite/tests/draft4/properties.json', 43 | 'spec/JSON-Schema-Test-Suite/tests/draft4/required.json', 44 | 'spec/JSON-Schema-Test-Suite/tests/draft4/additionalProperties.json', 45 | 'spec/JSON-Schema-Test-Suite/tests/draft4/patternProperties.json', 46 | 'spec/JSON-Schema-Test-Suite/tests/draft4/minProperties.json', 47 | 'spec/JSON-Schema-Test-Suite/tests/draft4/maxProperties.json', 48 | -- TODO: dependencies 49 | -- strings 50 | 'spec/JSON-Schema-Test-Suite/tests/draft4/minLength.json', 51 | 'spec/JSON-Schema-Test-Suite/tests/draft4/maxLength.json', 52 | 'spec/JSON-Schema-Test-Suite/tests/draft4/pattern.json', 53 | -- numbers 54 | 'spec/JSON-Schema-Test-Suite/tests/draft4/multipleOf.json', 55 | 'spec/JSON-Schema-Test-Suite/tests/draft4/minimum.json', 56 | 'spec/JSON-Schema-Test-Suite/tests/draft4/maximum.json', 57 | -- lists 58 | 'spec/JSON-Schema-Test-Suite/tests/draft4/items.json', 59 | 'spec/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json', 60 | 'spec/JSON-Schema-Test-Suite/tests/draft4/minItems.json', 61 | 'spec/JSON-Schema-Test-Suite/tests/draft4/maxItems.json', 62 | --'spec/JSON-Schema-Test-Suite/tests/draft4/uniqueItems.json', 63 | -- misc 64 | -- 'spec/JSON-Schema-Test-Suite/tests/draft4/enum.json', 65 | } 66 | 67 | local function decode_descriptor(path) 68 | local f = assert(io.open(path)) 69 | local testsuite = json.decode(assert(f:read('*a'))) 70 | f:close() 71 | return ipairs(testsuite) 72 | end 73 | 74 | local NRUNS = assert(tonumber(arg[1]), 'run count required') 75 | 76 | -- read all test cases 77 | local loadtimer = timer() 78 | local cases, ncases = {}, 0 79 | for _, descriptor in ipairs(supported) do 80 | for _, suite in decode_descriptor(descriptor) do 81 | local skipped = blacklist[suite.description] or {} 82 | if skipped ~= true then 83 | local validator = jsonschema.generate_validator(suite.schema, { 84 | name = suite.description, 85 | }) 86 | for _, case in ipairs(suite.tests) do 87 | --if case.valid then 88 | ncases = ncases+1 89 | cases[ncases] = { validator, case.data } 90 | --end 91 | end 92 | end 93 | end 94 | end 95 | print('testcases loaded:', loadtimer()) 96 | 97 | local runtimer = timer() 98 | for i=1, NRUNS do 99 | local case = cases[mrandom(ncases)] 100 | case[1](case[2]) 101 | end 102 | print('run ' .. NRUNS .. ' validations:', runtimer()) 103 | -------------------------------------------------------------------------------- /jsonschema/init.lua: -------------------------------------------------------------------------------- 1 | local store = require 'jsonschema.store' 2 | local tostring = tostring 3 | local pairs = pairs 4 | local ipairs = ipairs 5 | local unpack = unpack or table.unpack 6 | local sformat = string.format 7 | local mmax, mmodf = math.max, math.modf 8 | local tconcat = table.concat 9 | local coro_wrap = coroutine.wrap 10 | local coro_yield = coroutine.yield 11 | local DEBUG = os and os.getenv and os.getenv('DEBUG') == '1' 12 | 13 | -- default null token 14 | local default_null = nil 15 | do 16 | local ok, cjson = pcall(require, 'cjson') 17 | if ok then default_null = cjson.null end 18 | end 19 | 20 | -- 21 | -- Code generation 22 | -- 23 | 24 | local generate_validator -- forward declaration 25 | 26 | local codectx_mt = {} 27 | codectx_mt.__index = codectx_mt 28 | 29 | function codectx_mt:libfunc(globalname) 30 | local root = self._root 31 | local localname = root._globals[globalname] 32 | if not localname then 33 | localname = globalname:gsub('%.', '_') 34 | root._globals[globalname] = localname 35 | root:preface(sformat('local %s = %s', localname, globalname)) 36 | end 37 | return localname 38 | end 39 | 40 | function codectx_mt:localvar(init, nres) 41 | local names = {} 42 | local nloc = self._nloc 43 | nres = nres or 1 44 | for i=1, nres do 45 | names[i] = sformat('var_%d_%d', self._idx, nloc+i) 46 | end 47 | 48 | self:stmt(sformat('local %s = ', tconcat(names, ', ')), init or 'nil') 49 | self._nloc = nloc + nres 50 | return unpack(names) 51 | end 52 | 53 | function codectx_mt:param(n) 54 | self._nparams = mmax(n, self._nparams) 55 | return 'p_' .. n 56 | end 57 | 58 | function codectx_mt:label() 59 | local nlabel = self._nlabels + 1 60 | self._nlabels = nlabel 61 | return 'label_' .. nlabel 62 | end 63 | 64 | -- Returns an expression that will result in passed value. 65 | -- Currently user vlaues are stored in an array to avoid consuming a lot of local 66 | -- and upvalue slots. Array accesses are still decently fast. 67 | function codectx_mt:uservalue(val) 68 | local slot = #self._root._uservalues + 1 69 | self._root._uservalues[slot] = val 70 | return sformat('uservalues[%d]', slot) 71 | end 72 | 73 | 74 | 75 | 76 | 77 | 78 | local function q(s) return sformat('%q', s) end 79 | 80 | function codectx_mt:validator(path, schema) 81 | local ref = self._schema:child(path) 82 | local resolved = ref:resolve() 83 | local root = self._root 84 | local var = root._validators[resolved] 85 | if not var then 86 | var = root:localvar('nil') 87 | root._validators[resolved] = var 88 | root:stmt(sformat('%s = ', var), generate_validator(root:child(ref), resolved)) 89 | end 90 | return var 91 | end 92 | 93 | function codectx_mt:preface(...) 94 | assert(self._preface, 'preface is only available for root contexts') 95 | local n = #self._preface 96 | for i=1, select('#', ...) do 97 | self._preface[n+i] = select(i, ...) 98 | end 99 | self._preface[#self._preface+1] = '\n' 100 | end 101 | 102 | function codectx_mt:stmt(...) 103 | local n = #self._body 104 | for i=1, select('#', ...) do 105 | self._body[n+i] = select(i, ...) 106 | end 107 | self._body[#self._body+1] = '\n' 108 | end 109 | 110 | -- load doesn't like at all empty string, but sometimes it is easier to add 111 | -- some in the chunk buffer 112 | local function yield_chunk(chunk) 113 | if chunk and chunk ~= '' then 114 | coro_yield(chunk) 115 | end 116 | end 117 | 118 | function codectx_mt:_generate() 119 | local indent = '' 120 | if self._root == self then 121 | for _, stmt in ipairs(self._preface) do 122 | yield_chunk(indent) 123 | if getmetatable(stmt) == codectx_mt then 124 | stmt:_generate() 125 | else 126 | yield_chunk(stmt) 127 | end 128 | end 129 | else 130 | coro_yield('function(') 131 | for i=1, self._nparams do 132 | yield_chunk('p_' .. i) 133 | if i ~= self._nparams then yield_chunk(', ') end 134 | end 135 | yield_chunk(')\n') 136 | indent = string.rep(' ', self._idx) 137 | end 138 | 139 | for _, stmt in ipairs(self._body) do 140 | yield_chunk(indent) 141 | if getmetatable(stmt) == codectx_mt then 142 | stmt:_generate() 143 | else 144 | yield_chunk(stmt) 145 | end 146 | end 147 | 148 | if self._root ~= self then 149 | yield_chunk('end') 150 | end 151 | end 152 | 153 | function codectx_mt:_get_loader() 154 | return coro_wrap(function() 155 | self:_generate() 156 | end) 157 | end 158 | 159 | function codectx_mt:as_string() 160 | local buf, n = {}, 0 161 | for chunk in self:_get_loader() do 162 | n = n+1 163 | buf[n] = chunk 164 | end 165 | return table.concat(buf) 166 | end 167 | 168 | function codectx_mt:as_func(name, ...) 169 | local loader, err = load(self:_get_loader(), 'jsonschema:' .. (name or 'anonymous')) 170 | if loader then 171 | local validator 172 | validator, err = loader(self._uservalues, ...) 173 | if validator then return validator end 174 | end 175 | 176 | -- something went really wrong 177 | if DEBUG then 178 | local line=1 179 | print('------------------------------') 180 | print('FAILED to generate validator: ', err) 181 | print('generated code:') 182 | print('0001: ' .. self:as_string():gsub('\n', function() 183 | line = line + 1 184 | return sformat('\n%04d: ', line) 185 | end)) 186 | print('------------------------------') 187 | end 188 | error(err) 189 | end 190 | 191 | -- returns a child code context with the current context as parent 192 | function codectx_mt:child(ref) 193 | return setmetatable({ 194 | _schema = ref, 195 | _idx = self._idx+1, 196 | _nloc = 0, 197 | _nlabels = 0, 198 | _body = {}, 199 | _root = self._root, 200 | _nparams = 0, 201 | }, codectx_mt) 202 | end 203 | 204 | -- returns a root code context. A root code context holds the library function 205 | -- cache (as upvalues for the child contexts), a preface, and no named params 206 | local function codectx(schema, options) 207 | local self = setmetatable({ 208 | _schema = store.new(schema, options.external_resolver), 209 | _id = schema.id, 210 | _path = '', 211 | _idx = 0, 212 | -- code generation 213 | _nloc = 0, 214 | _nlabels = 0, 215 | _preface = {}, 216 | _body = {}, 217 | _globals = {}, 218 | _uservalues = {}, 219 | -- schema management 220 | _validators = {}, -- maps paths to local variable validators 221 | _external_resolver = options.external_resolver, 222 | }, codectx_mt) 223 | self._root = self 224 | return self 225 | end 226 | 227 | 228 | -- 229 | -- Validator util functions (available in the validator context 230 | -- 231 | local validatorlib = {} 232 | 233 | -- TODO: this function is critical for performance, optimize it 234 | -- Returns: 235 | -- 0 for objects 236 | -- 1 for empty object/table (these two are indistinguishable in Lua) 237 | -- 2 for arrays 238 | function validatorlib.tablekind(t) 239 | local length = #t 240 | if length == 0 then 241 | if next(t) == nil then 242 | return 1 -- empty table 243 | else 244 | return 0 -- pure hash 245 | end 246 | end 247 | 248 | -- not empty, check if the number of items is the same as the length 249 | local items = 0 250 | for k, v in pairs(t) do items = items + 1 end 251 | if items == #t then 252 | return 2 -- array 253 | else 254 | return 0 -- mixed array/object 255 | end 256 | end 257 | 258 | -- used for unique items in arrays (not fast at all) 259 | -- from: http://stackoverflow.com/questions/25922437 260 | -- If we consider only the JSON case, this function could be simplified: 261 | -- no loops, keys are only strings. But this library might also be used in 262 | -- other cases. 263 | local function deepeq(table1, table2) 264 | local avoid_loops = {} 265 | local function recurse(t1, t2) 266 | -- compare value types 267 | if type(t1) ~= type(t2) then return false end 268 | -- Base case: compare simple values 269 | if type(t1) ~= "table" then return t1 == t2 end 270 | -- Now, on to tables. 271 | -- First, let's avoid looping forever. 272 | if avoid_loops[t1] then return avoid_loops[t1] == t2 end 273 | avoid_loops[t1] = t2 274 | -- Copy keys from t2 275 | local t2keys = {} 276 | local t2tablekeys = {} 277 | for k, _ in pairs(t2) do 278 | if type(k) == "table" then table.insert(t2tablekeys, k) end 279 | t2keys[k] = true 280 | end 281 | -- Let's iterate keys from t1 282 | for k1, v1 in pairs(t1) do 283 | local v2 = t2[k1] 284 | if type(k1) == "table" then 285 | -- if key is a table, we need to find an equivalent one. 286 | local ok = false 287 | for i, tk in ipairs(t2tablekeys) do 288 | if deepeq(k1, tk) and recurse(v1, t2[tk]) then 289 | table.remove(t2tablekeys, i) 290 | t2keys[tk] = nil 291 | ok = true 292 | break 293 | end 294 | end 295 | if not ok then return false end 296 | else 297 | -- t1 has a key which t2 doesn't have, fail. 298 | if v2 == nil then return false end 299 | t2keys[k1] = nil 300 | if not recurse(v1, v2) then return false end 301 | end 302 | end 303 | -- if t2 has a key which t1 doesn't have, fail. 304 | if next(t2keys) then return false end 305 | return true 306 | end 307 | return recurse(table1, table2) 308 | end 309 | validatorlib.deepeq = deepeq 310 | 311 | 312 | -- 313 | -- Validation generator 314 | -- 315 | 316 | -- generate an expression to check a JSON type 317 | local function typeexpr(ctx, jsontype, datatype, tablekind) 318 | -- TODO: optimize the type check for arays/objects (using NaN as kind?) 319 | if jsontype == 'object' then 320 | return sformat(' %s == "table" and %s <= 1 ', datatype, tablekind) 321 | elseif jsontype == 'array' then 322 | return sformat(' %s == "table" and %s >= 1 ', datatype, tablekind) 323 | elseif jsontype == 'table' then 324 | return sformat(' %s == "table" ', datatype) 325 | elseif jsontype == 'integer' then 326 | return sformat(' (%s == "number" and %s(%s, 1.0) == 0.0) ', 327 | datatype, ctx:libfunc('math.fmod'), ctx:param(1)) 328 | elseif jsontype == 'string' or jsontype == 'boolean' or jsontype == 'number' then 329 | return sformat('%s == %q', datatype, jsontype) 330 | elseif jsontype == 'null' then 331 | return sformat('%s == %s', ctx:param(1), ctx:libfunc('custom.null')) 332 | elseif jsontype == 'function' then 333 | return sformat(' %s == "function" ', datatype) 334 | else 335 | error('invalid JSON type: ' .. jsontype) 336 | end 337 | end 338 | 339 | generate_validator = function(ctx, schema) 340 | -- get type informations as they will be necessary anyway 341 | local datatype = ctx:localvar(sformat('%s(%s)', 342 | ctx:libfunc('type'), ctx:param(1))) 343 | local datakind = ctx:localvar(sformat('%s == "table" and %s(%s)', 344 | datatype, ctx:libfunc('lib.tablekind'), ctx:param(1))) 345 | 346 | -- type check 347 | local tt = type(schema.type) 348 | if tt == 'string' then 349 | -- only one type allowed 350 | ctx:stmt('if not (', typeexpr(ctx, schema.type, datatype, datakind), ') then') 351 | ctx:stmt(sformat(' return false, "wrong type: expected %s, got " .. %s', schema.type, datatype)) 352 | ctx:stmt('end') 353 | elseif tt == 'table' then 354 | -- multiple types allowed 355 | ctx:stmt('if not (') 356 | for _, t in ipairs(schema.type) do 357 | ctx:stmt(' ', typeexpr(ctx, t, datatype, datakind), ' or') 358 | end 359 | ctx:stmt('false) then') -- close the last "or" statement 360 | ctx:stmt(sformat(' return false, "wrong type: expected one of %s, got " .. %s', table.concat(schema.type, ', '), datatype)) 361 | ctx:stmt('end') 362 | elseif tt ~= 'nil' then error('invalid "type" type: got ' .. tt) end 363 | 364 | -- properties check 365 | if schema.properties or 366 | schema.additionalProperties or 367 | schema.patternProperties or 368 | schema.minProperties or 369 | schema.maxProperties or 370 | schema.dependencies or 371 | schema.required 372 | then 373 | -- check properties, this differs from the spec as empty arrays are 374 | -- considered as object 375 | ctx:stmt(sformat('if %s == "table" and %s <= 1 then', datatype, datakind)) 376 | 377 | -- switch the required keys list to a set 378 | local required = {} 379 | local dependencies = schema.dependencies or {} 380 | local properties = schema.properties or {} 381 | if schema.required then 382 | for _, k in ipairs(schema.required) do required[k] = true end 383 | end 384 | 385 | -- opportunistically count keys if we walk the table 386 | local needcount = schema.minProperties or schema.maxProperties 387 | if needcount then 388 | ctx:stmt( ' local propcount = 0') 389 | end 390 | 391 | for prop, subschema in pairs(properties) do 392 | -- generate validator 393 | local propvalidator = ctx:validator({ 'properties', prop }, subschema) 394 | ctx:stmt( ' do') 395 | ctx:stmt(sformat( ' local propvalue = %s[%q]', ctx:param(1), prop)) 396 | ctx:stmt( ' if propvalue ~= nil then') 397 | ctx:stmt(sformat( ' local ok, err = %s(propvalue)', propvalidator)) 398 | ctx:stmt( ' if not ok then') 399 | ctx:stmt(sformat( " return false, 'property %q validation failed: ' .. err", prop)) 400 | ctx:stmt( ' end') 401 | 402 | if dependencies[prop] then 403 | local d = dependencies[prop] 404 | if #d > 0 then 405 | -- dependency is a list of properties 406 | for _, depprop in ipairs(d) do 407 | ctx:stmt(sformat(' if %s[%q] == nil then', ctx:param(1), depprop)) 408 | ctx:stmt(sformat(" return false, 'property %q is required when %q is set'", depprop, prop)) 409 | ctx:stmt( ' end') 410 | end 411 | else 412 | -- dependency is a schema 413 | local depvalidator = ctx:validator({ 'dependencies', prop }, d) 414 | -- ok and err are already defined in this block 415 | ctx:stmt(sformat(' ok, err = %s(%s)', depvalidator, ctx:param(1))) 416 | ctx:stmt( ' if not ok then') 417 | ctx:stmt(sformat(" return false, 'failed to validate dependent schema for %q: ' .. err", prop)) 418 | ctx:stmt( ' end') 419 | end 420 | end 421 | 422 | if required[prop] then 423 | ctx:stmt( ' else') 424 | ctx:stmt(sformat(" return false, 'property %q is required'", prop)) 425 | required[prop] = nil 426 | end 427 | ctx:stmt( ' end') -- if prop 428 | ctx:stmt( ' end') -- do 429 | end 430 | 431 | -- check the rest of required fields 432 | for prop, _ in pairs(required) do 433 | ctx:stmt(sformat(' if %s[%q] == nil then', ctx:param(1), prop)) 434 | ctx:stmt(sformat(" return false, 'property %q is required'", prop)) 435 | ctx:stmt( ' end') 436 | end 437 | 438 | -- check the rest of dependencies 439 | for prop, d in pairs(dependencies) do 440 | if not properties[prop] then 441 | if #d > 0 then 442 | -- dependencies are a list of properties 443 | for _, depprop in ipairs(d) do 444 | ctx:stmt(sformat(' if %s[%q] ~= nil and %s[%q] == nil then', ctx:param(1), prop, ctx:param(1), depprop)) 445 | ctx:stmt(sformat(" return false, 'property %q is required when %q is set'", depprop, prop)) 446 | ctx:stmt( ' end') 447 | end 448 | else 449 | -- dependency is a schema 450 | local depvalidator = ctx:validator({ 'dependencies', prop }, d) 451 | ctx:stmt(sformat(' if %s[%q] ~= nil then', ctx:param(1), prop)) 452 | ctx:stmt(sformat(' local ok, err = %s(%s)', depvalidator, ctx:param(1))) 453 | ctx:stmt( ' if not ok then') 454 | ctx:stmt(sformat(" return false, 'failed to validate dependent schema for %q: ' .. err", prop)) 455 | ctx:stmt( ' end') 456 | ctx:stmt( ' end') 457 | end 458 | end 459 | end 460 | 461 | -- patternProperties and additionalProperties 462 | local propset, addprop_validator -- all properties defined in the object 463 | if schema.additionalProperties ~= nil and schema.additionalProperties ~= true then 464 | -- TODO: can be optimized with a static table expression 465 | propset = ctx._root:localvar('{}') 466 | if schema.properties then 467 | for prop, _ in pairs(schema.properties) do 468 | ctx._root:stmt(sformat('%s[%q] = true', propset, prop)) 469 | end 470 | end 471 | 472 | if type(schema.additionalProperties) == 'table' then 473 | addprop_validator = ctx:validator({ 'additionalProperties' }, schema.additionalProperties) 474 | end 475 | end 476 | 477 | -- patternProperties and additionalProperties are matched together whenever 478 | -- possible in order to walk the table only once 479 | if schema.patternProperties then 480 | local patterns = {} 481 | for patt, patt_schema in pairs(schema.patternProperties) do 482 | patterns[patt] = ctx:validator({ 'patternProperties', patt }, patt_schema ) 483 | end 484 | 485 | ctx:stmt(sformat( ' for prop, value in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1))) 486 | if propset then 487 | ctx:stmt( ' local matched = false') 488 | for patt, validator in pairs(patterns) do 489 | ctx:stmt(sformat(' if %s(prop, %q) then', ctx:libfunc('custom.match_pattern'), patt)) 490 | ctx:stmt(sformat(' local ok, err = %s(value)', validator)) 491 | ctx:stmt( ' if not ok then') 492 | ctx:stmt(sformat(" return false, 'failed to validate '..prop..' (matching %q): '..err", patt)) 493 | ctx:stmt( ' end') 494 | ctx:stmt( ' matched = true') 495 | ctx:stmt( ' end') 496 | end 497 | -- additional properties check 498 | ctx:stmt(sformat( ' if not (%s[prop] or matched) then', propset)) 499 | if addprop_validator then 500 | -- the additional properties must match a schema 501 | ctx:stmt(sformat(' local ok, err = %s(value)', addprop_validator)) 502 | ctx:stmt( ' if not ok then') 503 | ctx:stmt( " return false, 'failed to validate additional property '..prop..': '..err") 504 | ctx:stmt( ' end') 505 | else 506 | -- additional properties are forbidden 507 | ctx:stmt( ' return false, "additional properties forbidden, found " .. prop') 508 | end 509 | ctx:stmt( ' end') -- if not (%s[prop] or matched) 510 | else 511 | for patt, validator in pairs(patterns) do 512 | ctx:stmt(sformat(' if %s(prop, %q) then', ctx:libfunc('custom.match_pattern'), patt)) 513 | ctx:stmt(sformat(' local ok, err = %s(value)', validator)) 514 | ctx:stmt( ' if not ok then') 515 | ctx:stmt(sformat(" return false, 'failed to validate '..prop..' (matching %q): '..err", patt)) 516 | ctx:stmt( ' end') 517 | ctx:stmt( ' end') 518 | end 519 | end 520 | if needcount then 521 | ctx:stmt( ' propcount = propcount + 1') 522 | end 523 | ctx:stmt( ' end') -- for 524 | elseif propset then 525 | -- additionalProperties alone 526 | ctx:stmt(sformat( ' for prop, value in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1))) 527 | ctx:stmt(sformat( ' if not %s[prop] then', propset)) 528 | if addprop_validator then 529 | -- the additional properties must match a schema 530 | ctx:stmt(sformat(' local ok, err = %s(value)', addprop_validator)) 531 | ctx:stmt( ' if not ok then') 532 | ctx:stmt( " return false, 'failed to validate additional property '..prop..': '..err") 533 | ctx:stmt( ' end') 534 | else 535 | -- additional properties are forbidden 536 | ctx:stmt( ' return false, "additional properties forbidden, found " .. prop') 537 | end 538 | ctx:stmt( ' end') -- if not %s[prop] 539 | if needcount then 540 | ctx:stmt( ' propcount = propcount + 1') 541 | end 542 | ctx:stmt( ' end') -- for prop 543 | elseif needcount then 544 | -- we might still need to walk the table to get the number of properties 545 | ctx:stmt(sformat( ' for _, _ in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1))) 546 | ctx:stmt( ' propcount = propcount + 1') 547 | ctx:stmt( ' end') 548 | end 549 | 550 | if schema.minProperties then 551 | ctx:stmt(sformat(' if propcount < %d then', schema.minProperties)) 552 | ctx:stmt(sformat(' return false, "expect object to have at least %s properties"', schema.minProperties)) 553 | ctx:stmt( ' end') 554 | end 555 | if schema.maxProperties then 556 | ctx:stmt(sformat(' if propcount > %d then', schema.maxProperties)) 557 | ctx:stmt(sformat(' return false, "expect object to have at most %s properties"', schema.maxProperties)) 558 | ctx:stmt( ' end') 559 | end 560 | 561 | ctx:stmt('end') -- if object 562 | end 563 | 564 | -- array checks 565 | if schema.items or schema.minItems or schema.maxItems or schema.uniqueItems then 566 | ctx:stmt(sformat('if %s == "table" and %s >= 1 then', datatype, datakind)) 567 | 568 | -- this check is rather cheap so do it before validating the items 569 | -- NOTE: getting the size could be avoided in the list validation case, but 570 | -- this would mean validating items beforehand 571 | if schema.minItems or schema.maxItems then 572 | ctx:stmt(sformat( ' local itemcount = #%s', ctx:param(1))) 573 | if schema.minItems then 574 | ctx:stmt(sformat(' if itemcount < %d then', schema.minItems)) 575 | ctx:stmt(sformat(' return false, "expect array to have at least %s items"', schema.minItems)) 576 | ctx:stmt( ' end') 577 | end 578 | if schema.maxItems then 579 | ctx:stmt(sformat(' if itemcount > %d then', schema.maxItems)) 580 | ctx:stmt(sformat(' return false, "expect array to have at least %s items"', schema.maxItems)) 581 | ctx:stmt( ' end') 582 | end 583 | end 584 | 585 | if schema.items and #schema.items > 0 then 586 | -- each item has a specific schema applied (tuple validation) 587 | 588 | -- From the section 5.1.3.2, missing an array with missing items is 589 | -- still valid, because... Well because! So we have to jump after 590 | -- validations whenever we meet a nil value 591 | local after = ctx:label() 592 | for i, ischema in ipairs(schema.items) do 593 | -- JSON arrays are zero-indexed: remove 1 for URI path 594 | local ivalidator = ctx:validator({ 'items', tostring(i-1) }, ischema) 595 | ctx:stmt( ' do') 596 | ctx:stmt(sformat(' local item = %s[%d]', ctx:param(1), i)) 597 | ctx:stmt(sformat(' if item == nil then goto %s end', after)) 598 | ctx:stmt(sformat(' local ok, err = %s(item)', ivalidator)) 599 | ctx:stmt(sformat(' if not ok then')) 600 | ctx:stmt(sformat(' return false, "failed to validate item %d: " .. err', i)) 601 | ctx:stmt( ' end') 602 | ctx:stmt( ' end') 603 | end 604 | 605 | -- additional items check 606 | if schema.additionalItems == false then 607 | ctx:stmt(sformat(' if %s[%d] ~= nil then', ctx:param(1), #schema.items+1)) 608 | ctx:stmt( ' return false, "found unexpected extra items in array"') 609 | ctx:stmt( ' end') 610 | elseif type(schema.additionalItems) == 'table' then 611 | local validator = ctx:validator({ 'additionalItems' }, schema.additionalItems) 612 | ctx:stmt(sformat(' for i=%d, #%s do', #schema.items+1, ctx:param(1))) 613 | ctx:stmt(sformat(' local ok, err = %s(%s[i])', validator, ctx:param(1))) 614 | ctx:stmt(sformat(' if not ok then')) 615 | ctx:stmt(sformat(' return false, %s("failed to validate additional item %%d: %%s", i, err)', ctx:libfunc('string.format'))) 616 | ctx:stmt( ' end') 617 | ctx:stmt( ' end') 618 | end 619 | 620 | ctx:stmt(sformat( '::%s::', after)) 621 | elseif schema.items then 622 | -- all of the items has to match the same schema (list validation) 623 | local validator = ctx:validator({ 'items' }, schema.items) 624 | ctx:stmt(sformat(' for i, item in %s(%s) do', ctx:libfunc('ipairs'), ctx:param(1))) 625 | ctx:stmt(sformat(' local ok, err = %s(item)', validator)) 626 | ctx:stmt(sformat(' if not ok then')) 627 | ctx:stmt(sformat(' return false, %s("failed to validate item %%d: %%s", i, err)', ctx:libfunc('string.format'))) 628 | ctx:stmt( ' end') 629 | ctx:stmt( ' end') 630 | end 631 | 632 | -- TODO: this is slow as hell, could be optimized by storing value items 633 | -- in a spearate set, and calling deepeq only for references. 634 | if schema.uniqueItems then 635 | ctx:stmt(sformat(' for i=2, #%s do', ctx:param(1))) 636 | ctx:stmt( ' for j=1, i-1 do') 637 | ctx:stmt(sformat(' if %s(%s[i], %s[j]) then', ctx:libfunc('lib.deepeq'), ctx:param(1), ctx:param(1))) 638 | ctx:stmt(sformat(' return false, %s("expected unique items but items %%d and %%d are equal", i, j)', ctx:libfunc('string.format'))) 639 | ctx:stmt( ' end') 640 | ctx:stmt( ' end') 641 | ctx:stmt( ' end') 642 | end 643 | ctx:stmt('end') -- if array 644 | end 645 | 646 | if schema.minLength or schema.maxLength or schema.pattern then 647 | ctx:stmt(sformat('if %s == "string" then', datatype)) 648 | if schema.minLength then 649 | ctx:stmt(sformat(' if #%s < %d then', ctx:param(1), schema.minLength)) 650 | ctx:stmt(sformat(' return false, %s("string too short, expected at least %d, got %%d", #%s)', 651 | ctx:libfunc('string.format'), schema.minLength, ctx:param(1))) 652 | ctx:stmt( ' end') 653 | end 654 | if schema.maxLength then 655 | ctx:stmt(sformat(' if #%s > %d then', ctx:param(1), schema.maxLength)) 656 | ctx:stmt(sformat(' return false, %s("string too long, expected at most %d, got %%d", #%s)', 657 | ctx:libfunc('string.format'), schema.maxLength, ctx:param(1))) 658 | ctx:stmt( ' end') 659 | end 660 | if schema.pattern then 661 | ctx:stmt(sformat(' if not %s(%s, %q) then', ctx:libfunc('custom.match_pattern'), ctx:param(1), schema.pattern)) 662 | ctx:stmt(sformat(' return false, %s([[failed to match pattern %q with %%q]], %s)', ctx:libfunc('string.format'), schema.pattern, ctx:param(1))) 663 | ctx:stmt( ' end') 664 | end 665 | ctx:stmt('end') -- if string 666 | end 667 | 668 | if schema.minimum or schema.maximum or schema.multipleOf then 669 | ctx:stmt(sformat('if %s == "number" then', datatype)) 670 | 671 | if schema.minimum then 672 | local op = schema.exclusiveMinimum and '<=' or '<' 673 | local msg = schema.exclusiveMinimum and 'sctrictly greater' or 'greater' 674 | ctx:stmt(sformat(' if %s %s %s then', ctx:param(1), op, schema.minimum)) 675 | ctx:stmt(sformat(' return false, %s("expected %%s to be %s than %s", %s)', 676 | ctx:libfunc('string.format'), msg, schema.minimum, ctx:param(1))) 677 | ctx:stmt( ' end') 678 | end 679 | 680 | if schema.maximum then 681 | local op = schema.exclusiveMaximum and '>=' or '>' 682 | local msg = schema.exclusiveMaximum and 'sctrictly smaller' or 'smaller' 683 | ctx:stmt(sformat(' if %s %s %s then', ctx:param(1), op, schema.maximum)) 684 | ctx:stmt(sformat(' return false, %s("expected %%s to be %s than %s", %s)', 685 | ctx:libfunc('string.format'), msg, schema.maximum, ctx:param(1))) 686 | ctx:stmt( ' end') 687 | end 688 | 689 | local mof = schema.multipleOf 690 | if mof then 691 | -- TODO: optimize integer case 692 | if mmodf(mof) == mof then 693 | -- integer multipleOf: modulo is enough 694 | ctx:stmt(sformat(' if %s %% %d ~= 0 then', ctx:param(1), mof)) 695 | else 696 | -- float multipleOf: it's a bit more hacky and slow 697 | ctx:stmt(sformat(' local quotient = %s / %s', ctx:param(1), mof)) 698 | ctx:stmt(sformat(' if %s(quotient) ~= quotient then', ctx:libfunc('math.modf'))) 699 | end 700 | ctx:stmt(sformat( ' return false, %s("expected %%s to be a multiple of %s", %s)', 701 | ctx:libfunc('string.format'), mof, ctx:param(1))) 702 | ctx:stmt( ' end') 703 | end 704 | ctx:stmt('end') -- if number 705 | end 706 | 707 | -- enum values 708 | -- TODO: for big sets of hashable values (> 16 or so), it might be intersing to create a 709 | -- table beforehand 710 | if schema.enum then 711 | ctx:stmt('if not (') 712 | local lasti = #schema.enum 713 | for i, val in ipairs(schema.enum) do 714 | local tval = type(val) 715 | local op = i == lasti and '' or ' or' 716 | 717 | if tval == 'number' or tval == 'boolean' then 718 | ctx:stmt(sformat(' %s == %s', ctx:param(1), val), op) 719 | elseif tval == 'string' then 720 | ctx:stmt(sformat(' %s == %q', ctx:param(1), val), op) 721 | elseif tval == 'table' then 722 | ctx:stmt(sformat(' %s(%s, %s)', ctx:libfunc('lib.deepeq'), ctx:param(1), ctx:uservalue(val)), op) 723 | else 724 | error('unsupported enum type: ' .. tval) -- TODO: null 725 | end 726 | end 727 | ctx:stmt(') then') 728 | ctx:stmt(' return false, "matches non of the enum values"') 729 | ctx:stmt('end') 730 | end 731 | 732 | -- compound schemas 733 | -- (very naive implementation for now, can be optimized a lot) 734 | if schema.allOf then 735 | for i, subschema in ipairs(schema.allOf) do 736 | local validator = ctx:validator({ 'allOf', tostring(i-1) }, subschema) 737 | ctx:stmt( 'do') 738 | ctx:stmt(sformat(' local ok, err = %s(%s)', validator, ctx:param(1))) 739 | ctx:stmt(sformat(' if not ok then')) 740 | ctx:stmt(sformat(' return false, "allOf %d failed: " .. err', i)) 741 | ctx:stmt( ' end') 742 | ctx:stmt( 'end') 743 | end 744 | end 745 | 746 | if schema.anyOf then 747 | local lasti = #schema.anyOf 748 | ctx:stmt('if not (') 749 | for i, subschema in ipairs(schema.anyOf) do 750 | local op = i == lasti and '' or ' or' 751 | local validator = ctx:validator({ 'anyOf', tostring(i-1) }, subschema) 752 | ctx:stmt(sformat(' %s(%s)', validator, ctx:param(1)), op) 753 | end 754 | ctx:stmt(') then') 755 | ctx:stmt(' return false, "object matches none of the alternatives"') 756 | ctx:stmt('end') 757 | end 758 | 759 | if schema.oneOf then 760 | ctx:stmt('do') 761 | ctx:stmt(' local matched') 762 | for i, subschema in ipairs(schema.oneOf) do 763 | local validator = ctx:validator({ 'oneOf', tostring(i-1) }, subschema) 764 | ctx:stmt(sformat(' if %s(%s) then', validator, ctx:param(1))) 765 | ctx:stmt( ' if matched then') 766 | ctx:stmt(sformat(' return false, %s("value sould match only one schema, but matches both schemas %%d and %%d", matched, %d)', 767 | ctx:libfunc('string.format'), i)) 768 | ctx:stmt( ' end') 769 | ctx:stmt( ' matched = ', tostring(i)) 770 | ctx:stmt( ' end') 771 | end 772 | ctx:stmt(' if not matched then') 773 | ctx:stmt(' return false, "value sould match only one schema, but matches none"') 774 | ctx:stmt(' end') 775 | ctx:stmt('end') 776 | end 777 | 778 | if schema['not'] then 779 | local validator = ctx:validator({ 'not' }, schema['not']) 780 | ctx:stmt(sformat('if %s(%s) then', validator, ctx:param(1))) 781 | ctx:stmt( ' return false, "value wasn\'t supposed to match schema"') 782 | ctx:stmt( 'end') 783 | end 784 | 785 | ctx:stmt('return true') 786 | return ctx 787 | end 788 | 789 | local function generate_main_validator_ctx(schema, options) 790 | local ctx = codectx(schema, options or {}) 791 | -- the root function takes two parameters: 792 | -- * the validation library (auxiliary function used during validation) 793 | -- * the custom callbacks (used to customize various aspects of validation 794 | -- or for dependency injection) 795 | ctx:preface('local uservalues, lib, custom = ...') 796 | ctx:stmt('return ', ctx:validator(nil, schema)) 797 | return ctx 798 | end 799 | 800 | return { 801 | generate_validator = function(schema, custom) 802 | local customlib = { 803 | null = custom and custom.null or default_null, 804 | match_pattern = custom and custom.match_pattern or string.find 805 | } 806 | local name = custom and custom.name 807 | return generate_main_validator_ctx(schema, custom):as_func(name, validatorlib, customlib) 808 | end, 809 | -- debug only 810 | generate_validator_code = function(schema, custom) 811 | return generate_main_validator_ctx(schema, custom):as_string() 812 | end, 813 | } 814 | -------------------------------------------------------------------------------- /jsonschema/store.lua: -------------------------------------------------------------------------------- 1 | -- This module is a store for all schemas unsed in a code context. 2 | -- It is meant to deal with the id and $ref madness that JSON schema authors 3 | -- managed to put together. Resolving JSON references involves full URI 4 | -- parsing, absolute/relative URLs, scope management, id aliases, multipass 5 | -- parsing (as you have to walk the document a first time to discover all ids) 6 | -- and other niceties. 7 | -- 8 | -- Don't try to find any logic in this code, there isn't: this is just an 9 | -- implementation of [1] which is foreign to the concept of *logic*. 10 | -- 11 | -- [1] http://json-schema.org/latest/json-schema-core.html#rfc.section.8 12 | 13 | -- I gave up (for now) on doing a stripped down URI parser only for JSON schema 14 | -- needs 15 | local url = require 'net.url' 16 | local schar = string.char 17 | 18 | -- the net.url is kinda weird when some uri parts are missing (sometimes it is 19 | -- nil, sometimes it is an empty string) 20 | local function noe(s) return s == nil or s == '' end 21 | 22 | -- fetching and parsing external schemas requires a lot of dependencies, and 23 | -- depends a lot on the application ecosystem (e.g. piping curl, LuaSocket, 24 | -- cqueues, ...). Moreover, most sane schemas are self contained, so it is not 25 | -- even useful. 26 | -- So it is up to the user to provide a resolver if it's really needed 27 | local function default_resolver(uri) 28 | error('an external resolver is required to fetch ' .. uri) 29 | end 30 | 31 | local function percent_unescape(x) 32 | return schar(tonumber(x, 16)) 33 | end 34 | local tilde_unescape = { ['~0']='~', ['~1']='/' } 35 | local function urlunescape(fragment) 36 | return fragment:gsub('%%(%x%x)', percent_unescape):gsub('~[01]', tilde_unescape) 37 | end 38 | 39 | -- attempt to translate a URI fragemnt part to a valid table index: 40 | -- * if the part can be converted to number, that number+1 is returned to 41 | -- compensate with Lua 1-based indices 42 | -- * otherwise, the part is returned URL-escaped 43 | local function decodepart(part) 44 | local n = tonumber(part) 45 | return n and (n+1) or urlunescape(part) 46 | end 47 | 48 | 49 | -- a reference points to a particular node of a particular schema in the store 50 | local ref_mt = {} 51 | ref_mt.__index = ref_mt 52 | 53 | function ref_mt:child(items) 54 | if not (items and items[1]) then return self end 55 | local schema = self:resolve() 56 | for _, node in ipairs(items) do 57 | schema = assert(schema[decodepart(node)]) 58 | end 59 | return setmetatable({ store=self.store, schema=schema }, ref_mt) 60 | end 61 | 62 | function ref_mt:resolve() 63 | local schema = self.schema 64 | 65 | -- resolve references 66 | while schema['$ref'] do 67 | -- ok, this is a ref, but what kind of ref?!? 68 | local ctx = self.store:ctx(schema) 69 | local ref = url.parse(ctx.base.id):resolve(schema['$ref']) 70 | local fragment = ref.fragment 71 | 72 | -- get the target schema 73 | ref.fragment = nil 74 | schema = self.store:fetch(tostring(ref:normalize())) 75 | 76 | -- no fragment? just retrun the root 77 | if not fragment then 78 | return schema 79 | end 80 | 81 | -- maybe the fragment is a id alias 82 | local by_id = self.store:ctx(ctx.base).map[fragment] 83 | if by_id then 84 | schema = by_id 85 | else 86 | -- maybe not after all, walk the schema 87 | -- TODO: notrmalize path (if there is people mean enough to put '.' or 88 | -- '..' components) 89 | for part in fragment:gmatch('[^/]+') do 90 | part = decodepart(part) 91 | local new = schema[part] 92 | if not new then 93 | error(string.format('reference not found: %s#%s (at %q)', 94 | ref, fragment, part)) 95 | end 96 | schema = new 97 | end 98 | end 99 | end 100 | 101 | return schema 102 | end 103 | 104 | 105 | -- a store manage all currently required schemas 106 | -- it is not exposed directly 107 | local store_mt = {} 108 | store_mt.__index = store_mt 109 | 110 | function store_mt:ref(schema) 111 | return setmetatable({ 112 | store = self, 113 | schema = schema, 114 | }, ref_mt) 115 | end 116 | 117 | -- store of additional metadata by schema table part, this is to avoid 118 | -- modifying schema tables themselves. For now, we have 119 | -- 120 | -- * `base`: refers to the base schema (e.g. for a nested subschema to find 121 | -- its parent schema 122 | -- * `map`: only for "root" schemas, maps indetifiers to subschemas 123 | function store_mt:ctx(t) 124 | local c = self.ctx_store[t] 125 | if not c then 126 | c = {} 127 | self.ctx_store[t] = c 128 | end 129 | return c 130 | end 131 | 132 | function store_mt:fetch(uri) 133 | local schema = self.schemas[uri] 134 | if schema then return schema end 135 | 136 | -- schema not yet known 137 | schema = self.resolver(uri) 138 | if not schema then 139 | error('faild to fetch schema for: ' .. uri) 140 | end 141 | if not schema.id then 142 | schema.id = uri 143 | end 144 | self:insert(schema) 145 | return schema 146 | end 147 | 148 | 149 | -- functions used to walk a schema 150 | local function is_schema(path) 151 | local n = #path 152 | local parent, grandparent = path[n], path[n-1] 153 | 154 | return n == 0 or -- root node 155 | parent == 'additionalItems' or 156 | parent == 'additionalProperties' or 157 | parent == 'items' or 158 | parent == 'not' or 159 | (type(parent) == 'number' and ( 160 | grandparent == 'items' or 161 | grandparent == 'allOf' or 162 | grandparent == 'anyOf' or 163 | grandparent == 'oneOf' 164 | )) or 165 | grandparent == 'properties' or 166 | grandparent == 'patternProperties' or 167 | grandparent == 'definitions' or 168 | grandparent == 'dependencies' 169 | end 170 | 171 | function store_mt:insert(schema) 172 | local id = url.parse(assert(schema.id, 'id is required')) 173 | assert(noe(id.fragment), 'schema ids should not have fragments') 174 | schema.id = tostring(id:normalize()) 175 | self.schemas[schema.id] = schema 176 | local base_id = id 177 | 178 | -- walk the schema to collect the ids and populate the base field in context 179 | local map = {} 180 | 181 | local function walk(s, p) 182 | local id = s.id 183 | if id and s ~= schema and is_schema(p) then 184 | -- there is an id, but it is not over: we have 2 different cases (!) 185 | -- 1. the id is a fragment: it is some kind of an internal alias 186 | -- 2. the id is an url (relative or absolute): resolve it using the 187 | -- current base and use that as a new base. 188 | if id:sub(1,1) == '#' then 189 | -- fragment (case 1) 190 | map[id.fragment] = self:ref(s) 191 | else 192 | -- relative url (case 2) 193 | -- FIXME: I'm sure it's broken bacasue resolution scopes could be 194 | -- nested... but at the same time, who the hell would do this and it 195 | -- passes the tests so ¯\_(ツ)_/¯ 196 | local resolved = base_id:resolve(id) 197 | assert(noe(resolved.fragment), 'fragment in relative id') 198 | s.id = tostring(resolved:normalize()) 199 | return self:insert(s) 200 | end 201 | end 202 | 203 | self:ctx(s).base = schema 204 | for k, v in pairs(s) do 205 | if type(v) == 'table' and 206 | (type(k) == 'number' or ( 207 | k ~= 'enum' and 208 | k:sub(1,1) ~= '_' 209 | )) 210 | then 211 | table.insert(p, k) 212 | walk(v, p) 213 | table.remove(p) 214 | end 215 | end 216 | end 217 | walk(schema, {}) 218 | self:ctx(schema).map = map 219 | return self:ref(schema) 220 | end 221 | 222 | local function new(schema, resolver) 223 | local self = setmetatable({ 224 | ctx_store = {}, -- used to store metadata aobut schema parts 225 | schemas = {}, 226 | resolver = resolver or default_resolver, 227 | }, store_mt) 228 | 229 | schema.id = schema.id or 'root:' 230 | return self:insert(schema) 231 | end 232 | 233 | return { 234 | new = new, 235 | } 236 | -------------------------------------------------------------------------------- /ljsonschema-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "ljsonschema" 2 | version = "scm-1" 3 | source = { 4 | url = "git://github.com/jdesgats/ljsonschema.git", 5 | branch = "master", 6 | } 7 | description = { 8 | summary = "JSON Schema data validator", 9 | detailed = [[ 10 | This module is data validator the implements JSON Schema draft 4. 11 | Given an JSON schema, it will generates a validator function that can be used 12 | to validate any kind of data (not limited to JSON). 13 | ]], 14 | homepage = "https://github.com/jdesgats/ljsonschema", 15 | license = "MIT/X11" 16 | } 17 | dependencies = { 18 | "lua >= 5.1", 19 | "net-url", 20 | } 21 | build = { 22 | type = "builtin", 23 | modules = { 24 | jsonschema = "jsonschema/init.lua", 25 | ["jsonschema.store"] = "jsonschema/store.lua", 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/TODO: -------------------------------------------------------------------------------- 1 | * missing external resolver 2 | * external resovler error handling 3 | -------------------------------------------------------------------------------- /spec/extra/dependencies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "dependencies (with described properties)", 4 | "schema": { 5 | "properties": { 6 | "bar": { "type": "integer" } 7 | }, 8 | "dependencies": {"bar": ["foo"]} 9 | }, 10 | "tests": [ 11 | { 12 | "description": "neither", 13 | "data": {}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "nondependant", 18 | "data": {"foo": 1}, 19 | "valid": true 20 | }, 21 | { 22 | "description": "with dependency", 23 | "data": {"foo": 1, "bar": 2}, 24 | "valid": true 25 | }, 26 | { 27 | "description": "missing dependency", 28 | "data": {"bar": 2}, 29 | "valid": false 30 | }, 31 | { 32 | "description": "ignores non-objects", 33 | "data": "foo", 34 | "valid": true 35 | } 36 | ] 37 | }, 38 | { 39 | "description": "multiple dependencies subschema (with described properties)", 40 | "schema": { 41 | "properties": { 42 | "bar": { "type": "integer" } 43 | }, 44 | "dependencies": { 45 | "bar": { 46 | "properties": { 47 | "foo": {"type": "integer"}, 48 | "bar": {"type": "integer"} 49 | } 50 | } 51 | } 52 | }, 53 | "tests": [ 54 | { 55 | "description": "valid", 56 | "data": {"foo": 1, "bar": 2}, 57 | "valid": true 58 | }, 59 | { 60 | "description": "no dependency", 61 | "data": {"foo": "quux"}, 62 | "valid": true 63 | }, 64 | { 65 | "description": "wrong type", 66 | "data": {"foo": "quux", "bar": 2}, 67 | "valid": false 68 | }, 69 | { 70 | "description": "wrong type other", 71 | "data": {"foo": 2, "bar": "quux"}, 72 | "valid": false 73 | }, 74 | { 75 | "description": "wrong type both", 76 | "data": {"foo": "quux", "bar": "quux"}, 77 | "valid": false 78 | } 79 | ] 80 | } 81 | 82 | ] 83 | -------------------------------------------------------------------------------- /spec/extra/empty.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "empty objects and empty arrays are the same (array)", 4 | "schema": {"type": "array"}, 5 | "tests": [ 6 | { 7 | "description": "an empty array is an array", 8 | "data": [], 9 | "valid": true 10 | }, 11 | { 12 | "description": "an empty object is an array - AGAINST SPEC", 13 | "data": {}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "a non-empty array is an array", 18 | "data": ["foo", "bar"], 19 | "valid": true 20 | }, 21 | { 22 | "description": "a non-empty object is not an array", 23 | "data": {"answer": 42}, 24 | "valid": false 25 | } 26 | ] 27 | }, 28 | { 29 | "description": "empty objects and empty arrays are the same (object)", 30 | "schema": {"type": "object"}, 31 | "tests": [ 32 | { 33 | "description": "an empty array is an object - AGAINST SPEC", 34 | "data": [], 35 | "valid": true 36 | }, 37 | { 38 | "description": "an empty object is an object", 39 | "data": {}, 40 | "valid": true 41 | }, 42 | { 43 | "description": "a non-empty array is not an object", 44 | "data": ["foo", "bar"], 45 | "valid": false 46 | }, 47 | { 48 | "description": "a non-empty object is an object", 49 | "data": {"answer": 42}, 50 | "valid": true 51 | } 52 | ] 53 | }, 54 | { 55 | "description": "confusion with properties", 56 | "schema": { 57 | "properties": { 58 | "foo": {"type": "integer"} 59 | }, 60 | "required": ["foo"] 61 | }, 62 | "tests": [ 63 | { 64 | "description": "empty array validates against empty property set", 65 | "data": [], 66 | "valid": false 67 | }, 68 | { 69 | "description": "empty object validates against empty property set", 70 | "data": {}, 71 | "valid": false 72 | } 73 | ] 74 | } 75 | ] 76 | 77 | -------------------------------------------------------------------------------- /spec/extra/function.lua: -------------------------------------------------------------------------------- 1 | return { 2 | { 3 | description = "function type", 4 | schema = { type = "function" }, 5 | tests = { 6 | { 7 | description = "Lua function", 8 | data = function() end, 9 | valid = true 10 | }, 11 | { 12 | description = "C function", 13 | data = print, 14 | valid = true 15 | }, 16 | { 17 | description = "table", 18 | data = {}, 19 | valid = false 20 | }, 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /spec/extra/ref.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "pointer ref to object containing special chars", 4 | "schema": { 5 | "properties": { 6 | "foo/bar": {"type": "integer"}, 7 | "bar": {"$ref": "#/properties/foo%2fbar"} 8 | } 9 | }, 10 | "tests": [ 11 | { 12 | "description": "match", 13 | "data": {"bar": 3}, 14 | "valid": true 15 | }, 16 | { 17 | "description": "mismatch", 18 | "data": {"bar": true}, 19 | "valid": false 20 | } 21 | ] 22 | }, 23 | { 24 | "description": "relative pointer to definitions array", 25 | "schema": { 26 | "definitions": { 27 | "array_schema": { 28 | "items": [ 29 | {"type": "integer"}, 30 | {"type": "string"} 31 | ] 32 | } 33 | }, 34 | "items": [ 35 | {"$ref": "#/definitions/array_schema/items/1"}, 36 | {"$ref": "#/definitions/array_schema/items/0"} 37 | ] 38 | }, 39 | "tests": [ 40 | { 41 | "description": "match array", 42 | "data": ["foo", 2], 43 | "valid": true 44 | }, 45 | { 46 | "description": "mismatch array", 47 | "data": [1, "foo"], 48 | "valid": false 49 | } 50 | ] 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /spec/extra/sanity.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "empty schema validates everything", 4 | "schema": {}, 5 | "tests": [ 6 | { 7 | "description": "an integer is valid", 8 | "data": 1, 9 | "valid": true 10 | }, 11 | { 12 | "description": "a float is valid", 13 | "data": 1.1, 14 | "valid": true 15 | }, 16 | { 17 | "description": "a string is valid", 18 | "data": "foo", 19 | "valid": true 20 | }, 21 | { 22 | "description": "an object is valid", 23 | "data": {"foo": "bar"}, 24 | "valid": true 25 | }, 26 | { 27 | "description": "an array is valid", 28 | "data": [ 3.1415 ], 29 | "valid": true 30 | }, 31 | { 32 | "description": "a boolean is valid", 33 | "data": false, 34 | "valid": true 35 | }, 36 | { 37 | "description": "null is valid", 38 | "data": null, 39 | "valid": true 40 | } 41 | ] 42 | }, 43 | { 44 | "description": "type and properties interaction", 45 | "schema": { 46 | "type": "object", 47 | "properties": { 48 | "foo": {"type": "integer"} 49 | } 50 | }, 51 | "tests": [ 52 | { 53 | "description": "correct type and properties is valid", 54 | "data": {"foo": 42}, 55 | "valid": true 56 | }, 57 | { 58 | "description": "correct type but invalid properties is invalid", 59 | "data": {"foo": "bar"}, 60 | "valid": false 61 | }, 62 | { 63 | "description": "incorrect type is invalid", 64 | "data": ["foo", 42], 65 | "valid": false 66 | } 67 | ] 68 | }, 69 | { 70 | "description": "object properties with wrong type", 71 | "schema": { 72 | "properties": { 73 | "foo": {"type": "integer"}, 74 | "bar": {"type": "string"} 75 | }, 76 | "required": ["foo"] 77 | }, 78 | "tests": [ 79 | { 80 | "description": "ignores non-objects", 81 | "data": 42, 82 | "valid": true 83 | }, 84 | { 85 | "description": "ignores non-objects (empty array version) - AGAINST SPEC", 86 | "data": [], 87 | "valid": false 88 | } 89 | ] 90 | }, 91 | { 92 | "description": "additionalProperties can be true", 93 | "schema": { 94 | "type": "object", 95 | "additionalProperties": true 96 | }, 97 | "tests": [ 98 | { 99 | "description": "validates object with additional properties", 100 | "data": { "foo": "bar" }, 101 | "valid": true 102 | } 103 | ] 104 | } 105 | ] 106 | -------------------------------------------------------------------------------- /spec/extra/table.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "description": "table type", 4 | "schema": { "type": "table" }, 5 | "tests": [ 6 | { 7 | "description": "empty object", 8 | "data": {}, 9 | "valid": true 10 | }, 11 | { 12 | "description": "empty array", 13 | "data": [], 14 | "valid": true 15 | }, 16 | { 17 | "description": "non empty array", 18 | "data": [1,2,3], 19 | "valid": true 20 | }, 21 | { 22 | "description": "non empty object", 23 | "data": { "foo": "bar" }, 24 | "valid": true 25 | }, 26 | { 27 | "description": "number", 28 | "data": 42, 29 | "valid": false 30 | } 31 | ] 32 | }, 33 | { 34 | "description": "table can have properties", 35 | "schema": { 36 | "type": "table", 37 | "properties": { 38 | "foo": { "type": "integer" } 39 | }, 40 | "required": [ "foo" ], 41 | "additionalProperties": false 42 | }, 43 | "tests": [ 44 | { 45 | "description": "expected object", 46 | "data": { "foo": 42 }, 47 | "valid": true 48 | }, 49 | { 50 | "description": "additional proprty", 51 | "data": { "foo": 42, "bar": false }, 52 | "valid": false 53 | }, 54 | { 55 | "description": "wrong property type", 56 | "data": { "foo": "bar" }, 57 | "valid": false 58 | }, 59 | { 60 | "description": "mising property", 61 | "data": { }, 62 | "valid": false 63 | } 64 | ] 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /spec/jsonschema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /spec/suite.lua: -------------------------------------------------------------------------------- 1 | -- this test uses the official JSON schema test suite: 2 | -- https://github.com/json-schema-org/JSON-Schema-Test-Suite 3 | 4 | local json = require 'cjson' 5 | --local lfs = require 'lfs' 6 | local jsonschema = require 'jsonschema' 7 | 8 | local telescope = require 'telescope' 9 | telescope.make_assertion('success', "%s to be a success, got error '%s'", function(ok, err) return not not ok end) 10 | telescope.make_assertion('failure', "a failure with error '%s', got (%s, %s)", function(exp, ok, err) 11 | return not ok and err == exp 12 | end) 13 | telescope.status_labels[telescope.status_codes.err] = '\27[31;1mE\27[0m' 14 | telescope.status_labels[telescope.status_codes.fail] = '\27[31;1mF\27[0m' 15 | telescope.status_labels[telescope.status_codes.pass] = '\27[32mP\27[0m' 16 | telescope.status_labels[telescope.status_codes.pending] = '\27[34;1m?\27[0m' 17 | telescope.status_labels[telescope.status_codes.unassertive] = '\27[33;1mU\27[0m' 18 | 19 | -- the full support of JSON schema in Lua is difficult to achieve in some cases 20 | -- so some tests from the official test suite fail, skip them. 21 | local blacklist = { 22 | -- edge cases, not supported features 23 | ['object type matches objects'] = { 24 | ['an array is not an object'] = true, -- empty object/array confusion 25 | }, 26 | ['array type matches arrays'] = { 27 | ['an object is not an array'] = true, -- empty object/array confusion 28 | }, 29 | ['regexes are not anchored by default and are case sensitive'] = { 30 | ['recognized members are accounted for'] = true, -- uses a unsupported pattern construct 31 | }, 32 | ['minLength validation'] = { 33 | ['one supplementary Unicode code point is not long enough'] = true, -- unicode handling 34 | }, 35 | ['maxLength validation'] = { 36 | ['two supplementary Unicode code points is long enough'] = true, -- unicode handling 37 | }, 38 | } 39 | 40 | local supported = { 41 | 'spec/extra/sanity.json', 42 | 'spec/extra/empty.json', 43 | 'spec/JSON-Schema-Test-Suite/tests/draft4/type.json', 44 | -- objects 45 | 'spec/JSON-Schema-Test-Suite/tests/draft4/properties.json', 46 | 'spec/JSON-Schema-Test-Suite/tests/draft4/required.json', 47 | 'spec/JSON-Schema-Test-Suite/tests/draft4/additionalProperties.json', 48 | 'spec/JSON-Schema-Test-Suite/tests/draft4/patternProperties.json', 49 | 'spec/JSON-Schema-Test-Suite/tests/draft4/minProperties.json', 50 | 'spec/JSON-Schema-Test-Suite/tests/draft4/maxProperties.json', 51 | 'spec/JSON-Schema-Test-Suite/tests/draft4/dependencies.json', 52 | 'spec/extra/dependencies.json', 53 | -- strings 54 | 'spec/JSON-Schema-Test-Suite/tests/draft4/minLength.json', 55 | 'spec/JSON-Schema-Test-Suite/tests/draft4/maxLength.json', 56 | 'spec/JSON-Schema-Test-Suite/tests/draft4/pattern.json', 57 | -- numbers 58 | 'spec/JSON-Schema-Test-Suite/tests/draft4/multipleOf.json', 59 | 'spec/JSON-Schema-Test-Suite/tests/draft4/minimum.json', 60 | 'spec/JSON-Schema-Test-Suite/tests/draft4/maximum.json', 61 | -- lists 62 | 'spec/JSON-Schema-Test-Suite/tests/draft4/items.json', 63 | 'spec/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json', 64 | 'spec/JSON-Schema-Test-Suite/tests/draft4/minItems.json', 65 | 'spec/JSON-Schema-Test-Suite/tests/draft4/maxItems.json', 66 | 'spec/JSON-Schema-Test-Suite/tests/draft4/uniqueItems.json', 67 | -- misc 68 | 'spec/JSON-Schema-Test-Suite/tests/draft4/enum.json', 69 | 'spec/JSON-Schema-Test-Suite/tests/draft4/default.json', 70 | -- compound 71 | 'spec/JSON-Schema-Test-Suite/tests/draft4/allOf.json', 72 | 'spec/JSON-Schema-Test-Suite/tests/draft4/anyOf.json', 73 | 'spec/JSON-Schema-Test-Suite/tests/draft4/oneOf.json', 74 | 'spec/JSON-Schema-Test-Suite/tests/draft4/not.json', 75 | -- links/refs 76 | 'spec/JSON-Schema-Test-Suite/tests/draft4/ref.json', 77 | 'spec/JSON-Schema-Test-Suite/tests/draft4/refRemote.json', 78 | 'spec/JSON-Schema-Test-Suite/tests/draft4/definitions.json', 79 | 'spec/extra/ref.json', 80 | -- Lua extensions 81 | 'spec/extra/table.json', 82 | 'spec/extra/function.lua', 83 | } 84 | 85 | local function readjson(path) 86 | if path:match('%.json$') then 87 | local f = assert(io.open(path)) 88 | local body = json.decode(assert(f:read('*a'))) 89 | f:close() 90 | return body 91 | elseif path:match('%.lua$') then 92 | return dofile(path) 93 | end 94 | error('cannot read ' .. path) 95 | end 96 | 97 | local external_schemas = { 98 | ['http://json-schema.org/draft-04/schema'] = readjson('spec/jsonschema.json'), 99 | ['http://localhost:1234/integer.json'] = readjson('spec/JSON-Schema-Test-Suite/remotes/integer.json'), 100 | ['http://localhost:1234/subSchemas.json'] = readjson('spec/JSON-Schema-Test-Suite/remotes/subSchemas.json'), 101 | ['http://localhost:1234/folder/folderInteger.json'] = readjson('spec/JSON-Schema-Test-Suite/remotes/folder/folderInteger.json'), 102 | } 103 | 104 | local options = { 105 | external_resolver = function(url) 106 | return external_schemas[url] 107 | end, 108 | } 109 | 110 | for _, descriptor in ipairs(supported) do 111 | for _, suite in ipairs(readjson(descriptor)) do 112 | local skipped = blacklist[suite.description] or {} 113 | if skipped ~= true then 114 | describe(suite.description, function() 115 | local schema = suite.schema 116 | local validator 117 | before(function() 118 | local val, err = jsonschema.generate_validator(schema, options) 119 | assert_success(val, err) 120 | assert_type(val, 'function') 121 | validator = val 122 | package.loaded.valcode = jsonschema.generate_validator_code(schema, options) 123 | end) 124 | 125 | for _, case in ipairs(suite.tests) do 126 | if not skipped[case.description] then 127 | test(case.description, function() 128 | if case.valid then 129 | assert_true(validator(case.data)) 130 | else 131 | assert_false(validator(case.data)) 132 | -- TODO: test error message? 133 | end 134 | end) -- test 135 | end -- case skipped 136 | end -- for cases 137 | end) -- describe 138 | end -- suite skipped 139 | end -- for suite 140 | end -- for descriptor 141 | --------------------------------------------------------------------------------