184 lines
4.4 KiB
Lua
184 lines
4.4 KiB
Lua
---
|
|
-- JSON schema validation.
|
|
--
|
|
--
|
|
local _M = {}
|
|
|
|
local lrucache = require "resty.lrucache"
|
|
local jsonschema = require "resty.ljsonschema"
|
|
local metaschema = require "resty.ljsonschema.metaschema"
|
|
local cjson = require "cjson"
|
|
|
|
local sha256_hex = require("kong.tools.sha256").sha256_hex
|
|
local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy
|
|
|
|
local type = type
|
|
local cjson_encode = cjson.encode
|
|
|
|
|
|
---@class kong.db.schema.json.schema_doc : table
|
|
---
|
|
---@field id string|nil
|
|
---@field ["$id"] string|nil
|
|
---@field ["$schema"] string|nil
|
|
---@field type string
|
|
|
|
|
|
-- The correct identifier for draft-4 is 'http://json-schema.org/draft-04/schema#'
|
|
-- with the fragment (#) intact. Newer editions use an identifier _without_
|
|
-- the fragment (e.g. 'https://json-schema.org/draft/2020-12/schema'), so we
|
|
-- will be lenient when comparing these strings.
|
|
assert(type(metaschema.id) == "string",
|
|
"JSON metaschema .id not defined or not a string")
|
|
local DRAFT_4_NO_FRAGMENT = metaschema.id:gsub("#$", "")
|
|
local DRAFT_4 = DRAFT_4_NO_FRAGMENT .. "#"
|
|
|
|
_M.DRAFT_4 = DRAFT_4
|
|
|
|
|
|
---@type table<string, kong.db.schema.json.schema_doc>
|
|
local schemas = {}
|
|
|
|
|
|
-- Creating a json schema validator is somewhat expensive as it requires
|
|
-- generating and evaluating some Lua code, so we memoize this step with
|
|
-- a local LRU cache.
|
|
local cache = lrucache.new(1000)
|
|
|
|
local schema_cache_key
|
|
do
|
|
local cache_keys = setmetatable({}, { __mode = "k" })
|
|
|
|
---
|
|
-- Generate a unique cache key for a schema document.
|
|
--
|
|
---@param schema kong.db.schema.json.schema_doc
|
|
---@return string
|
|
function schema_cache_key(schema)
|
|
local cache_key = cache_keys[schema]
|
|
|
|
if not cache_key then
|
|
cache_key = "hash://" .. sha256_hex(cjson_encode(schema))
|
|
cache_keys[schema] = cache_key
|
|
end
|
|
|
|
return cache_key
|
|
end
|
|
end
|
|
|
|
|
|
---@param id any
|
|
---@return boolean
|
|
local function is_draft_4(id)
|
|
return id
|
|
and type(id) == "string"
|
|
and (id == DRAFT_4 or id == DRAFT_4_NO_FRAGMENT)
|
|
end
|
|
|
|
|
|
---@param id any
|
|
---@return boolean
|
|
local function is_non_draft_4(id)
|
|
return id
|
|
and type(id) == "string"
|
|
and (id ~= DRAFT_4 and id ~= DRAFT_4_NO_FRAGMENT)
|
|
end
|
|
|
|
|
|
---
|
|
-- Validate input according to a JSON schema document.
|
|
--
|
|
---@param input any
|
|
---@param schema kong.db.schema.json.schema_doc
|
|
---@return boolean? ok
|
|
---@return string? error
|
|
local function validate(input, schema)
|
|
assert(type(schema) == "table")
|
|
|
|
-- we are validating a JSON schema document and need to ensure that it is
|
|
-- not using supported JSON schema draft/version
|
|
if is_draft_4(schema.id or schema["$id"])
|
|
and is_non_draft_4(input["$schema"])
|
|
then
|
|
return nil, "unsupported document $schema: '" .. input["$schema"] ..
|
|
"', expected: " .. DRAFT_4
|
|
end
|
|
|
|
local cache_key = schema_cache_key(schema)
|
|
|
|
local validator = cache:get(cache_key)
|
|
|
|
if not validator then
|
|
validator = assert(jsonschema.generate_validator(schema, {
|
|
name = cache_key,
|
|
-- lua-resty-ljsonschema's default behavior for detecting an array type
|
|
-- is to compare its metatable against `cjson.array_mt`. This is
|
|
-- efficient, but we can't assume that all inputs will necessarily
|
|
-- conform to this, so we opt to use the heuristic approach instead
|
|
-- (determining object/array type based on the table contents).
|
|
array_mt = false,
|
|
}))
|
|
cache:set(cache_key, validator)
|
|
end
|
|
|
|
return validator(input)
|
|
end
|
|
|
|
|
|
---@type table
|
|
_M.metaschema = metaschema
|
|
|
|
|
|
_M.validate = validate
|
|
|
|
|
|
---
|
|
-- Validate a JSON schema document.
|
|
--
|
|
-- This is primarily for use in `kong.db.schema.metaschema`
|
|
--
|
|
---@param input kong.db.schema.json.schema_doc
|
|
---@return boolean? ok
|
|
---@return string? error
|
|
function _M.validate_schema(input)
|
|
local typ = type(input)
|
|
|
|
if typ ~= "table" then
|
|
return nil, "schema must be a table"
|
|
end
|
|
|
|
return validate(input, _M.metaschema)
|
|
end
|
|
|
|
|
|
---
|
|
-- Add a JSON schema document to the local registry.
|
|
--
|
|
---@param name string
|
|
---@param schema kong.db.schema.json.schema_doc
|
|
function _M.add_schema(name, schema)
|
|
schemas[name] = cycle_aware_deep_copy(schema, true)
|
|
end
|
|
|
|
|
|
---
|
|
-- Retrieve a schema from local storage by name.
|
|
--
|
|
---@param name string
|
|
---@return kong.db.schema.json.schema_doc? schema
|
|
function _M.get_schema(name)
|
|
return schemas[name]
|
|
end
|
|
|
|
|
|
---
|
|
-- Remove a schema from local storage by name (if it exists).
|
|
--
|
|
---@param name string
|
|
function _M.remove_schema(name)
|
|
schemas[name] = nil
|
|
end
|
|
|
|
|
|
return _M
|