diff --git a/src/caosadvancedtools/json_schema_exporter.py b/src/caosadvancedtools/json_schema_exporter.py index 47821d7225cc8f8aada1a9a70f4fc6707902f2cb..7ed78205afb3c450630378110885ab61af79e962 100644 --- a/src/caosadvancedtools/json_schema_exporter.py +++ b/src/caosadvancedtools/json_schema_exporter.py @@ -39,6 +39,8 @@ class JsonSchemaExporter: name_property_for_new_records: bool = False, description_property_for_new_records: bool = False, additional_options_for_text_props: dict = None, + additional_json_schema: Dict[str, dict] = None, + additional_ui_schema: Dict[str, dict] = None, units_in_description: bool = True, do_not_create: List[str] = None, do_not_retrieve: List[str] = None, @@ -61,6 +63,10 @@ class JsonSchemaExporter: additional_options_for_text_props : dict, optional Dictionary containing additional "pattern" or "format" options for string-typed properties. Optional, default is empty. + additional_json_schema : dict[str, dict], optional + Additional schema content for elements of the given names. + additional_ui_schema : dict[str, dict], optional + Additional ui schema content for elements of the given names. units_in_description : bool, optional Whether to add the unit of a LinkAhead property (if it has any) to the description of the corresponding schema entry. If set to false, an @@ -83,6 +89,10 @@ class JsonSchemaExporter: """ if not additional_options_for_text_props: additional_options_for_text_props = {} + if not additional_json_schema: + additional_json_schema = {} + if not additional_ui_schema: + additional_ui_schema = {} if not do_not_create: do_not_create = [] if not do_not_retrieve: @@ -94,6 +104,8 @@ class JsonSchemaExporter: self._name_property_for_new_records = name_property_for_new_records self._description_property_for_new_records = description_property_for_new_records self._additional_options_for_text_props = additional_options_for_text_props + self._additional_json_schema = additional_json_schema + self._additional_ui_schema = additional_ui_schema self._units_in_description = units_in_description self._do_not_create = do_not_create self._do_not_retrieve = do_not_retrieve @@ -117,11 +129,18 @@ class JsonSchemaExporter: def _make_segment_from_prop(self, prop: db.Property) -> Tuple[OrderedDict, dict]: """Return the JSON Schema and ui schema segments for the given property. + The result may either be a simple json schema segment, such as a `string + <https://json-schema.org/understanding-json-schema/reference/string>`_ element (or another + simple type), a combination such as `anyOf + <https://json-schema.org/understanding-json-schema/reference/combining#anyof>`_ or an `array + <https://json-schema.org/understanding-json-schema/reference/array>`_ element + Parameters ---------- prop : db.Property The property to be transformed. """ + json_prop = OrderedDict() ui_schema: dict = {} if prop.datatype == db.TEXT or prop.datatype == db.DATETIME: text_format = None @@ -139,9 +158,9 @@ class JsonSchemaExporter: # options list. text_format = ["date", "date-time"] - return self._make_text_property(prop.description, text_format, text_pattern), ui_schema + json_prop = self._make_text_property(prop.description, text_format, text_pattern) + return self._customize(json_prop, ui_schema, prop) - json_prop = OrderedDict() if prop.description: json_prop["description"] = prop.description if self._units_in_description and prop.unit: @@ -216,13 +235,31 @@ class JsonSchemaExporter: raise ValueError( f"Unknown or no property datatype. Property {prop.name} with type {prop.datatype}") - return json_prop, ui_schema + return self._customize(json_prop, ui_schema, prop) @staticmethod - def _make_text_property(description="", text_format=None, text_pattern=None): - prop = { + def _make_text_property(description="", text_format=None, text_pattern=None) -> OrderedDict: + """Create a text element. + + Can be a `string <https://json-schema.org/understanding-json-schema/reference/string>`_ + element or an `anyOf + <https://json-schema.org/understanding-json-schema/reference/combining#anyof>`_ combination + thereof. + + Example: + + .. code-block:: json + + { + "type": "string", + "description": "Some description", + "pattern": "[0-9]{2..4}-[0-9]{2-4}", + "format": "hostname", + } + """ + prop: OrderedDict[str, Union[str, list]] = OrderedDict({ "type": "string" - } + }) if description: prop["description"] = description if text_format is not None: @@ -265,6 +302,22 @@ class JsonSchemaExporter: def _make_segment_from_recordtype(self, rt: db.RecordType) -> Tuple[OrderedDict, dict]: """Return Json schema and uischema segments for the given RecordType. + + The result is an element of type `object + <https://json-schema.org/understanding-json-schema/reference/object>`_ and typically + contains more properties: + + .. code-block:: json + + { + "type": "object", + "title": "MyRecordtypeName", + "properties": { + "number": { "type": "number" }, + "street_name": { "type": "string" }, + "street_type": { "enum": ["Street", "Avenue", "Boulevard"] } + } + } """ schema: OrderedDict[str, Any] = OrderedDict({ "type": "object" @@ -299,6 +352,38 @@ class JsonSchemaExporter: return schema, ui_schema + def _customize(self, schema: OrderedDict, ui_schema: dict, entity: db.Entity = None) -> ( + Tuple[OrderedDict, dict]): + """Generic customization method. + +Walk over the available customization stores and apply all applicable ones. No specific order is +guaranteed (as of now). + + Parameters + ---------- + schema, ui_schema : dict + The input schemata. + entity: db.Entity : , optional + An Entity object, may be useful in the future for customizers. + + Returns + ------- + out : Tuple[dict, dict] + The modified input schemata. + """ + + name = schema.get("title", None) + if entity and entity.name: + name = entity.name + for key, add_schema in self._additional_json_schema.items(): + if key == name: + schema.update(add_schema) + for key, add_schema in self._additional_ui_schema.items(): + if key == name: + ui_schema.update(add_schema) + + return schema, ui_schema + def recordtype_to_json_schema(self, rt: db.RecordType, rjsf: bool = False) -> Union[ dict, Tuple[dict, dict]]: """Create a jsonschema from a given RecordType that can be used, e.g., to @@ -327,6 +412,7 @@ class JsonSchemaExporter: schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" if rt.description: schema["description"] = rt.description + schema, inner_uischema = self._customize(schema, inner_uischema, rt) if rjsf: uischema = {} @@ -340,6 +426,8 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T name_property_for_new_records: bool = False, description_property_for_new_records: bool = False, additional_options_for_text_props: Optional[dict] = None, + additional_json_schema: Dict[str, dict] = None, + additional_ui_schema: Dict[str, dict] = None, units_in_description: bool = True, do_not_create: List[str] = None, do_not_retrieve: List[str] = None, @@ -369,6 +457,10 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T additional_options_for_text_props : dict, optional Dictionary containing additional "pattern" or "format" options for string-typed properties. Optional, default is empty. + additional_json_schema : dict[str, dict], optional + Additional schema content for elements of the given names. + additional_ui_schema : dict[str, dict], optional + Additional ui schema content for elements of the given names. units_in_description : bool, optional Whether to add the unit of a LinkAhead property (if it has any) to the description of the corresponding schema entry. If set to false, an @@ -406,6 +498,8 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T name_property_for_new_records=name_property_for_new_records, description_property_for_new_records=description_property_for_new_records, additional_options_for_text_props=additional_options_for_text_props, + additional_json_schema=additional_json_schema, + additional_ui_schema=additional_ui_schema, units_in_description=units_in_description, do_not_create=do_not_create, do_not_retrieve=do_not_retrieve, diff --git a/unittests/test_json_schema_exporter.py b/unittests/test_json_schema_exporter.py index f7e84c38aee30bafc2d0bc80181b444cd3ea11e9..9adccc45db433c8e4df600507704d7b953ecedbc 100644 --- a/unittests/test_json_schema_exporter.py +++ b/unittests/test_json_schema_exporter.py @@ -99,7 +99,7 @@ RT3: elif query_string == "SELECT name, id FROM FILE": return all_files else: - print(f"Query string: {query_string}") + # print(f"Query string: {query_string}") if unique is True: return db.Entity() return db.Container() @@ -929,3 +929,69 @@ RT3: assert array2_ui["items"] == uischema_2 assert (str(array2_ui["items"]) == "{'RT1': {'ui:widget': 'checkboxes', 'ui:options': {'inline': True}}}") + + +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}}")