diff --git a/src/caosadvancedtools/json_schema_exporter.py b/src/caosadvancedtools/json_schema_exporter.py index 7ed78205afb3c450630378110885ab61af79e962..801a4861d2f6bd1cc16f1b66cffa1bb9c9c5b223 100644 --- a/src/caosadvancedtools/json_schema_exporter.py +++ b/src/caosadvancedtools/json_schema_exporter.py @@ -46,6 +46,7 @@ class JsonSchemaExporter: do_not_retrieve: List[str] = None, no_remote: bool = False, multiple_choice: List[str] = None, + wrap_files_in_objects: bool = False, ): """Set up a JsonSchemaExporter, which can then be applied on RecordTypes. @@ -86,6 +87,13 @@ class JsonSchemaExporter: A list of reference Property names which shall be denoted as multiple choice properties. This means that each option in this property may be selected at most once. This is not implemented yet if the Property is not in ``do_not_create`` as well. + wrap_files_in_objects : bool, optional + Whether (lists of) files should be wrapped into an array of objects + that have a file property. The sole purpose of this wrapping is to + provide a workaround for a `react-jsonschema-form + bug<https://github.com/rjsf-team/react-jsonschema-form/issues/3957>`_ + so only set this to True if you're using the exported schema with + react-json-form and you are experiencing the bug. Default is False. """ if not additional_options_for_text_props: additional_options_for_text_props = {} @@ -111,6 +119,7 @@ class JsonSchemaExporter: self._do_not_retrieve = do_not_retrieve self._no_remote = no_remote self._multiple_choice = multiple_choice + self._wrap_files_in_objects = wrap_files_in_objects @staticmethod def _make_required_list(rt: db.RecordType): @@ -177,7 +186,9 @@ class JsonSchemaExporter: json_prop["type"] = "integer" elif prop.datatype == db.DOUBLE: json_prop["type"] = "number" - elif is_list_datatype(prop.datatype): + elif is_list_datatype(prop.datatype) and not ( + self._wrap_files_in_objects and get_list_datatype(prop.datatype, + strict=True) == db.FILE): json_prop["type"] = "array" list_element_prop = db.Property( name=prop.name, datatype=get_list_datatype(prop.datatype, strict=True)) @@ -196,9 +207,39 @@ class JsonSchemaExporter: json_prop["enum"] = values if prop.name in self._multiple_choice: json_prop["uniqueItems"] = True - elif prop.datatype == db.FILE: - json_prop["type"] = "string" - json_prop["format"] = "data-url" + elif prop.datatype == db.FILE or ( + self._wrap_files_in_objects and + is_list_datatype(prop.datatype) and + get_list_datatype(prop.datatype, strict=True) == db.FILE + ): + if self._wrap_files_in_objects: + # Workaround for react-jsonschema-form bug + # https://github.com/rjsf-team/react-jsonschema-form/issues/3957: + # Wrap all FILE references (regardless whether lists or + # scalars) in an array of objects that have a file property, + # since objects can be deleted, files can't. + json_prop["type"] = "array" + json_prop["items"] = { + "type": "object", + "title": "Next file", + # The wrapper object must wrap a file and can't be empty. + "required": ["file"], + # Wrapper objects must only contain the wrapped file. + "additionalProperties": False, + "properties": { + "file": { + "title": "Enter your file.", + "type": "string", + "format": "data-url" + } + } + } + if not is_list_datatype(prop.datatype): + # Scalar file, so the array has maximum length 1 + json_prop["maxItems"] = 1 + else: + json_prop["type"] = "string" + json_prop["format"] = "data-url" else: prop_name = prop.datatype if isinstance(prop.datatype, db.Entity): @@ -434,6 +475,7 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T no_remote: bool = False, multiple_choice: List[str] = None, rjsf: bool = False, + wrap_files_in_objects: bool = False ) -> Union[dict, Tuple[dict, 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. @@ -483,6 +525,14 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T rjsf : bool, optional If True, uiSchema definitions for react-jsonschema-forms will be output as the second return value. Default is False. + wrap_files_in_objects : bool, optional + Whether (lists of) files should be wrapped into an array of objects that + have a file property. The sole purpose of this wrapping is to provide a + workaround for a `react-jsonschema-form + bug<https://github.com/rjsf-team/react-jsonschema-form/issues/3957>`_ so + only set this to True if you're using the exported schema with + react-json-form and you are experiencing the bug. Default is False. + Returns ------- @@ -505,6 +555,7 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T do_not_retrieve=do_not_retrieve, no_remote=no_remote, multiple_choice=multiple_choice, + wrap_files_in_objects=wrap_files_in_objects ) return exporter.recordtype_to_json_schema(rt, rjsf=rjsf) diff --git a/unittests/test_json_schema_exporter.py b/unittests/test_json_schema_exporter.py index 9adccc45db433c8e4df600507704d7b953ecedbc..5b3cd41635a0ecef6a3966fedaa20da5109a3cbd 100644 --- a/unittests/test_json_schema_exporter.py +++ b/unittests/test_json_schema_exporter.py @@ -331,10 +331,6 @@ def test_rt_with_list_props(): @patch("linkahead.execute_query", new=Mock(side_effect=_mock_execute_query)) def test_rt_with_references(): - """References and lists of references to files will come later, so test if - the errors are thrown correctly. - - """ rt = db.RecordType() rt.add_property(name="RefProp", datatype=db.REFERENCE) @@ -560,6 +556,21 @@ def test_rt_with_references(): 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)) @@ -568,6 +579,22 @@ def test_rt_with_references(): 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():