281 lines
9.4 KiB
PL/PgSQL
281 lines
9.4 KiB
PL/PgSQL
-- from gavinwahl/postgres-json-schema commit 5a257e19a1569a77b82e9182b0b7d9fc8b6f6382
|
|
|
|
/*
|
|
Copyright (c) 2016, Gavin Wahl
|
|
|
|
Permission to use, copy, modify, and distribute this software and its
|
|
documentation for any purpose, without fee, and without a written agreement is
|
|
hereby granted, provided that the above copyright notice and this paragraph and
|
|
the following two paragraphs appear in all copies.
|
|
|
|
IN NO EVENT SHALL GAVIN WAHL BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
|
|
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING
|
|
OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF GAVIN WAHL HAS
|
|
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
GAVIN WAHL SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND GAVIN WAHL
|
|
HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
|
|
MODIFICATIONS.
|
|
*/
|
|
|
|
CREATE OR REPLACE FUNCTION public._validate_json_schema_type(type text, data jsonb) RETURNS boolean AS $f$
|
|
BEGIN
|
|
IF type = 'integer' THEN
|
|
IF jsonb_typeof(data) != 'number' THEN
|
|
RETURN false;
|
|
END IF;
|
|
IF trunc(data::text::numeric) != data::text::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
ELSE
|
|
IF type != jsonb_typeof(data) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
RETURN true;
|
|
END;
|
|
$f$ LANGUAGE 'plpgsql' IMMUTABLE;
|
|
|
|
CREATE OR REPLACE FUNCTION test.validate_json_schema(schema jsonb, data jsonb, root_schema jsonb DEFAULT NULL) RETURNS boolean AS $f$
|
|
DECLARE
|
|
prop text;
|
|
item jsonb;
|
|
path text[];
|
|
types text[];
|
|
pattern text;
|
|
props text[];
|
|
BEGIN
|
|
IF root_schema IS NULL THEN
|
|
root_schema = schema;
|
|
END IF;
|
|
|
|
IF schema ? 'type' THEN
|
|
IF jsonb_typeof(schema->'type') = 'array' THEN
|
|
types = ARRAY(SELECT jsonb_array_elements_text(schema->'type'));
|
|
ELSE
|
|
types = ARRAY[schema->>'type'];
|
|
END IF;
|
|
IF (SELECT NOT bool_or(public._validate_json_schema_type(type, data)) FROM unnest(types) type) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'properties' THEN
|
|
FOR prop IN SELECT jsonb_object_keys(schema->'properties') LOOP
|
|
IF data ? prop AND NOT validate_json_schema(schema->'properties'->prop, data->prop, root_schema) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END LOOP;
|
|
END IF;
|
|
|
|
IF schema ? 'required' AND jsonb_typeof(data) = 'object' THEN
|
|
IF NOT ARRAY(SELECT jsonb_object_keys(data)) @>
|
|
ARRAY(SELECT jsonb_array_elements_text(schema->'required')) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'items' AND jsonb_typeof(data) = 'array' THEN
|
|
IF jsonb_typeof(schema->'items') = 'object' THEN
|
|
FOR item IN SELECT jsonb_array_elements(data) LOOP
|
|
IF NOT validate_json_schema(schema->'items', item, root_schema) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END LOOP;
|
|
ELSE
|
|
IF NOT (
|
|
SELECT bool_and(i > jsonb_array_length(schema->'items') OR validate_json_schema(schema->'items'->(i::int - 1), elem, root_schema))
|
|
FROM jsonb_array_elements(data) WITH ORDINALITY AS t(elem, i)
|
|
) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF jsonb_typeof(schema->'additionalItems') = 'boolean' and NOT (schema->'additionalItems')::text::boolean AND jsonb_typeof(schema->'items') = 'array' THEN
|
|
IF jsonb_array_length(data) > jsonb_array_length(schema->'items') THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF jsonb_typeof(schema->'additionalItems') = 'object' THEN
|
|
IF NOT (
|
|
SELECT bool_and(validate_json_schema(schema->'additionalItems', elem, root_schema))
|
|
FROM jsonb_array_elements(data) WITH ORDINALITY AS t(elem, i)
|
|
WHERE i > jsonb_array_length(schema->'items')
|
|
) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'minimum' AND jsonb_typeof(data) = 'number' THEN
|
|
IF data::text::numeric < (schema->>'minimum')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'maximum' AND jsonb_typeof(data) = 'number' THEN
|
|
IF data::text::numeric > (schema->>'maximum')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF COALESCE((schema->'exclusiveMinimum')::text::bool, FALSE) THEN
|
|
IF data::text::numeric = (schema->>'minimum')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF COALESCE((schema->'exclusiveMaximum')::text::bool, FALSE) THEN
|
|
IF data::text::numeric = (schema->>'maximum')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'anyOf' THEN
|
|
IF NOT (SELECT bool_or(validate_json_schema(sub_schema, data, root_schema)) FROM jsonb_array_elements(schema->'anyOf') sub_schema) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'allOf' THEN
|
|
IF NOT (SELECT bool_and(validate_json_schema(sub_schema, data, root_schema)) FROM jsonb_array_elements(schema->'allOf') sub_schema) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'oneOf' THEN
|
|
IF 1 != (SELECT COUNT(*) FROM jsonb_array_elements(schema->'oneOf') sub_schema WHERE validate_json_schema(sub_schema, data, root_schema)) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF COALESCE((schema->'uniqueItems')::text::boolean, false) THEN
|
|
IF (SELECT COUNT(*) FROM jsonb_array_elements(data)) != (SELECT count(DISTINCT val) FROM jsonb_array_elements(data) val) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'additionalProperties' AND jsonb_typeof(data) = 'object' THEN
|
|
props := ARRAY(
|
|
SELECT key
|
|
FROM jsonb_object_keys(data) key
|
|
WHERE key NOT IN (SELECT jsonb_object_keys(schema->'properties'))
|
|
AND NOT EXISTS (SELECT * FROM jsonb_object_keys(schema->'patternProperties') pat WHERE key ~ pat)
|
|
);
|
|
IF jsonb_typeof(schema->'additionalProperties') = 'boolean' THEN
|
|
IF NOT (schema->'additionalProperties')::text::boolean AND jsonb_typeof(data) = 'object' AND NOT props <@ ARRAY(SELECT jsonb_object_keys(schema->'properties')) THEN
|
|
RETURN false;
|
|
END IF;
|
|
ELSEIF NOT (
|
|
SELECT bool_and(validate_json_schema(schema->'additionalProperties', data->key, root_schema))
|
|
FROM unnest(props) key
|
|
) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? '$ref' THEN
|
|
path := ARRAY(
|
|
SELECT regexp_replace(regexp_replace(path_part, '~1', '/'), '~0', '~')
|
|
FROM UNNEST(regexp_split_to_array(schema->>'$ref', '/')) path_part
|
|
);
|
|
-- ASSERT path[1] = '#', 'only refs anchored at the root are supported';
|
|
IF NOT validate_json_schema(root_schema #> path[2:array_length(path, 1)], data, root_schema) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'enum' THEN
|
|
IF NOT EXISTS (SELECT * FROM jsonb_array_elements(schema->'enum') val WHERE val = data) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'minLength' AND jsonb_typeof(data) = 'string' THEN
|
|
IF char_length(data #>> '{}') < (schema->>'minLength')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'maxLength' AND jsonb_typeof(data) = 'string' THEN
|
|
IF char_length(data #>> '{}') > (schema->>'maxLength')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'not' THEN
|
|
IF validate_json_schema(schema->'not', data, root_schema) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'maxProperties' AND jsonb_typeof(data) = 'object' THEN
|
|
IF (SELECT count(*) FROM jsonb_object_keys(data)) > (schema->>'maxProperties')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'minProperties' AND jsonb_typeof(data) = 'object' THEN
|
|
IF (SELECT count(*) FROM jsonb_object_keys(data)) < (schema->>'minProperties')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'maxItems' AND jsonb_typeof(data) = 'array' THEN
|
|
IF (SELECT count(*) FROM jsonb_array_elements(data)) > (schema->>'maxItems')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'minItems' AND jsonb_typeof(data) = 'array' THEN
|
|
IF (SELECT count(*) FROM jsonb_array_elements(data)) < (schema->>'minItems')::numeric THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'dependencies' THEN
|
|
FOR prop IN SELECT jsonb_object_keys(schema->'dependencies') LOOP
|
|
IF data ? prop THEN
|
|
IF jsonb_typeof(schema->'dependencies'->prop) = 'array' THEN
|
|
IF NOT (SELECT bool_and(data ? dep) FROM jsonb_array_elements_text(schema->'dependencies'->prop) dep) THEN
|
|
RETURN false;
|
|
END IF;
|
|
ELSE
|
|
IF NOT validate_json_schema(schema->'dependencies'->prop, data, root_schema) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
END IF;
|
|
END LOOP;
|
|
END IF;
|
|
|
|
IF schema ? 'pattern' AND jsonb_typeof(data) = 'string' THEN
|
|
IF (data #>> '{}') !~ (schema->>'pattern') THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
IF schema ? 'patternProperties' AND jsonb_typeof(data) = 'object' THEN
|
|
FOR prop IN SELECT jsonb_object_keys(data) LOOP
|
|
FOR pattern IN SELECT jsonb_object_keys(schema->'patternProperties') LOOP
|
|
RAISE NOTICE 'prop %s, pattern %, schema %', prop, pattern, schema->'patternProperties'->pattern;
|
|
IF prop ~ pattern AND NOT validate_json_schema(schema->'patternProperties'->pattern, data->prop, root_schema) THEN
|
|
RETURN false;
|
|
END IF;
|
|
END LOOP;
|
|
END LOOP;
|
|
END IF;
|
|
|
|
IF schema ? 'multipleOf' AND jsonb_typeof(data) = 'number' THEN
|
|
IF data::text::numeric % (schema->>'multipleOf')::numeric != 0 THEN
|
|
RETURN false;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN true;
|
|
END;
|
|
$f$ LANGUAGE 'plpgsql' IMMUTABLE;
|