├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── lib └── resty │ └── jsonschema │ └── compiler.lua ├── spec └── extra │ ├── README.md │ ├── dependencies.json │ ├── empty.json │ ├── sanity.json │ └── table.json ├── t └── test.lua └── util └── lua-releng /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.a 3 | *.so 4 | *.out 5 | *.pyc 6 | *.dylib 7 | *.dSYM/ 8 | 9 | *.swp 10 | *.swo 11 | *.orig 12 | *.un~ 13 | *.DS_Store 14 | 15 | tags 16 | 17 | spec/JSON-Schema-Test-Suite 18 | t/servroot 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2018 Jinzheng Zhang 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST_SUITE = JSON-Schema-Test-Suite 2 | TEST_SUITE_DEST = spec/$(TEST_SUITE) 3 | TEST_SUITE_URL = https://github.com/json-schema-org/$(TEST_SUITE) 4 | 5 | .PHONY: all test 6 | 7 | all: 8 | ifeq ($(wildcard $(TEST_SUITE_DEST)),) 9 | git clone $(TEST_SUITE_URL) $(TEST_SUITE_DEST) 10 | endif 11 | 12 | test: all 13 | resty t/test.lua 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lua-resty-jsonschema 2 | ==== 3 | 4 | The JSON Schema validator for OpenResty. Currently supports draft-04. 5 | 6 | 7 | # Status 8 | 9 | This library is still experimental and under early development. 10 | 11 | 12 | # License 13 | 14 | MIT 15 | -------------------------------------------------------------------------------- /lib/resty/jsonschema/compiler.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Jinzheng Zhang (tianchaijz). 2 | 3 | local cjson = require "cjson.safe" 4 | 5 | local type = type 6 | local next = next 7 | local pairs = pairs 8 | local ipairs = ipairs 9 | local assert = assert 10 | local tostring = tostring 11 | local floor = math.floor 12 | local string_rep = string.rep 13 | local string_match = string.match 14 | local string_format = string.format 15 | local concat = table.concat 16 | local table_remove = table.remove 17 | local loadstring = loadstring 18 | local setmetatable = setmetatable 19 | 20 | local ngx = ngx 21 | local re_find = ngx.re.find 22 | 23 | 24 | local function is_str(obj) return type(obj) == "string" end 25 | local function is_num(obj) return type(obj) == "number" end 26 | local function is_tbl(obj) return type(obj) == "table" end 27 | local function is_bool(obj) return type(obj) == "boolean" end 28 | local function is_nil(obj) return obj == nil end 29 | local function is_null(obj) return obj == cjson.null end 30 | local function is_int(obj) return is_num(obj) and floor(obj) == obj end 31 | 32 | 33 | local function table_type(obj) 34 | local max = 0 35 | local count = 0 36 | for k in pairs(obj) do 37 | if is_num(k) then 38 | if k > max then max = k end 39 | count = count + 1 40 | else 41 | return -1 42 | end 43 | end 44 | 45 | if max > count * 2 then 46 | return -1 47 | end 48 | 49 | return max 50 | end 51 | 52 | 53 | local table_new 54 | local table_nkeys 55 | local table_isarray 56 | local is_array 57 | local is_empty 58 | 59 | 60 | do 61 | local ok 62 | ok, table_new = pcall(require, "table.new") 63 | if not ok or type(table_new) ~= "function" then 64 | table_new = function(narr, nrec) return {} end 65 | end 66 | 67 | ok, table_nkeys = pcall(require, "table.nkeys") 68 | if not ok or type(table_nkeys) ~= "function" then 69 | table_nkeys = function(t) 70 | local n = 0 71 | for _ in pairs(t) do 72 | n = n + 1 73 | end 74 | return n 75 | end 76 | end 77 | 78 | ok, table_isarray = pcall(require, "table.isarray") 79 | if ok and type(table_isarray) == "function" then 80 | is_array = function(obj) 81 | return is_tbl(obj) and table_isarray(obj) 82 | end 83 | else 84 | table_isarray = function(obj) 85 | return is_tbl(obj) and table_type(obj) >= 0 86 | end 87 | is_array = table_isarray 88 | end 89 | 90 | ok, is_empty = pcall(require, "table.isempty") 91 | if not ok or type(is_empty) ~= "function" then 92 | is_empty = function(obj) return next(obj) ~= nil end 93 | end 94 | end 95 | 96 | 97 | local function is_object(obj) 98 | return is_tbl(obj) and (is_empty(obj) or not table_isarray(obj)) 99 | end 100 | 101 | 102 | local Var = {} 103 | 104 | Var.__index = Var 105 | Var.__call = function(self) return self._ref end 106 | Var.__tostring = function(self) return self._alias or self._ref end 107 | 108 | 109 | function Var.new(ref, alias, attributes) 110 | local var = { _ref = ref, _alias = alias } 111 | if attributes then 112 | for k, v in pairs(attributes) do 113 | var[k] = v 114 | end 115 | end 116 | return setmetatable(var, Var) 117 | end 118 | 119 | 120 | function Var.set_alias(self, alias) 121 | self._alias = alias 122 | end 123 | 124 | 125 | function Var.len(self) 126 | return self.length or "#" .. self() 127 | end 128 | 129 | 130 | local Dump = {} 131 | 132 | 133 | function Dump.value(v) 134 | if is_str(v) then 135 | return string_format("%q", v) 136 | elseif is_null(v) then 137 | return "base.null" 138 | elseif is_tbl(v) then 139 | return Dump.tostring(v) 140 | end 141 | 142 | return tostring(v) 143 | end 144 | 145 | 146 | function Dump.key(k) 147 | if is_str(k) and string_match(k, "^[_%a][_%a%d]*$") then 148 | return k 149 | end 150 | 151 | return "[" .. Dump.value(k) .. "]" 152 | end 153 | 154 | 155 | -- http://lua-users.org/wiki/TableUtils 156 | function Dump.tostring(obj) 157 | if not is_tbl(obj) then 158 | return Dump.value(obj) 159 | end 160 | 161 | local result, done = {}, {} 162 | for k, v in ipairs(obj) do 163 | result[k] = Dump.value(v) 164 | done[k] = true 165 | end 166 | 167 | for k, v in pairs(obj) do 168 | if not done[k] then 169 | result[#result + 1] = Dump.key(k) .. "=" .. Dump.value(v) 170 | end 171 | end 172 | 173 | return "{" .. concat(result, ", ") .. "}" 174 | end 175 | 176 | 177 | local dump = Dump.tostring 178 | 179 | 180 | local function table_len(t) 181 | local count = 0 182 | for _ in pairs(t) do 183 | count = count + 1 184 | end 185 | return count 186 | end 187 | 188 | 189 | local function equal(x, y) 190 | if type(x) ~= type(y) then 191 | return false 192 | end 193 | 194 | if is_tbl(x) then 195 | local keys = {} 196 | for k, v in pairs(x) do 197 | if y[k] == nil or not equal(v, y[k]) then 198 | return false 199 | end 200 | keys[k] = true 201 | end 202 | for k in pairs(y) do 203 | if not keys[k] then 204 | return false 205 | end 206 | end 207 | return true 208 | end 209 | 210 | return x == y 211 | end 212 | 213 | 214 | local type_order = { 215 | number = 1, 216 | boolean = 2, 217 | string = 3, 218 | table = 4, 219 | userdata = 5, 220 | } 221 | 222 | 223 | local function serialize(obj) 224 | return type_order[type(obj)] .. ":" .. dump(obj) 225 | end 226 | 227 | 228 | local _M = { 229 | _VERSION = "0.01", 230 | } 231 | local _mt = { __index = _M } 232 | 233 | local mapping = { 234 | { "type", "type" }, 235 | { "enum", "enum" }, 236 | { "allOf", "all_of" }, 237 | { "anyOf", "any_of" }, 238 | { "oneOf", "one_of" }, 239 | { "not", "not" }, 240 | { 241 | { 242 | { "length", "string_length" }, 243 | { "minLength", "min_length" }, 244 | { "maxLength", "max_length" }, 245 | { "pattern", "pattern" }, 246 | }, 247 | "string" 248 | }, 249 | { 250 | { 251 | { "minimum", "minimum" }, -- gt, ge, lt, le 252 | { "maximum", "maximum" }, 253 | { "multipleOf", "multiple_of" }, 254 | }, 255 | "number" 256 | }, 257 | { 258 | { 259 | { "items", "items" }, 260 | { "fixedItems", "fixed_items" }, 261 | { "minItems", "min_items" }, 262 | { "maxItems", "max_items" }, 263 | { "uniqueItems", "unique_items" }, 264 | }, 265 | "array" 266 | }, 267 | { 268 | { 269 | { "properties", "properties" }, 270 | { "required", "required" }, 271 | { "minProperties", "min_properties" }, 272 | { "maxProperties", "max_properties" }, 273 | { "dependencies", "dependencies" }, 274 | }, 275 | "object" 276 | }, 277 | { "validator", "validator" }, 278 | } 279 | 280 | local Base = { 281 | is_nil = is_nil, 282 | is_null = is_null, 283 | is_string = is_str, 284 | is_number = is_num, 285 | is_integer = is_int, 286 | is_boolean = is_bool, 287 | is_table = is_tbl, 288 | is_empty = is_empty, 289 | is_array = is_array, 290 | is_object = is_object, 291 | equal = equal, 292 | serialize = serialize, 293 | re_find = re_find, 294 | table_len = table_len, 295 | null = cjson.null, 296 | 297 | table_new = table_new, 298 | table_nkeys = table_nkeys, 299 | } 300 | 301 | _M.Base = Base 302 | 303 | 304 | local function _stmts(...) return concat({ ... }, "; ") end 305 | local function _and(...) return concat({ ... }, " and ") end 306 | local function _assign(lhs, rhs) return lhs .. " = " .. rhs end 307 | local function _for(expr, ...) return "for " .. concat({ ... }, ", ") .. " in " .. expr .. " do" end 308 | local function _for_assign(var, ...) return "for " .. var .. " = " .. concat({ ... }, ", ") .. " do" end 309 | local function _call(func, ...) return func .. "(" .. concat({ ... }, ", ") .. ")" end 310 | local function _list(l) return "{" .. concat(l, ", ") .. "}" end 311 | local function _if(expr) return "if " .. expr .. " then" end 312 | local function _not(expr) return "not " .. expr end 313 | local function _op(lhs, op, rhs) return lhs .. " " .. op .. " " .. rhs end 314 | local function _len(obj) return "#" .. obj end 315 | local function _index(obj, k) return obj .. "[" .. k .. "]" end 316 | 317 | 318 | function _M.new(schema, lib, name) 319 | name = name or "data" 320 | local m = { 321 | _indent = 0, 322 | _code = {}, 323 | _lib = lib, 324 | _name = name, 325 | _var = Var.new("data", name), 326 | _nvar = 0, 327 | _vars = {}, 328 | _pool = {}, 329 | _schema = schema, 330 | } 331 | 332 | setmetatable(m, _mt) 333 | m:generate_function() 334 | 335 | return m 336 | end 337 | 338 | 339 | function _M.code(self) 340 | if self._program then 341 | return self._program 342 | end 343 | 344 | if self._nvar > 0 then 345 | local vars = {} 346 | for i = 1, self._nvar do 347 | vars[i] = "_" .. i 348 | end 349 | self._code[3] = self._code[3] .. "local " .. concat(vars, ", ") 350 | else 351 | table_remove(self._code, 3) 352 | end 353 | 354 | self._program = concat(self._code, "\n") 355 | 356 | return self._program 357 | end 358 | 359 | 360 | function _M.k(self) 361 | return "k" .. self._indent 362 | end 363 | 364 | 365 | function _M.compile(self) 366 | local code = self:code() 367 | local chunk, err = loadstring(code) 368 | if not chunk then 369 | ngx.log(ngx.ERR, "compile error: ", err) 370 | end 371 | 372 | local func = chunk() 373 | return function(data, ctx) return func(data, Base, self._lib, ctx) end 374 | end 375 | 376 | 377 | function _M.emit(self, l) 378 | self._code[#self._code + 1] = string_rep(" ", self._indent) .. l 379 | end 380 | 381 | 382 | function _M.generate_function(self) 383 | self:generate_code_block( 384 | "return function(data, base, lib, ctx)", 385 | function() 386 | self:generate_code_block( 387 | "local validate = function(data, base, lib, ctx)", 388 | function() 389 | self:emit("") -- place holder for vars 390 | self:generate() 391 | self:emit("return true") 392 | end 393 | ) 394 | self:emit("return validate(data, base, lib, ctx)") 395 | end 396 | ) 397 | end 398 | 399 | 400 | function _M.generate_code_block(self, head, generator, done) 401 | local indent = self._indent 402 | 403 | self:emit(head) 404 | self._indent = indent + 1 405 | 406 | generator() 407 | 408 | local pool = self._pool[self._indent] 409 | if pool then 410 | for idx, alive in pairs(pool) do 411 | if alive then 412 | self:do_free_variable(idx) 413 | end 414 | end 415 | end 416 | 417 | self._indent = indent 418 | 419 | if done == false then 420 | return 421 | end 422 | 423 | self:emit(done or "end") 424 | end 425 | 426 | 427 | function _M.generate_error(self, ...) 428 | local err = concat({ ... }, " ") 429 | self:emit(string_format("return nil, %q", err)) 430 | end 431 | 432 | 433 | function _M.ensure_type(self, typ) 434 | local is = "is_" .. typ 435 | if self._var[is] then 436 | return 437 | end 438 | 439 | self._var[is] = true 440 | self:generate_code_block( 441 | _if(_not(_call("base." .. is, self._var()))), 442 | function() 443 | self:generate_error(tostring(self._var), "must be", typ) 444 | end 445 | ) 446 | end 447 | 448 | 449 | function _M.ignore_except_type(self, typ) 450 | local ignore = "ignore_except_" .. typ 451 | if self._var[ignore] then 452 | return 453 | end 454 | 455 | self._var[ignore] = true 456 | 457 | if self._schema.type ~= typ then 458 | self:generate_code_block( 459 | _if(_not(_call("base.is_" .. typ, self._var()))), 460 | function() 461 | self:emit("return true") 462 | end 463 | ) 464 | end 465 | end 466 | 467 | 468 | function _M.get_variable(self, alias) 469 | local idx = self._vars[0] 470 | if idx then 471 | self._vars[0] = self._vars[idx] 472 | else 473 | idx = #self._vars + 1 474 | self._nvar = idx 475 | end 476 | 477 | self._vars[idx] = true 478 | 479 | local pool = self._pool[self._indent] 480 | if not pool then 481 | pool = {} 482 | self._pool[self._indent] = pool 483 | end 484 | 485 | pool[idx] = true 486 | 487 | return Var.new("_" .. idx, alias, { idx = idx }) 488 | end 489 | 490 | 491 | function _M.do_free_variable(self, idx) 492 | self._vars[idx] = self._vars[0] 493 | self._vars[0] = idx 494 | 495 | local pool = self._pool[self._indent] 496 | if pool then 497 | pool[idx] = false 498 | end 499 | end 500 | 501 | 502 | function _M.free_variable(self, var) 503 | self:do_free_variable(var.idx) 504 | end 505 | 506 | 507 | function _M.set_variable_properties(self, var) 508 | var = var or self._var 509 | if not var.properties then 510 | local properties = self:get_variable() 511 | self:emit(_assign(properties(), _call("base.table_len", var()))) 512 | var.properties = properties() 513 | end 514 | end 515 | 516 | 517 | function _M.set_variable_length(self, var) 518 | var = var or self._var 519 | if not var.length then 520 | local len = self:get_variable() 521 | self:emit(_assign(len(), _len(var()))) 522 | var.length = len() 523 | end 524 | end 525 | 526 | 527 | function _M.generate_specific(self, entry, typ) 528 | local has = {} 529 | local schema = self._schema 530 | for _, k in ipairs(entry) do 531 | if schema[k[1]] then 532 | has[#has + 1] = k 533 | elseif ( 534 | k[1] == "properties" and ( 535 | schema.additionalProperties ~= nil or 536 | schema.patternProperties) 537 | ) or ( 538 | k[1] == "items" and ( 539 | schema.additionalItems ~= nil) 540 | ) then 541 | has[#has + 1] = k 542 | end 543 | end 544 | 545 | if #has == 0 then 546 | return 547 | end 548 | 549 | local function generate() 550 | for _, k in ipairs(has) do 551 | self["_generate_" .. k[2]](self, self._schema[k[1]]) 552 | end 553 | end 554 | 555 | local ensure = self._var["is_" .. typ] 556 | if ensure then 557 | return generate() 558 | end 559 | 560 | self:generate_code_block( 561 | _if(_call("base.is_" .. typ, self._var())), 562 | function() 563 | generate() 564 | end 565 | ) 566 | end 567 | 568 | 569 | function _M.generate(self, var, schema) 570 | local _schema, _var = self._schema, self._var 571 | 572 | if var then self._var = var end 573 | if schema ~= nil then self._schema = schema end 574 | 575 | assert(is_tbl(self._schema), string_format( 576 | "schema must be table, but got: %s, when: %s", 577 | tostring(self._schema), tostring(self._var))) 578 | 579 | for _, v in ipairs(mapping) do 580 | if is_tbl(v[1]) then 581 | self:generate_specific(v[1], v[2]) 582 | elseif self._schema[v[1]] then 583 | self["_generate_" .. v[2]](self, self._schema[v[1]]) 584 | end 585 | end 586 | 587 | self._schema, self._var = _schema, _var 588 | end 589 | 590 | 591 | function _M._generate_not(self, schema) 592 | local ok = self:get_variable() 593 | local sub = self:get_variable() 594 | 595 | self:generate_code_block( 596 | _assign(sub(), "function()"), 597 | function() 598 | self:generate(nil, schema) 599 | self:emit("return true") 600 | end 601 | ) 602 | 603 | self:emit(_assign(ok(), _call(sub()))) 604 | self:generate_code_block( 605 | _if(ok()), 606 | function() 607 | self:generate_error(tostring(self._var), 608 | "must not be valid by not schema") 609 | end 610 | ) 611 | 612 | self:free_variable(ok) 613 | self:free_variable(sub) 614 | end 615 | 616 | 617 | function _M._generate_type(self, types) 618 | types = is_tbl(types) and types or { types } 619 | 620 | local validators = {} 621 | for _, typ in ipairs(types) do 622 | validators[#validators + 1] = "base.is_" .. typ 623 | end 624 | 625 | if #validators == 1 then 626 | self:ensure_type(types[1]) 627 | return 628 | end 629 | 630 | local ok = self:get_variable() 631 | self:emit(_assign(ok(), "false")) 632 | self:generate_code_block( 633 | _for(_call("ipairs", _list(validators)), "_", "v"), 634 | function() 635 | self:generate_code_block( 636 | _if(_call("v", self._var())), 637 | function() 638 | self:emit(_stmts(_assign(ok(), "true"), "break")) 639 | end 640 | ) 641 | end 642 | ) 643 | 644 | self:generate_code_block( 645 | _if(_not(ok())), 646 | function() 647 | self:generate_error(tostring(self._var), "must be", 648 | concat(types, " or ")) 649 | end 650 | ) 651 | 652 | self:free_variable(ok) 653 | end 654 | 655 | 656 | function _M._generate_enum(self, enum) 657 | local ok = self:get_variable() 658 | 659 | enum = dump(enum) 660 | self:emit(_assign(ok(), "false")) 661 | 662 | self:generate_code_block( 663 | _for(_call("ipairs", enum), "_", "elem"), 664 | function() 665 | self:generate_code_block( 666 | _if(_call("base.equal", self._var(), "elem")), 667 | function() 668 | self:emit(_stmts(_assign(ok(), "true"), "break")) 669 | end) 670 | end 671 | ) 672 | 673 | self:generate_code_block( 674 | _if(_not(ok())), 675 | function() 676 | self:generate_error(tostring(self._var), "must be one of", enum) 677 | end 678 | ) 679 | 680 | self:free_variable(ok) 681 | end 682 | 683 | 684 | function _M._generate_string_length(self, n) 685 | self:generate_code_block( 686 | _if(_op(self._var:len(), "~=", dump(n))), 687 | function() 688 | self:generate_error(tostring(self._var), 689 | "must be equal to", dump(n), "characters") 690 | end 691 | ) 692 | end 693 | 694 | 695 | function _M._generate_min_length(self, n) 696 | self:generate_code_block( 697 | _if(_op(self._var:len(), "<", dump(n))), 698 | function() 699 | self:generate_error(tostring(self._var), 700 | "must be longer than or equal to", dump(n), 701 | "characters") 702 | end 703 | ) 704 | end 705 | 706 | 707 | function _M._generate_max_length(self, n) 708 | self:generate_code_block( 709 | _if(_op(self._var:len(), ">", dump(n))), 710 | function() 711 | self:generate_error(tostring(self._var), 712 | "must be shorter than or equal to", dump(n), 713 | "characters") 714 | end 715 | ) 716 | end 717 | 718 | 719 | function _M._generate_fixed_items(self, n) 720 | self:generate_code_block( 721 | _if(_op(self._var:len(), "~=", dump(n))), 722 | function() 723 | self:generate_error(tostring(self._var), 724 | "must contain", dump(n), "items") 725 | end 726 | ) 727 | end 728 | 729 | 730 | function _M._generate_min_items(self, n) 731 | self:generate_code_block( 732 | _if(_op(self._var:len(), "<", dump(n))), 733 | function() 734 | self:generate_error(tostring(self._var), 735 | "must contain at least", dump(n), "items") 736 | end 737 | ) 738 | end 739 | 740 | 741 | function _M._generate_max_items(self, n) 742 | self:generate_code_block( 743 | _if(_op(self._var:len(), ">", dump(n))), 744 | function() 745 | self:generate_error(tostring(self._var), 746 | "must contain less than or equal to", dump(n), 747 | "items") 748 | end 749 | ) 750 | end 751 | 752 | 753 | function _M._generate_unique_items(self, unique) 754 | if unique == false then 755 | return 756 | end 757 | 758 | local k = self:k() 759 | local mem = self:get_variable() 760 | 761 | self:emit(_assign(mem(), _call("base.table_new", "0", self._var:len()))) 762 | self:generate_code_block( 763 | _for(_call("ipairs", self._var()), "_", k), 764 | function() 765 | self:emit(_assign("local id", _call("base.serialize", k))) 766 | self:generate_code_block( 767 | _if(_index(mem(), "id")), 768 | function() 769 | self:generate_error(tostring(self._var), 770 | "must contain unique items") 771 | end, 772 | false 773 | ) 774 | self:generate_code_block( 775 | "else", 776 | function() 777 | self:emit(_assign(_index(mem(), "id"), "true")) 778 | end 779 | ) 780 | end 781 | ) 782 | end 783 | 784 | 785 | function _M._generate_required(self, required) 786 | local k = self:k() 787 | 788 | required = dump(required) 789 | self:generate_code_block( 790 | _for(_call("ipairs", required), "_", k), 791 | function() 792 | self:generate_code_block( 793 | _if(_op(_index(self._var(), k), "==", "nil")), 794 | function() 795 | self:generate_error(tostring(self._var), "must contain", 796 | required, "properties") 797 | end 798 | ) 799 | end 800 | ) 801 | end 802 | 803 | 804 | function _M._generate_dependencies(self, dependencies) 805 | local function generate_dependency(prop, dep) 806 | dep = dump(dep) 807 | self:generate_code_block( 808 | _if(_op(_index(self._var(), dep), "==", "nil")), 809 | function() 810 | self:generate_error( 811 | prop, "depends on", _index(tostring(self._var), dep)) 812 | end 813 | ) 814 | end 815 | 816 | local function generate_list_dependencies(prop, dep) 817 | if #dep == 1 then 818 | return generate_dependency(prop, dep[1]) 819 | end 820 | 821 | local k = self:k() 822 | self:generate_code_block( 823 | _for(_call("ipairs", dump(dep)), "_", k), 824 | function() 825 | self:generate_code_block( 826 | _if(_op(_index(self._var(), k), "==", "nil")), 827 | function() 828 | self:generate_error( 829 | prop, "depends on", 830 | tostring(self._var) .. dump(dep)) 831 | end 832 | ) 833 | end 834 | ) 835 | end 836 | 837 | local function generate_schema_dependencies(prop, schema) 838 | for k, v in pairs(schema) do 839 | local var = self:get_variable(_index(tostring(self._var), dump(k))) 840 | self:emit(_assign(var(), _index(self._var(), dump(k)))) 841 | self:generate_code_block( 842 | _if(_op(var(), "==", "nil")), 843 | function() 844 | self:generate_error( 845 | prop, "depends on", tostring(var)) 846 | end 847 | ) 848 | self:generate(var, v) 849 | self:free_variable(var) 850 | end 851 | end 852 | 853 | for k, v in pairs(dependencies) do 854 | local prop = _index(tostring(self._var), dump(k)) 855 | self:generate_code_block( 856 | _if(_op(_index(self._var(), dump(k)), "~=", "nil")), 857 | function() 858 | if is_array(v) then 859 | generate_list_dependencies(prop, v) 860 | elseif is_tbl(v) and v.properties then 861 | generate_schema_dependencies(prop, v.properties) 862 | else 863 | generate_dependency(prop, v) 864 | end 865 | end 866 | ) 867 | end 868 | end 869 | 870 | 871 | function _M._generate_minimum(self, min) 872 | local op, hint 873 | if self._schema.exclusiveMinimum then 874 | op, hint = "<=", "bigger than" 875 | else 876 | op, hint = "<", "bigger than or equal to" 877 | end 878 | 879 | self:generate_code_block( 880 | _if(_op(self._var(), op, dump(min))), 881 | function() 882 | self:generate_error(tostring(self._var), 883 | "must be", hint, dump(min)) 884 | end 885 | ) 886 | end 887 | 888 | 889 | function _M._generate_maximum(self, min) 890 | local op, hint 891 | if self._schema.exclusiveMaximum then 892 | op, hint = ">=", "smaller than" 893 | else 894 | op, hint = ">", "smaller than or equal to" 895 | end 896 | 897 | self:generate_code_block( 898 | _if(_op(self._var(), op, dump(min))), 899 | function() 900 | self:generate_error(tostring(self._var), 901 | "must be", hint, dump(min)) 902 | end 903 | ) 904 | end 905 | 906 | 907 | function _M._generate_pattern(self, pattern) 908 | local opt = self._schema.ignoreCase and "ijo" or "jo" 909 | pattern = dump(pattern) 910 | self:generate_code_block( 911 | _if(_not(_call("base.re_find", self._var(), pattern, dump(opt)))), 912 | function() 913 | self:generate_error(tostring(self._var), 914 | "must match pattern", pattern) 915 | end 916 | ) 917 | end 918 | 919 | 920 | function _M._generate_multiple_of(self, n) 921 | self:generate_code_block( 922 | _if(_not(_call("base.is_integer", _op(self._var(), "/", dump(n))))), 923 | function() 924 | self:generate_error(tostring(self._var), 925 | "must be multiple of", dump(n)) 926 | end 927 | ) 928 | end 929 | 930 | 931 | function _M._generate_items(self, items) 932 | local parent = self._schema 933 | local is_root = tostring(self._var) == self._name 934 | local root 935 | 936 | if is_root then 937 | root = self._var 938 | else 939 | root = self:get_variable(tostring(self._var)) 940 | self:emit(_assign(root(), self._var())) 941 | end 942 | 943 | self:set_variable_length(root) 944 | 945 | local function generate(i, schema) 946 | local var = Var.new(_index(root(), i), _index(tostring(root), i)) 947 | local default = schema.default 948 | 949 | schema.default = nil 950 | 951 | local function set_default() 952 | self:emit(_assign(var(), dump(default))) 953 | end 954 | 955 | if next(schema) then 956 | self:generate_code_block( 957 | _if(_op(root:len(), ">", dump(i - 1))), 958 | function() 959 | self:generate(var, schema) 960 | end, 961 | false 962 | ) 963 | if default ~= nil then 964 | self:generate_code_block( 965 | "else", 966 | set_default, 967 | false 968 | ) 969 | end 970 | self:emit("end") 971 | elseif default ~= nil then 972 | self:generate_code_block( 973 | _if(_op(root:len(), "<=", dump(i - 1))), 974 | set_default 975 | ) 976 | end 977 | 978 | schema.default = default 979 | end 980 | 981 | if is_array(items) and #items > 0 then 982 | for i, schema in ipairs(items) do 983 | generate(i, schema) 984 | end 985 | elseif is_tbl(items) and next(items) then 986 | self:generate_code_block( 987 | _for(_call("ipairs", root()), "_", "elem"), 988 | function() 989 | local var = Var.new("elem", _index(tostring(root), "?")) 990 | self:generate(var, items) 991 | end 992 | ) 993 | else 994 | items = {} 995 | end 996 | 997 | if is_array(items) and #items > 0 and parent.additionalItems == false then 998 | self:generate_code_block( 999 | _if(_op(root:len(), ">", dump(#items))), 1000 | function() 1001 | self:generate_error( 1002 | tostring(self._var), 1003 | "must contain only specified items") 1004 | end 1005 | ) 1006 | elseif is_tbl(parent.additionalItems) and next(parent.additionalItems) then 1007 | self:generate_code_block( 1008 | _for_assign("i", #items + 1, root:len(), 1), 1009 | function() 1010 | local var = Var.new( 1011 | _index(root(), "i"), _index(tostring(root), "?")) 1012 | self:generate(var, parent.additionalItems) 1013 | end 1014 | ) 1015 | end 1016 | 1017 | if not is_root then 1018 | self:free_variable(root) 1019 | end 1020 | end 1021 | 1022 | 1023 | function _M._generate_any_of(self, schemas) 1024 | local ok = self:get_variable() 1025 | self:generate_code_block( 1026 | _assign(ok(), "function()"), 1027 | function() 1028 | for _, schema in ipairs(schemas) do 1029 | local pass = self:get_variable() 1030 | self:generate_code_block( 1031 | _assign(pass(), "function()"), 1032 | function() 1033 | self:generate(nil, schema) 1034 | self:emit("return true") 1035 | end 1036 | ) 1037 | self:generate_code_block( 1038 | _if(_call(pass())), 1039 | function() 1040 | self:emit("return true") 1041 | end 1042 | ) 1043 | self:free_variable(pass) 1044 | end 1045 | end 1046 | ) 1047 | self:generate_code_block( 1048 | _if(_not(_call(ok()))), 1049 | function() 1050 | self:generate_error(tostring(self._var), 1051 | "must be valid by one of anyOf definition") 1052 | end 1053 | ) 1054 | self:free_variable(ok) 1055 | end 1056 | 1057 | 1058 | function _M._generate_all_of(self, schemas) 1059 | for _, schema in ipairs(schemas) do 1060 | self:generate(nil, schema) 1061 | end 1062 | end 1063 | 1064 | 1065 | function _M._generate_one_of(self, schemas) 1066 | local blank = self:get_variable() 1067 | local count = self:get_variable() 1068 | 1069 | self:emit(_assign(count(), "0")) 1070 | self:generate_code_block( 1071 | _assign(blank(), "(function()"), 1072 | function() 1073 | self:emit("local _") 1074 | for _, schema in ipairs(schemas) do 1075 | self:generate_code_block( 1076 | _assign("_", "(function()"), 1077 | function() 1078 | self:generate(nil, schema) 1079 | self:emit(_assign(count(), _op(count(), "+", "1"))) 1080 | end, 1081 | "end)()" 1082 | ) 1083 | self:generate_code_block( 1084 | _if(_op(count(), ">", "1")), 1085 | function() 1086 | self:emit("return") 1087 | end 1088 | ) 1089 | end 1090 | end, 1091 | false 1092 | ) 1093 | self:emit("end)()") 1094 | self:generate_code_block( 1095 | _if(_op(count(), "~=", "1")), 1096 | function() 1097 | self:generate_error(tostring(self._var), 1098 | "must be valid by one of oneOf definition") 1099 | end 1100 | ) 1101 | 1102 | self:free_variable(blank) 1103 | self:free_variable(count) 1104 | end 1105 | 1106 | 1107 | function _M._generate_min_properties(self, properties) 1108 | self:set_variable_properties() 1109 | self:generate_code_block( 1110 | _if(_op(self._var.properties, "<", dump(properties))), 1111 | function() 1112 | self:generate_error(tostring(self._var), 1113 | "must contain at least", properties, "properties") 1114 | end 1115 | ) 1116 | end 1117 | 1118 | 1119 | function _M._generate_max_properties(self, properties) 1120 | self:set_variable_properties() 1121 | self:generate_code_block( 1122 | _if(_op(self._var.properties, ">", dump(properties))), 1123 | function() 1124 | self:generate_error(tostring(self._var), 1125 | "must contain less than or equal to", properties, "properties") 1126 | end 1127 | ) 1128 | end 1129 | 1130 | 1131 | function _M._generate_properties(self) 1132 | local parent = self._schema 1133 | local k = self:k() 1134 | local keys = self:get_variable() 1135 | local is_root = tostring(self._var) == self._name 1136 | local root 1137 | 1138 | if is_root then 1139 | root = self._var 1140 | else 1141 | root = self:get_variable(tostring(self._var)) 1142 | self:emit(_assign(root(), self._var())) 1143 | end 1144 | 1145 | self:emit(_assign(keys(), _call("base.table_new", "0", 1146 | _call("base.table_nkeys", root())))) 1147 | self:generate_code_block( 1148 | _for(_call("pairs", root()), k), 1149 | function() 1150 | self:emit(_assign(_index(keys(), k), "true")) 1151 | end 1152 | ) 1153 | 1154 | local function generate_properties(schema, var, key) 1155 | self:generate_code_block( 1156 | _if(_index(keys(), key)), 1157 | function() 1158 | self:emit(_assign(_index(keys(), key), "nil")) 1159 | self:generate(var, schema) 1160 | end, 1161 | false 1162 | ) 1163 | if schema.default ~= nil then 1164 | self:generate_code_block( 1165 | "else", 1166 | function() 1167 | self:emit(_assign(_index(self._var(), key), 1168 | dump(schema.default))) 1169 | end, 1170 | false 1171 | ) 1172 | end 1173 | self:emit("end") 1174 | end 1175 | 1176 | if parent.properties then 1177 | for key, schema in pairs(parent.properties) do 1178 | local var = Var.new( 1179 | _index(root(), dump(key)), _index(tostring(root), dump(key))) 1180 | generate_properties(schema, var, dump(key)) 1181 | end 1182 | end 1183 | 1184 | if parent.patternProperties then 1185 | for pattern, schema in pairs(parent.patternProperties) do 1186 | self:generate_code_block( 1187 | _for(_call("pairs", root()), k), 1188 | function() 1189 | self:generate_code_block( 1190 | _if(_and(_call("base.is_string", k), 1191 | _call("base.re_find", 1192 | k, dump(pattern), dump("jo")))), 1193 | function() 1194 | local var = Var.new( 1195 | _index(root(), k), 1196 | _index(tostring(root), dump(pattern))) 1197 | self:emit(_assign(_index(keys(), k), "nil")) 1198 | self:generate(var, schema) 1199 | end 1200 | ) 1201 | end 1202 | ) 1203 | end 1204 | end 1205 | 1206 | if parent.additionalProperties == false then 1207 | self:generate_code_block( 1208 | _if(_not(_call("base.is_empty", keys()))), 1209 | function() 1210 | self:generate_error(tostring(self._var), 1211 | "must contain only specified properties") 1212 | end 1213 | ) 1214 | elseif is_tbl(parent.additionalProperties) then 1215 | local var = self:get_variable(_index(tostring(root), "?")) 1216 | self:generate_code_block( 1217 | _for(_call("pairs", keys()), k), 1218 | function() 1219 | self:emit(_assign(var(), _index(root(), k))) 1220 | self:generate(var, parent.additionalProperties) 1221 | end 1222 | ) 1223 | self:free_variable(var) 1224 | end 1225 | 1226 | if not is_root then 1227 | self:free_variable(root) 1228 | end 1229 | 1230 | self:free_variable(keys) 1231 | end 1232 | 1233 | 1234 | function _M._generate_validator(self, validator) 1235 | self:generate_code_block( 1236 | _if(_not(_call("lib." .. validator, self._var(), "ctx"))), 1237 | function() 1238 | self:generate_error( 1239 | tostring(self._var), "must be valid by", validator) 1240 | end 1241 | ) 1242 | end 1243 | 1244 | 1245 | return _M 1246 | -------------------------------------------------------------------------------- /spec/extra/README.md: -------------------------------------------------------------------------------- 1 | https://github.com/jdesgats/ljsonschema/tree/master/spec/extra 2 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /t/test.lua: -------------------------------------------------------------------------------- 1 | -- https://github.com/jdesgats/ljsonschema/blob/master/bench.lua 2 | 3 | local cjson = require "cjson.safe" 4 | local clock = require("os").clock 5 | local jsonschema = require "lib/resty/jsonschema/compiler" 6 | 7 | 8 | local supported = { 9 | "spec/extra/sanity.json", 10 | "spec/extra/empty.json", 11 | "spec/extra/table.json", 12 | "spec/extra/dependencies.json", 13 | 14 | "spec/JSON-Schema-Test-Suite/tests/draft4/type.json", 15 | "spec/JSON-Schema-Test-Suite/tests/draft4/default.json", 16 | 17 | -- object 18 | "spec/JSON-Schema-Test-Suite/tests/draft4/properties.json", 19 | "spec/JSON-Schema-Test-Suite/tests/draft4/required.json", 20 | "spec/JSON-Schema-Test-Suite/tests/draft4/additionalProperties.json", 21 | "spec/JSON-Schema-Test-Suite/tests/draft4/patternProperties.json", 22 | "spec/JSON-Schema-Test-Suite/tests/draft4/minProperties.json", 23 | "spec/JSON-Schema-Test-Suite/tests/draft4/maxProperties.json", 24 | "spec/JSON-Schema-Test-Suite/tests/draft4/dependencies.json", 25 | 26 | -- string 27 | "spec/JSON-Schema-Test-Suite/tests/draft4/minLength.json", 28 | "spec/JSON-Schema-Test-Suite/tests/draft4/maxLength.json", 29 | "spec/JSON-Schema-Test-Suite/tests/draft4/pattern.json", 30 | 31 | -- number 32 | "spec/JSON-Schema-Test-Suite/tests/draft4/multipleOf.json", 33 | "spec/JSON-Schema-Test-Suite/tests/draft4/minimum.json", 34 | "spec/JSON-Schema-Test-Suite/tests/draft4/maximum.json", 35 | 36 | -- array 37 | "spec/JSON-Schema-Test-Suite/tests/draft4/items.json", 38 | "spec/JSON-Schema-Test-Suite/tests/draft4/additionalItems.json", 39 | "spec/JSON-Schema-Test-Suite/tests/draft4/minItems.json", 40 | "spec/JSON-Schema-Test-Suite/tests/draft4/maxItems.json", 41 | "spec/JSON-Schema-Test-Suite/tests/draft4/uniqueItems.json", 42 | 43 | -- enum 44 | "spec/JSON-Schema-Test-Suite/tests/draft4/enum.json", 45 | 46 | -- misc 47 | "spec/JSON-Schema-Test-Suite/tests/draft4/not.json", 48 | "spec/JSON-Schema-Test-Suite/tests/draft4/allOf.json", 49 | "spec/JSON-Schema-Test-Suite/tests/draft4/anyOf.json", 50 | "spec/JSON-Schema-Test-Suite/tests/draft4/oneOf.json", 51 | } 52 | 53 | local blacklist = { 54 | -- edge cases, not supported features 55 | ["object type matches objects"] = { 56 | ["an array is not an object"] = true, -- empty object/array confusion 57 | }, 58 | ["array type matches arrays"] = { 59 | ["an object is not an array"] = true, -- empty object/array confusion 60 | }, 61 | ["required validation"] = { 62 | ["ignores arrays"] = true, 63 | }, 64 | ["minProperties validation"] = { 65 | ["ignores arrays"] = true, 66 | }, 67 | ["maxProperties validation"] = { 68 | ["ignores arrays"] = true, 69 | }, 70 | ["minLength validation"] = { 71 | ["one supplementary Unicode code point is not long enough"] = true, -- unicode handling 72 | }, 73 | ["maxLength validation"] = { 74 | ["two supplementary Unicode code points is long enough"] = true, -- unicode handling 75 | }, 76 | ["a schema given for items"] = { 77 | ["JavaScript pseudo-array is valid"] = true, -- pseudo array 78 | }, 79 | ["an array of schemas for items"] = { 80 | ["JavaScript pseudo-array is valid"] = true, -- pseudo array 81 | }, 82 | } 83 | 84 | 85 | local function bench(func, msg, iters, count) 86 | count = count or 1 87 | iters = iters or 1000 88 | local start = clock() 89 | local n = 0 90 | while n < iters do 91 | func() 92 | n = n + 1 93 | end 94 | local elapsed = clock() - start 95 | print(string.format("%s: elapsed time: %.6f s, per %.6f us", 96 | msg, elapsed, 1000000 * elapsed / iters / count)) 97 | end 98 | 99 | 100 | local function decode_descriptor(path) 101 | local f = assert(io.open(path)) 102 | local testsuite = cjson.decode(assert(f:read("*a"))) 103 | f:close() 104 | return ipairs(testsuite) 105 | end 106 | 107 | 108 | local function load_test() 109 | local cases, n = {}, 0 110 | for _, descriptor in ipairs(supported) do 111 | for _, suite in decode_descriptor(descriptor) do 112 | local skipped = blacklist[suite.description] or {} 113 | if skipped == true then 114 | print("skip suite: " .. suite.description) 115 | else 116 | print("add suite: " .. suite.description) 117 | local schema = cjson.encode(suite.schema) 118 | local ok, js = pcall(jsonschema.new, suite.schema) 119 | if not ok then 120 | print("error: " .. js .. ", " .. 121 | cjson.encode(suite.schema)) 122 | return 123 | end 124 | for _, case in ipairs(suite.tests) do 125 | if skipped[case.description] then 126 | print("skip suite case: " .. case.description) 127 | else 128 | n = n + 1 129 | local ok, jv = pcall(js.compile, js) 130 | if not ok then 131 | print(js:code()) 132 | end 133 | cases[n] = { jv, schema, case, js:code() } 134 | end 135 | end 136 | end 137 | end 138 | end 139 | return cases, n 140 | end 141 | 142 | 143 | local cases, ncases = load_test() 144 | local function test() 145 | for _, case in ipairs(cases) do 146 | local ok, pass, err = pcall(case[1], case[3].data) 147 | local valid = pass or false 148 | local expect = case[3].valid 149 | if valid ~= expect then 150 | print(string.format("expect %s, but got %s", expect, valid)) 151 | print(case[2]) 152 | print(cjson.encode(case[3])) 153 | print(case[4]) 154 | if err then 155 | error(err) 156 | end 157 | return 158 | end 159 | end 160 | end 161 | 162 | 163 | bench(load_test, "load test cases", 1) 164 | 165 | print("total test cases: " .. ncases) 166 | bench(test, "run test cases", tonumber(arg[1]) or 1, ncases) 167 | 168 | 169 | local schema = { 170 | properties = { 171 | num = { 172 | type = "number", 173 | validator = "positive" 174 | }, 175 | x = { 176 | default = 2, 177 | } 178 | } 179 | } 180 | 181 | local js = jsonschema.new(schema, { positive = function(n) return n > 0 end }, "test") 182 | local jv = js:compile() 183 | local obj = { num = 1 } 184 | local ok, err = jv(obj) 185 | assert(ok, err) 186 | assert(obj.x == 2, obj.x) 187 | 188 | obj.num = -1 189 | obj.x = 3 190 | ok, err = jv(obj) 191 | assert(not ok, err) 192 | assert(obj.x == 3) 193 | 194 | schema = { 195 | items = { 196 | { type = "number" }, 197 | { default = 2 }, 198 | { enum = { cjson.null } }, 199 | }, 200 | } 201 | 202 | obj = { 1 } 203 | js = jsonschema.new(schema) 204 | jv = js:compile() 205 | 206 | ok, err = jv(obj) 207 | assert(ok) 208 | assert(obj[2] == 2) 209 | 210 | obj = { 1, 3, cjson.null } 211 | ok, err = jv(obj) 212 | assert(ok) 213 | assert(obj[2] == 3) 214 | 215 | schema = { 216 | type = "object", 217 | properties = { 218 | foo = { 219 | type = "object", 220 | patternProperties = { 221 | ["^bar"] = { 222 | type = "object", 223 | properties = { 224 | baz = { type = "string", }, 225 | bazz = { type = "string", length = 1024 }, 226 | }, 227 | required = { "baz", }, 228 | }, 229 | }, 230 | }, 231 | unique_array = { 232 | type = "array", 233 | fixedItems = 2, 234 | uniqueItems = true, 235 | }, 236 | object = { 237 | type = "object", 238 | properties = { 239 | foo = {}, 240 | bar = {}, 241 | }, 242 | additionalProperties = false 243 | }, 244 | object2 = { 245 | type = "object", 246 | properties = { 247 | foo = {}, 248 | bar = {}, 249 | }, 250 | additionalProperties = { 251 | type = "boolean" 252 | } 253 | } 254 | }, 255 | } 256 | 257 | obj = { 258 | foo = { 259 | bar1 = { 260 | baz = "hi", 261 | bazz = string.rep("x", 1024) 262 | } 263 | }, 264 | unique_array = { 1, 2 }, 265 | object = { foo = true, bar = {} }, 266 | object2 = { foo = true, bar = {}, baz = true }, 267 | } 268 | 269 | js = jsonschema.new(schema) 270 | 271 | -- print(js:code()) 272 | 273 | jv = js:compile() 274 | 275 | ok, err = jv(obj) 276 | assert(ok, err) 277 | 278 | obj.unique_array[3] = 3 279 | ok, err = jv(obj) 280 | assert(not ok) 281 | assert(string.find(err, "must contain 2 items")) 282 | -------------------------------------------------------------------------------- /util/lua-releng: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | sub file_contains ($$); 7 | 8 | for my $file (map glob, qw{ *.lua t/*.lua lib/resty/*/*.lua }) { 9 | print "Checking use of Lua global variables in file $file ...\n"; 10 | my $output = `luac5.1 -p -l $file | grep ETGLOBAL | grep -vE 'arg\$|require|type|tostring|loadstring|error|ngx\$|ndk|jit|setmetatable|getmetatable| (os|io|bit|math|string|table)\$|print|tonumber|math|pcall|xpcall|unpack|pairs|ipairs|assert|module|package|coroutine|[gs]etfenv|next|select|rawset|rawget|debug|collectgarbage'`; 11 | if ($output) { 12 | print $output; 13 | exit 1; 14 | } 15 | #file_contains($file, "attempt to write to undeclared variable"); 16 | system("grep -H -n -E --color --exclude errorpage.lua '.{120}' $file"); 17 | } 18 | 19 | sub file_contains ($$) { 20 | my ($file, $regex) = @_; 21 | open my $in, $file 22 | or die "Cannot open $file fo reading: $!\n"; 23 | my $content = do { local $/; <$in> }; 24 | close $in; 25 | #print "$content"; 26 | return scalar ($content =~ /$regex/); 27 | } 28 | 29 | if (-d 'test') { 30 | for my $file (map glob, qw{ test/*.t test/*/*.t test/*/*/*.t }) { 31 | system(qq{grep -H -n --color -E '\\--- ?(ONLY|LAST)' $file}); 32 | } 33 | } 34 | --------------------------------------------------------------------------------