From 5b8df05b855af46ea862f38283a4949248fde742 Mon Sep 17 00:00:00 2001
From: fspreck <f.spreckelsen@indiscale.com>
Date: Fri, 13 Oct 2023 17:54:57 +0200
Subject: [PATCH] ENH: Implement list and scalar properties

---
 src/caosadvancedtools/json_schema_exporter.py | 89 +++++++++++++++++--
 1 file changed, 81 insertions(+), 8 deletions(-)

diff --git a/src/caosadvancedtools/json_schema_exporter.py b/src/caosadvancedtools/json_schema_exporter.py
index bb4d0f22..e7f54151 100644
--- a/src/caosadvancedtools/json_schema_exporter.py
+++ b/src/caosadvancedtools/json_schema_exporter.py
@@ -20,6 +20,8 @@
 # with this program. If not, see <https://www.gnu.org/licenses/>.
 #
 
+import re
+
 import linkahead as db
 
 
@@ -33,15 +35,76 @@ def _make_required_list(rt: db.RecordType):
     return required
 
 
-def _make_prop_from_prop(prop):
+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}")
 
-    if prop.datatype == db.TEXT:
-        return _make_text_property(prop.description)
-    elif prop.datatype == db.DATETIME:
-        return _make_text_property(prop.description, "date-time")
+
+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
+    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 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):
@@ -52,7 +115,10 @@ def _make_text_property(description="", text_format=None, text_pattern=None):
     if description:
         prop["description"] = description
     if text_format is not None:
-        prop["format"] = text_format
+        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
 
@@ -60,7 +126,8 @@ def _make_text_property(description="", text_format=None, text_pattern=None):
 
 
 def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = True,
-                              name_and_description_in_properties: bool = False):
+                              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.
 
@@ -74,6 +141,9 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T
     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
     -------
@@ -99,5 +169,8 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T
         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:
+        props[prop.name] = _make_prop_from_prop(prop, additional_options_for_text_props)
+
     schema["properties"] = props
     return schema
-- 
GitLab