Skip to content
Snippets Groups Projects

ENH: JsonSchemaExporter accepts do_not_create parameter.

Merged Florian Spreckelsen requested to merge release-v0.9.0 into main
1 file
+ 18
0
Compare changes
  • Side-by-side
  • Inline
+ 1049
0
#!/usr/bin/env python
# encoding: utf-8
#
# This file is a part of the CaosDB Project.
#
# Copyright (C) 2023 Indiscale GmbH <info@indiscale.com>
# Copyright (C) 2023 Florian Spreckelsen <f.spreckelsen@indiscale.com>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""Tests the Json schema exporter."""
import json
import linkahead as db
import caosadvancedtools.json_schema_exporter as jsex
from collections import OrderedDict
from jsonschema import FormatChecker, validate, ValidationError
from pytest import raises
from unittest.mock import Mock, patch
from caosadvancedtools.json_schema_exporter import recordtype_to_json_schema as rtjs
from caosadvancedtools.models.parser import parse_model_from_string
GLOBAL_MODEL = parse_model_from_string("""
RT1:
description: some description
obligatory_properties:
some_date:
datatype: DATETIME
description: Just some date
RT21:
obligatory_properties:
RT1:
datatype: LIST<RT1>
RT31:
obligatory_properties:
RT1:
""")
RT1 = GLOBAL_MODEL.get_deep("RT1")
RT21 = GLOBAL_MODEL.get_deep("RT21")
RT31 = GLOBAL_MODEL.get_deep("RT31")
def _mock_execute_query(query_string, unique=False, **kwargs):
"""Mock the response to queries for references."""
all_records = db.Container()
all_files = db.Container()
other_type_rt = db.RecordType(name="OtherType")
other_type_rt.add_property(name="IntegerProp", datatype=db.INTEGER, importance=db.OBLIGATORY)
other_type_records = db.Container().extend([
db.Record(id=100, name="otherA").add_parent(other_type_rt),
db.Record(id=101, name="otherB").add_parent(other_type_rt),
db.Record(id=102).add_parent(other_type_rt)
])
all_records.extend(other_type_records)
referencing_type_rt = db.RecordType(name="ReferencingType")
referencing_type_rt.add_property(name=other_type_rt.name, datatype=db.LIST(other_type_rt.name))
referencing_type_records = db.Container().extend([
db.Record(id=103).add_parent(referencing_type_rt),
db.Record(id=104, name="referencing").add_parent(referencing_type_rt)
])
all_records.extend(referencing_type_records)
all_files.append(db.File(id=105, name="GenericFile.txt"))
if query_string == "SELECT name, id FROM RECORD 'OtherType'":
return other_type_records
elif query_string == "FIND RECORDTYPE WITH name='OtherType'" and unique is True:
return other_type_rt
elif query_string == "SELECT name, id FROM RECORD 'ReferencingType'":
return referencing_type_records
elif query_string == "FIND RECORDTYPE WITH name='ReferencingType'" and unique is True:
return referencing_type_rt
elif query_string == "SELECT name, id FROM RECORD 'RT1'":
return referencing_type_records # wrong types, but who cares for the test?
elif query_string == "FIND RECORDTYPE WITH name='RT1'" and unique is True:
return RT1
elif query_string == "FIND RECORDTYPE WITH name='RT21'" and unique is True:
return RT21
elif query_string == "FIND RECORDTYPE WITH name='RT31'" and unique is True:
return RT31
elif query_string == "SELECT name, id FROM RECORD":
return all_records
elif query_string == "SELECT name, id FROM FILE":
return all_files
else:
print(f"Query string: {query_string}")
if unique is True:
return db.Entity()
return db.Container()
def test_empty_rt():
rt = db.RecordType(name="Test", description="descr")
schema = rtjs(rt)
assert schema["title"] == rt.name
assert schema["description"] == rt.description
assert len(schema["properties"]) == 0
assert len(schema["required"]) == 0
assert schema["additionalProperties"] is True
schema = rtjs(rt, additional_properties=False)
assert schema["title"] == rt.name
assert schema["description"] == rt.description
assert len(schema["properties"]) == 0
assert len(schema["required"]) == 0
assert schema["additionalProperties"] is False
schema = rtjs(rt, name_property_for_new_records=True,
description_property_for_new_records=True)
assert len(schema["properties"]) == 2
assert "name" in schema["properties"]
assert "description" in schema["properties"]
assert schema["properties"]["name"]["type"] == "string"
assert schema["properties"]["description"]["type"] == "string"
def test_rt_with_scalar_props():
rt = db.RecordType(name="Test")
rt.add_property(name="SimpleText", datatype=db.TEXT, description="This is a simple text")
rt.add_property(name="ObligatoryDatetime", datatype=db.DATETIME, importance=db.OBLIGATORY)
rt.add_property(name="JustDateNoTime", datatype=db.DATETIME, description="Only dates, no times")
rt.add_property(name="ObligatoryInteger", datatype=db.INTEGER, importance=db.OBLIGATORY)
rt.add_property(name="Double", datatype=db.DOUBLE)
# Suggested shouldn't influence the result in any way.
rt.add_property(name="Boolean", datatype=db.BOOLEAN, importance=db.SUGGESTED)
schema = rtjs(rt, additional_options_for_text_props={"JustDateNoTime": {"format": "date"}})
assert "properties" in schema
props = schema["properties"]
assert len(props) == 6
assert "required" in schema
assert len(schema["required"]) == 2
assert "ObligatoryDatetime" in schema["required"]
assert "ObligatoryInteger" in schema["required"]
assert "SimpleText" in props
assert props["SimpleText"]["type"] == "string"
assert "format" not in props["SimpleText"]
assert "description" in props["SimpleText"]
assert props["SimpleText"]["description"] == "This is a simple text"
assert "ObligatoryDatetime" in props
assert "type" not in props["ObligatoryDatetime"]
assert "anyOf" in props["ObligatoryDatetime"]
assert len(props["ObligatoryDatetime"]["anyOf"]) == 2
date_found = 0
datetime_found = 0
for option in props["ObligatoryDatetime"]["anyOf"]:
assert option["type"] == "string"
fmt = option["format"]
if fmt == "date":
date_found += 1
if fmt == "date-time":
datetime_found += 1
assert date_found == 1
assert datetime_found == 1
assert "JustDateNoTime" in props
assert props["JustDateNoTime"]["type"] == "string"
assert "anyOf" not in props["JustDateNoTime"]
assert "pattern" not in props["JustDateNoTime"]
assert props["JustDateNoTime"]["format"] == "date"
assert props["JustDateNoTime"]["description"] == "Only dates, no times"
assert "ObligatoryInteger" in props
assert props["ObligatoryInteger"]["type"] == "integer"
assert "Double" in props
assert props["Double"]["type"] == "number"
assert "Boolean" in props
assert props["Boolean"]["type"] == "boolean"
# test validation (we turst the jsonschema.validat function, so only test
# some more or less tricky cases with format or required).
example = {
"SimpleText": "something",
"ObligatoryInteger": 23,
"ObligatoryDatetime": "1900-01-01T12:34:56.0Z",
"JustDateNoTime": "2023-10-13"
}
# We need to explicitly enable the FormatChecker, otherwise format will be
# ignored
# (https://python-jsonschema.readthedocs.io/en/latest/validate/#validating-formats)
validate(example, schema, format_checker=FormatChecker())
example = {
"SimpleText": "something",
"ObligatoryInteger": 23,
"ObligatoryDatetime": "1900-01-01",
"JustDateNoTime": "2023-10-13"
}
validate(example, schema, format_checker=FormatChecker())
example = {
"SimpleText": "something",
"ObligatoryDatetime": "1900-01-01T12:34:56.0Z",
"JustDateNoTime": "2023-10-13"
}
with raises(ValidationError):
# required missing
validate(example, schema, format_checker=FormatChecker())
example = {
"SimpleText": "something",
"ObligatoryInteger": 23,
"ObligatoryDatetime": "1900-01-01T12:34:56.0Z",
"JustDateNoTime": "2023-10-13T23:59:59.123Z"
}
with raises(ValidationError):
# date expected in JustDateNoTime, but datetime given
validate(example, schema, format_checker=FormatChecker())
def test_units():
rt = db.RecordType()
rt.add_property(name="ScalarWithUnit", datatype=db.DOUBLE, unit="m")
rt.add_property(name="ListWithUnit", description="This is a list.",
datatype=db.LIST(db.DOUBLE), unit="m")
schema = rtjs(rt, units_in_description=True)
props = schema["properties"]
assert "ScalarWithUnit" in props
assert props["ScalarWithUnit"]["type"] == "number"
assert "description" in props["ScalarWithUnit"]
assert props["ScalarWithUnit"]["description"] == "Unit is m."
assert "unit" not in props["ScalarWithUnit"]
assert "ListWithUnit" in props
assert props["ListWithUnit"]["type"] == "array"
assert "items" in props["ListWithUnit"]
assert props["ListWithUnit"]["items"]["type"] == "number"
assert "description" in props["ListWithUnit"]
assert props["ListWithUnit"]["description"] == "This is a list. Unit is m."
assert "unit" not in props["ListWithUnit"]
schema = rtjs(rt, units_in_description=False)
props = schema["properties"]
assert "ScalarWithUnit" in props
assert props["ScalarWithUnit"]["type"] == "number"
assert "description" not in props["ScalarWithUnit"]
assert "unit" in props["ScalarWithUnit"]
assert props["ScalarWithUnit"]["unit"] == "m"
assert "ListWithUnit" in props
assert props["ListWithUnit"]["type"] == "array"
assert "items" in props["ListWithUnit"]
assert props["ListWithUnit"]["items"]["type"] == "number"
assert "description" in props["ListWithUnit"]
assert props["ListWithUnit"]["description"] == "This is a list."
assert "unit" in props["ListWithUnit"]
assert props["ListWithUnit"]["unit"] == "m"
def test_rt_with_list_props():
rt = db.RecordType()
rt.add_property(name="ListOfIntegers", datatype=db.LIST(
db.INTEGER), description="List of integers")
rt.add_property(name="ListOfPatterns", datatype=db.LIST(db.TEXT))
schema = rtjs(rt, additional_options_for_text_props={"ListOfPatterns": {"pattern": "[A-Z]+"}})
props = schema["properties"]
assert "ListOfIntegers" in props
assert props["ListOfIntegers"]["type"] == "array"
assert "items" in props["ListOfIntegers"]
assert props["ListOfIntegers"]["items"]["type"] == "integer"
assert "description" not in props["ListOfIntegers"]["items"]
assert props["ListOfIntegers"]["description"] == "List of integers"
assert "ListOfPatterns" in props
assert props["ListOfPatterns"]["type"] == "array"
assert "items" in props["ListOfPatterns"]
assert props["ListOfPatterns"]["items"]["type"] == "string"
assert props["ListOfPatterns"]["items"]["pattern"] == "[A-Z]+"
# Validation
example = {
"ListOfIntegers": [1, 2, 3],
"ListOfPatterns": ["A", "BB", "CCC"]
}
validate(example, schema, format_checker=FormatChecker())
example = {
"ListOfIntegers": 1,
"ListOfPatterns": ["A", "BB", "CCC"]
}
with raises(ValidationError):
# No list
validate(example, schema, format_checker=FormatChecker())
example = {
"ListOfIntegers": [1, 2, 3],
"ListOfPatterns": ["A", "bb", "CCC"]
}
with raises(ValidationError):
# Pattern doesn't match
validate(example, schema, format_checker=FormatChecker())
@patch("linkahead.execute_query", new=Mock(side_effect=_mock_execute_query))
def test_rt_with_references():
rt = db.RecordType()
rt.add_property(name="RefProp", datatype=db.REFERENCE)
schema = rtjs(rt)
props = schema["properties"]
assert "RefProp" in props
assert "enum" in props["RefProp"]
assert isinstance(props["RefProp"]["enum"], list)
assert len(props["RefProp"]["enum"]) == len(
db.execute_query("SELECT name, id FROM RECORD")) + len(
db.execute_query("SELECT name, id FROM FILE"))
assert "oneOf" not in props["RefProp"]
example = {
"RefProp": "otherB"
}
validate(example, schema)
example = {
"RefProp": "I don't exist"
}
with raises(ValidationError):
# Wrong enum value
validate(example, schema)
example = {
"RefProp": {
"IntegerProp": 12
}
}
with raises(ValidationError):
# Can't have objects in generic references
validate(example, schema)
rt = db.RecordType()
rt.add_property(name="RefProp", datatype="OtherType")
rt.add_property(name="OtherTextProp", datatype=db.TEXT)
schema = rtjs(rt)
props = schema["properties"]
assert "RefProp" in props
assert "oneOf" in props["RefProp"]
assert len(props["RefProp"]["oneOf"]) == 2
enum_index = 0
if "enum" not in props["RefProp"]["oneOf"][enum_index]:
# We can't really require the order here, so we just know that one of
# the two elements must be the enum, the other the object.
enum_index = 1 - enum_index
assert "enum" in props["RefProp"]["oneOf"][enum_index]
assert isinstance(props["RefProp"]["oneOf"][enum_index]["enum"], list)
assert len(props["RefProp"]["oneOf"][enum_index]["enum"]) == 3
assert "otherA" in props["RefProp"]["oneOf"][enum_index]["enum"]
assert "otherB" in props["RefProp"]["oneOf"][enum_index]["enum"]
assert "102" in props["RefProp"]["oneOf"][enum_index]["enum"]
# the other element of oneOf is the OtherType object
assert props["RefProp"]["oneOf"][1 - enum_index]["type"] == "object"
other_props = props["RefProp"]["oneOf"][1 - enum_index]["properties"]
assert "IntegerProp" in other_props
assert other_props["IntegerProp"]["type"] == "integer"
assert "required" in props["RefProp"]["oneOf"][1 - enum_index]
assert len(props["RefProp"]["oneOf"][1 - enum_index]["required"]) == 1
assert "IntegerProp" in props["RefProp"]["oneOf"][1 - enum_index]["required"]
# The other prop also works as before
assert "OtherTextProp" in props
assert props["OtherTextProp"]["type"] == "string"
example = {
"RefProp": {
"IntegerProp": 12
}
}
validate(example, schema)
example = {
"RefProp": "otherB",
"OtherTextProp": "something"
}
validate(example, schema)
rt = db.RecordType(name="TestType", description="Some description")
rt.add_property(name="RefProp", datatype=db.LIST(db.REFERENCE),
description="I'm a list of references.")
schema = rtjs(rt)
assert schema["title"] == rt.name
assert schema["description"] == rt.description
assert "RefProp" in schema["properties"]
ref_prop = schema["properties"]["RefProp"]
assert ref_prop["type"] == "array"
assert "description" in ref_prop
assert ref_prop["description"] == "I'm a list of references."
assert "items" in ref_prop
items = ref_prop["items"]
assert "enum" in items
assert isinstance(items["enum"], list)
assert len(items["enum"]) == len(
db.execute_query("SELECT name, id FROM RECORD")) + len(
db.execute_query("SELECT name, id FROM FILE"))
assert "oneOf" not in items
assert "description" not in items
example = {
"RefProp": "otherB"
}
with raises(ValidationError):
# Should be list but isn't
validate(example, schema)
example = {
"RefProp": ["otherB"]
}
validate(example, schema)
example = {
"RefProp": ["otherB", "102", "referencing"]
}
validate(example, schema)
rt = db.RecordType()
rt.add_property(name="RefProp", datatype=db.LIST("OtherType"))
schema = rtjs(rt, additional_properties=False, name_property_for_new_records=True,
description_property_for_new_records=True)
assert schema["additionalProperties"] is False
assert "name" in schema["properties"]
assert schema["properties"]["name"]["type"] == "string"
assert "description" in schema["properties"]
assert schema["properties"]["description"]["type"] == "string"
assert "RefProp" in schema["properties"]
assert schema["properties"]["RefProp"]["type"] == "array"
assert "additionalProperties" not in schema["properties"]["RefProp"]
assert "items" in schema["properties"]["RefProp"]
items = schema["properties"]["RefProp"]["items"]
assert "oneOf" in items
assert len(items["oneOf"]) == 2
# same as above, we can't rely on the order
enum_index = 0
if "enum" not in items["oneOf"][enum_index]:
enum_index = 1 - enum_index
assert "enum" in items["oneOf"][enum_index]
assert isinstance(items["oneOf"][enum_index]["enum"], list)
assert len(items["oneOf"][enum_index]["enum"]) == 3
assert "otherA" in items["oneOf"][enum_index]["enum"]
assert "otherB" in items["oneOf"][enum_index]["enum"]
assert "102" in items["oneOf"][enum_index]["enum"]
other_type = items["oneOf"][1 - enum_index]
assert other_type["type"] == "object"
assert other_type["additionalProperties"] is False
assert "IntegerProp" in other_type["properties"]
assert len(other_type["required"]) == 1
assert "IntegerProp" in other_type["required"]
example = {
"RefProp": ["otherB", "102", "referencing"]
}
with raises(ValidationError):
# Wrong value in enum
validate(example, schema)
example = {
"RefProp": [{"IntegerProp": 12}]
}
validate(example, schema)
example = {
"RefProp": [{"IntegerProp": 12, "additionalProperty": "something"}]
}
with raises(ValidationError):
# we have additional_properties=False which propagates to subschemas
validate(example, schema)
example = {
"RefProp": [{"IntegerProp": 12}, "otherB"]
}
validate(example, schema)
rt = db.RecordType(name="ReferenceofReferencesType")
rt.add_property(name="RefRefProp", datatype="ReferencingType")
schema = rtjs(rt)
assert "RefRefProp" in schema["properties"]
ref_ref = schema["properties"]["RefRefProp"]
assert "oneOf" in ref_ref
assert len(ref_ref["oneOf"]) == 2
enum_index = 0
if "enum" not in ref_ref["oneOf"][enum_index]:
enum_index = 1 - enum_index
assert len(ref_ref["oneOf"][enum_index]["enum"]) == 2
assert "103" in ref_ref["oneOf"][enum_index]["enum"]
assert "referencing" in ref_ref["oneOf"][enum_index]["enum"]
assert ref_ref["oneOf"][1 - enum_index]["type"] == "object"
assert "OtherType" in ref_ref["oneOf"][1 - enum_index]["properties"]
assert ref_ref["oneOf"][1 - enum_index]["properties"]["OtherType"]["type"] == "array"
items = ref_ref["oneOf"][1 - enum_index]["properties"]["OtherType"]["items"]
assert "oneOf" in items
assert len(items["oneOf"]) == 2
# same as above, we can't rely on the order
enum_index = 0
if "enum" not in items["oneOf"][enum_index]:
enum_index = 1 - enum_index
assert "enum" in items["oneOf"][enum_index]
assert isinstance(items["oneOf"][enum_index]["enum"], list)
assert len(items["oneOf"][enum_index]["enum"]) == 3
assert "otherA" in items["oneOf"][enum_index]["enum"]
assert "otherB" in items["oneOf"][enum_index]["enum"]
assert "102" in items["oneOf"][enum_index]["enum"]
other_type = items["oneOf"][1 - enum_index]
assert other_type["type"] == "object"
assert "IntegerProp" in other_type["properties"]
assert len(other_type["required"]) == 1
assert "IntegerProp" in other_type["required"]
example = {
"RefRefProp": {
"OtherType": [
"otherA",
{"IntegerProp": 12}
]
}
}
validate(example, schema)
# Single file and multiple files
rt = db.RecordType()
rt.add_property(name="FileProp", datatype=db.FILE)
schema = rtjs(rt)
assert schema["properties"]["FileProp"]["type"] == "string"
assert schema["properties"]["FileProp"]["format"] == "data-url"
# wrap in array (cf. https://github.com/rjsf-team/react-jsonschema-form/issues/3957)
schema = rtjs(rt, wrap_files_in_objects=True)
assert schema["properties"]["FileProp"]["type"] == "array"
assert schema["properties"]["FileProp"]["maxItems"] == 1
assert "items" in schema["properties"]["FileProp"]
items = schema["properties"]["FileProp"]["items"]
assert items["type"] == "object"
assert len(items["required"]) == 1
assert "file" in items["required"]
assert items["additionalProperties"] is False
assert len(items["properties"]) == 1
assert "file" in items["properties"]
assert items["properties"]["file"]["type"] == "string"
assert items["properties"]["file"]["format"] == "data-url"
rt = db.RecordType()
rt.add_property(name="FileProp", datatype=db.LIST(db.FILE))
schema = rtjs(rt)
assert schema["properties"]["FileProp"]["type"] == "array"
assert schema["properties"]["FileProp"]["items"]["type"] == "string"
assert schema["properties"]["FileProp"]["items"]["format"] == "data-url"
# wrap in array (cf. https://github.com/rjsf-team/react-jsonschema-form/issues/3957)
print(schema)
schema = rtjs(rt, wrap_files_in_objects=True)
assert schema["properties"]["FileProp"]["type"] == "array"
assert "maxItems" not in schema["properties"]["FileProp"]
assert "items" in schema["properties"]["FileProp"]
items = schema["properties"]["FileProp"]["items"]
assert items["type"] == "object"
assert len(items["required"]) == 1
assert "file" in items["required"]
assert items["additionalProperties"] is False
assert len(items["properties"]) == 1
assert "file" in items["properties"]
assert items["properties"]["file"]["type"] == "string"
assert items["properties"]["file"]["format"] == "data-url"
def test_broken():
rt = db.RecordType()
rt.add_property(name="something", datatype=None)
with raises(ValueError) as ve:
rtjs(rt)
assert str(ve).startswith("Unknown or no property datatype.")
rt = db.RecordType()
rt.add_property(name="MultiProp", datatype=db.INTEGER)
rt.add_property(name="MultiProp", datatype=db.INTEGER)
with raises(NotImplementedError) as nie:
rtjs(rt)
assert "MultiProp" in str(nie)
assert str(nie).startswith("Creating a schema for multi-properties is not specified.")
@patch("linkahead.execute_query", new=Mock(side_effect=_mock_execute_query))
def test_reference_options():
"""Testing miscellaneous options.
"""
model_str = """
RT1:
description: some description
obligatory_properties:
some_date:
datatype: DATETIME
description: Just some date
RT2:
obligatory_properties:
RT1:
RT3:
obligatory_properties:
RT1_prop:
datatype: RT1
description: property description
"""
model = parse_model_from_string(model_str)
# First test: without reference
rt1_dict = rtjs(model.get_deep("RT1"))
assert json.dumps(rt1_dict, indent=2) == """{
"type": "object",
"required": [
"some_date"
],
"additionalProperties": true,
"description": "some description",
"title": "RT1",
"properties": {
"some_date": {
"description": "Just some date",
"anyOf": [
{
"type": "string",
"format": "date"
},
{
"type": "string",
"format": "date-time"
}
]
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema"
}"""
# Second test: with reference
rt2_deep = model.get_deep("RT2")
rt2_dict = rtjs(rt2_deep)
assert json.dumps(rt2_dict, indent=2) == """{
"type": "object",
"required": [
"RT1"
],
"additionalProperties": true,
"title": "RT2",
"properties": {
"RT1": {
"description": "some description",
"oneOf": [
{
"title": "Existing entries",
"enum": [
"103",
"referencing"
]
},
{
"type": "object",
"required": [
"some_date"
],
"additionalProperties": true,
"description": "some description",
"title": "Create new",
"properties": {
"some_date": {
"description": "Just some date",
"anyOf": [
{
"type": "string",
"format": "date"
},
{
"type": "string",
"format": "date-time"
}
]
}
}
}
]
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema"
}"""
# Third test: Reference prop shall be only existing references, no option to create new ones.
rt2_dict = rtjs(model.get_deep("RT2"), do_not_create=["RT1"])
assert json.dumps(rt2_dict, indent=2) == """{
"type": "object",
"required": [
"RT1"
],
"additionalProperties": true,
"title": "RT2",
"properties": {
"RT1": {
"description": "some description",
"enum": [
"103",
"referencing"
]
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema"
}"""
# No effect of do_not_create (real property name should be used)
rt3_dict = rtjs(model.get_deep("RT3"), do_not_create=["RT1"])
rt1_prop = rt3_dict["properties"]["RT1_prop"]
assert rt1_prop["description"] == "property description"
assert "oneOf" in rt1_prop.keys()
assert "enum" not in rt1_prop.keys()
# Now we use the real property name
rt3_dict = rtjs(model.get_deep("RT3"), do_not_create=["RT1_prop"])
rt1_prop = rt3_dict["properties"]["RT1_prop"]
assert rt1_prop["description"] == "property description"
assert "oneOf" not in rt1_prop.keys()
assert "enum" in rt1_prop.keys()
assert rt1_prop["enum"][0] == "103"
def test_schema_modification():
"""Testing functions which modify json schema dicts:
- make_array()
- merge_schemas().
"""
model_str = """
some_date:
datatype: DATETIME
RT1:
obligatory_properties:
some_date:
some_text:
datatype: TEXT
RT2:
obligatory_properties:
some_text:
"""
model = parse_model_from_string(model_str)
schema_RT1 = rtjs(model.get_deep("RT1"), additional_properties=False)
schema_RT2 = rtjs(model.get_deep("RT2"), additional_properties=False)
# Merge the schemata
merged_list = jsex.merge_schemas([schema_RT1, schema_RT2])
with raises(ValidationError):
validate({}, merged_list)
assert merged_list["type"] == "object"
assert merged_list["properties"]["RT1"]["title"] == "RT1"
assert merged_list["properties"]["RT2"]["properties"]["some_text"]["type"] == "string"
merged_dict = jsex.merge_schemas({"schema1": schema_RT1, "schema2": schema_RT2})
with raises(ValidationError):
validate({}, merged_dict)
assert merged_dict["type"] == "object"
assert merged_dict["properties"]["schema1"]["title"] == "RT1"
assert merged_dict["properties"]["schema2"]["properties"]["some_text"]["type"] == "string"
# Make an array
array = jsex.make_array(schema_RT1)
with raises(ValidationError):
validate({}, array)
assert array["type"] == "array"
assert array["items"] == schema_RT1
def test_inheritance():
"""Test data models with inherited properties."""
model_str = """
some_date:
datatype: DATETIME
RT1:
obligatory_properties:
some_date:
RT2:
inherit_from_suggested:
- RT1
"""
model = parse_model_from_string(model_str)
rt2_deep = model.get_deep("RT2")
assert "some_date" in [prop.name for prop in rt2_deep.properties]
model_str = """
RT1:
obligatory_properties:
RT2:
RT2:
inherit_from_suggested:
- RT1
RT3:
inherit_from_suggested:
- RT4
RT4:
inherit_from_suggested:
- RT3
RT5:
inherit_from_suggested:
- RT5
"""
model = parse_model_from_string(model_str)
# This must not lead to an infinite recursion
rt1_deep = model.get_deep("RT1")
rt2_deep = model.get_deep("RT2")
assert rt2_deep.get_property("RT2").name == rt1_deep.get_property("RT2").name
rt3_deep = model.get_deep("RT3")
assert rt3_deep.get_parents()[0].name == "RT4"
rt4_deep = model.get_deep("RT4")
assert rt4_deep.get_parents()[0].name == "RT3"
rt5_deep = model.get_deep("RT5")
assert rt5_deep.get_parents()[0].name == "RT5"
@patch("linkahead.execute_query", new=Mock(side_effect=_mock_execute_query))
def test_empty_retrieve():
"""Special case: ``do_not_retrieve`` is set, or the retrieve result is empty."""
model_str = """
RT1:
description: Some text.
RT2:
obligatory_properties:
RT1:
# some_text:
# datatype: TEXT
NoRecords:
description: A RecordType without Records.
recommended_properties:
some_text:
datatype: TEXT
RT3:
obligatory_properties:
NoRecords:
"""
model = parse_model_from_string(model_str)
schema_default = rtjs(model.get_deep("RT2"))
assert "oneOf" in schema_default["properties"]["RT1"]
assert any([el.get("title") == "Existing entries" for el in
schema_default["properties"]["RT1"]["oneOf"]])
schema_noexist = rtjs(model.get_deep("RT3"))
assert schema_noexist["properties"]["NoRecords"].get("type") == "object"
schema_noexist_noremote = rtjs(model.get_deep("RT3"), no_remote=True)
assert schema_noexist_noremote["properties"]["NoRecords"].get("type") == "object"
assert (schema_noexist_noremote["properties"]["NoRecords"].get("properties")
== OrderedDict([('some_text', {'type': 'string'})]))
uischema = {}
schema_noexist_noretrieve = rtjs(model.get_deep("RT2"), do_not_retrieve=["RT1"],
rjsf=uischema)
assert schema_noexist_noretrieve["properties"]["RT1"].get("type") == "object"
assert "some_date" in schema_noexist_noretrieve["properties"]["RT1"].get("properties")
assert not uischema
@patch("linkahead.execute_query", new=Mock(side_effect=_mock_execute_query))
def test_multiple_choice():
"""Multiple choice is mostyly a matter of UI."""
model_str = """
RT1:
RT21:
obligatory_properties:
RT1:
datatype: LIST<RT1>
RT3:
obligatory_properties:
RT21:
RT4:
obligatory_properties:
RT21:
datatype: LIST<RT21>
"""
model = parse_model_from_string(model_str)
# generate a multiple choice, in first level
schema, uischema = rtjs(model.get_deep("RT21"), additional_properties=False,
do_not_create=["RT1"], multiple_choice=["RT1"], rjsf=True)
assert schema["properties"]["RT1"]["uniqueItems"] is True
assert str(uischema) == "{'RT1': {'ui:widget': 'checkboxes', 'ui:inline': True}}"
# second level
schema, uischema = rtjs(model.get_deep("RT3"), additional_properties=False,
do_not_create=["RT1"], multiple_choice=["RT1"], rjsf=True)
assert schema["properties"]["RT21"]["properties"]["RT1"]["uniqueItems"] is True
assert (str(uischema)
== "{'RT21': {'RT1': {'ui:widget': 'checkboxes', 'ui:inline': True}}}")
# second level with lists
schema, uischema = rtjs(model.get_deep("RT4"), additional_properties=False,
do_not_create=["RT1"], multiple_choice=["RT1"], rjsf=True)
assert schema["properties"]["RT21"]["items"]["properties"]["RT1"]["uniqueItems"] is True
assert (str(uischema) ==
"{'RT21': {'items': {'RT1': {'ui:widget': 'checkboxes', "
"'ui:inline': True}}}}")
@patch("linkahead.execute_query", new=Mock(side_effect=_mock_execute_query))
def test_uischema():
model_str = """
RT1:
RT2:
obligatory_properties:
RT1:
datatype: LIST<RT1>
RT3:
obligatory_properties:
RT1:
datatype: LIST<RT1>
"""
model = parse_model_from_string(model_str)
schema_2, uischema_2 = rtjs(model.get_deep("RT2"), additional_properties=False,
do_not_create=["RT1"], multiple_choice=["RT1"], rjsf=True)
schema_3, uischema_3 = rtjs(model.get_deep("RT3"), additional_properties=False,
do_not_create=["RT1"], multiple_choice=["RT1"], rjsf=True)
# Merging #################################################################
# Using dictionaries
schemas_dict = {"schema_2": schema_2, "schema_3": schema_3}
uischemas_dict = {"schema_2": uischema_2, "schema_3": uischema_3}
merged_dict, merged_dict_ui = jsex.merge_schemas(schemas_dict, uischemas_dict)
assert merged_dict_ui["schema_2"] == merged_dict_ui["schema_3"]
assert (str(merged_dict_ui["schema_2"])
== "{'RT1': {'ui:widget': 'checkboxes', 'ui:inline': True}}")
# Using lists
schemas_list = [schema_2, schema_3]
uischemas_list = [uischema_2, uischema_3]
merged_list, merged_list_ui = jsex.merge_schemas(schemas_list, uischemas_list)
assert merged_list["properties"]["RT2"] == merged_dict["properties"]["schema_2"]
assert merged_list_ui["RT2"] == merged_list_ui["RT3"]
assert merged_list_ui["RT2"] == merged_dict_ui["schema_2"]
# Asserting failures
with raises(ValueError):
jsex.merge_schemas(schemas_dict, uischemas_list)
with raises(ValueError):
jsex.merge_schemas(schemas_list, uischemas_dict)
# Arraying ################################################################
array2, array2_ui = jsex.make_array(schema_2, uischema_2)
assert array2["items"] == schema_2
assert array2_ui["items"] == uischema_2
assert (str(array2_ui["items"])
== "{'RT1': {'ui:widget': 'checkboxes', 'ui:inline': True}}")
@patch("linkahead.execute_query", new=Mock(side_effect=_mock_execute_query))
def test_schema_customization_with_dicts():
"""Testing the ``additional_json_schema`` and ``additional_ui_schema`` parameters."""
model_str = """
RT1:
RT21:
obligatory_properties:
RT1:
datatype: LIST<RT1>
text:
datatype: TEXT
description: Some description
RT3:
obligatory_properties:
number:
datatype: INTEGER
"""
model = parse_model_from_string(model_str)
custom_schema = {
"RT21": {
"minProperties": 2,
},
"text": {
"format": "email",
"description": "Better description.",
},
"number": {
"minimum": 0,
"exclusiveMaximum": 100,
},
}
custom_ui_schema = {
"text": {
"ui:help": "Hint: keep it short.",
"ui:widget": "password",
},
"number": {
"ui:order": 2,
}
}
schema_21, uischema_21 = rtjs(model.get_deep("RT21"), additional_properties=False,
do_not_create=["RT1"], rjsf=True)
assert len(uischema_21) == 0
assert schema_21["properties"]["text"]["description"] == "Some description"
assert "format" not in schema_21["properties"]["text"]
schema_21, uischema_21 = rtjs(model.get_deep("RT21"), additional_properties=False,
additional_json_schema=custom_schema,
additional_ui_schema=custom_ui_schema, do_not_create=["RT1"],
rjsf=True)
assert (str(uischema_21)
== "{'text': {'ui:help': 'Hint: keep it short.', 'ui:widget': 'password'}}")
assert schema_21["properties"]["text"]["description"] == "Better description."
assert schema_21["properties"]["text"].get("format") == "email"
assert schema_21.get("minProperties") == 2
schema_3, uischema_3 = rtjs(model.get_deep("RT3"), additional_properties=False,
additional_json_schema=custom_schema,
additional_ui_schema=custom_ui_schema, rjsf=True)
assert (json.dumps(schema_3["properties"]["number"]) ==
'{"type": "integer", "minimum": 0, "exclusiveMaximum": 100}')
assert (str(uischema_3) == "{'number': {'ui:order': 2}}")
Loading