-
Florian Spreckelsen authoredFlorian Spreckelsen authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
json_schema_exporter.py 6.49 KiB
#!/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/>.
#
import re
import linkahead as db
def _make_required_list(rt: db.RecordType):
"""Return the list of names of properties with importance db.OBLIGATORY."""
required = []
for prop in rt.properties:
importance = rt.get_importance(prop.name)
if importance == db.OBLIGATORY:
required.append(prop.name)
return required
def _extract_elemet_dtyp_from_list_dtype(list_dtype):
if not list_dtype.lower().startswith("list"):
raise ValueError(f"Not a list dtype: {list_dtype}")
pattern = r"^[Ll][Ii][Ss][Tt](<(?P<dtype1>.*)>|\((?P<dtype2>.*)\))$"
comp = re.compile(pattern)
mtch = comp.match(list_dtype)
if mtch is None:
raise ValueError(f"Not a list dtype: {list_dtype}")
elif "dtype1" in mtch.groupdict() and mtch.groupdict()["dtype1"] is not None:
return mtch.groupdict()["dtype1"]
elif "dtype2" in mtch.groupdict() and mtch.groupdict()["dtype2"] is not None:
return mtch.groupdict()["dtype2"]
else:
raise ValueError(f"Not a list dtype: {list_dtype}")
def _make_prop_from_prop(prop, additional_options_for_text_props):
if prop.is_reference():
raise NotImplementedError(
"Reference properties are not supported in this version of the json schema exporter."
)
if prop.datatype == db.TEXT or prop.datatype == db.DATETIME:
text_format = None
text_pattern = None
if prop.name in additional_options_for_text_props:
if "pattern" in additional_options_for_text_props[prop.name]:
text_pattern = additional_options_for_text_props[prop.name]["pattern"]
if "format" in additional_options_for_text_props[prop.name]:
text_format = additional_options_for_text_props[prop.name]["format"]
elif prop.datatype == db.DATETIME:
# Set the date or datetime format if only a pattern is given ...
text_format = ["date", "date-time"]
elif prop.datatype == db.DATETIME:
# ... again, for those props that don't appear in the additional
# options list.
text_format = ["date", "date-time"]
return _make_text_property(prop.description, text_format, text_pattern)
json_prop = {}
if prop.description:
json_prop["description"] = prop.description
if prop.datatype == db.BOOLEAN:
json_prop["type"] = "boolean"
elif prop.datatype == db.INTEGER:
json_prop["type"] = "integer"
elif prop.datatype == db.DOUBLE:
json_prop["type"] = "number"
elif isinstance(prop.datatype, str) and prop.datatype.startswith("LIST"):
json_prop["type"] = "array"
list_element_prop = db.Property(
name=prop.name, datatype=_extract_elemet_dtyp_from_list_dtype(prop.datatype))
json_prop["items"] = _make_prop_from_prop(
list_element_prop, additional_options_for_text_props)
else:
raise ValueError(
f"Unknown or no property datatype. Property {prop.name} with type {prop.datatype}")
return json_prop
def _make_text_property(description="", text_format=None, text_pattern=None):
prop = {
"type": "string"
}
if description:
prop["description"] = description
if text_format is not None:
if isinstance(text_format, list):
prop["anyOf"] = [{"format": tf} for tf in text_format]
else:
prop["format"] = text_format
if text_pattern is not None:
prop["pattern"] = text_pattern
return prop
def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = True,
name_and_description_in_properties: bool = False,
additional_options_for_text_props: dict = {}):
"""Create a jsonschema from a given RecordType that can be used, e.g., to
validate a json specifying a record of the given type.
Parameters
----------
rt : RecordType
The RecordType from which a json schema will be created.
additional_properties : bool, optional
Whether additional propeties will be admitted in the resulting
schema. Optional, default is True.
name_and_description_in_properties : bool, optional
Whether to include name and description in the `properties` section of
the schema to be exported. Optional, default is False.
additional_options_for_text_props : dict, optional
Dictionary containing additional "pattern" or "format" options for
string-typed properties. Optional, default is empty.
Returns
-------
schema : dict
A dict containing the json schema created from the given RecordType's properties.
"""
schema = {
"$schema": "https://json-schema.org/draft/2019-09/schema",
"type": "object"
}
if rt.name:
schema["title"] = rt.name
if rt.description:
schema["description"] = rt.description
schema["required"] = _make_required_list(rt)
schema["additionalProperties"] = additional_properties
props = {}
if name_and_description_in_properties:
props["name"] = _make_text_property("The name of the Record to be created")
props["description"] = _make_text_property("The description of the Record to be created")
for prop in rt.properties:
if prop.name in props:
# Multi property
raise NotImplementedError(
"Creating a schema for multi-properties is not specified. "
f"Property {prop.name} occurs more than once."
)
props[prop.name] = _make_prop_from_prop(prop, additional_options_for_text_props)
schema["properties"] = props
return schema