diff --git a/integrationtests/test_json_schema_exporter.py b/integrationtests/test_json_schema_exporter.py index b80a1aa5e781fd5de5479e3166a4892df1dc2764..69edcf42d1fd285c030ad6d6ccb7f73f2d1b5536 100644 --- a/integrationtests/test_json_schema_exporter.py +++ b/integrationtests/test_json_schema_exporter.py @@ -70,8 +70,8 @@ def test_uniqueness_of_reference_types(): enum_index = 1 - enum_index assert "enum" in one_of[enum_index] assert len(one_of[enum_index]["enum"]) == 2 - assert f"{recA.id}, {recA.name}" in one_of[enum_index]["enum"] - assert f"{recB.id}, {recB.name}" in one_of[enum_index]["enum"] + assert recA.name in one_of[enum_index]["enum"] + assert recB.name in one_of[enum_index]["enum"] assert one_of[1 - enum_index]["type"] == "object" # No properties in parent_type assert len(one_of[1 - enum_index]["properties"]) == 0 diff --git a/src/caosadvancedtools/json_schema_exporter.py b/src/caosadvancedtools/json_schema_exporter.py index f950b06f5d48f27536cbe4760113388df1dda2a1..47821d7225cc8f8aada1a9a70f4fc6707902f2cb 100644 --- a/src/caosadvancedtools/json_schema_exporter.py +++ b/src/caosadvancedtools/json_schema_exporter.py @@ -25,7 +25,7 @@ The scope of this json schema is the automatic generation of user interfaces. """ from collections import OrderedDict -from typing import Any, Dict, Iterable, List, Optional, Union +from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union import linkahead as db from linkahead.common.datatype import get_list_datatype, is_list_datatype @@ -43,6 +43,7 @@ class JsonSchemaExporter: do_not_create: List[str] = None, do_not_retrieve: List[str] = None, no_remote: bool = False, + multiple_choice: List[str] = None, ): """Set up a JsonSchemaExporter, which can then be applied on RecordTypes. @@ -75,6 +76,10 @@ class JsonSchemaExporter: parameter, the behavior is undefined. no_remote : bool If True, do not attempt to connect to a LinkAhead server at all. Default is False. + multiple_choice : list[str], optional + 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. """ if not additional_options_for_text_props: additional_options_for_text_props = {} @@ -82,6 +87,8 @@ class JsonSchemaExporter: do_not_create = [] if not do_not_retrieve: do_not_retrieve = [] + if not multiple_choice: + multiple_choice = [] self._additional_properties = additional_properties self._name_property_for_new_records = name_property_for_new_records @@ -91,6 +98,7 @@ class JsonSchemaExporter: self._do_not_create = do_not_create self._do_not_retrieve = do_not_retrieve self._no_remote = no_remote + self._multiple_choice = multiple_choice @staticmethod def _make_required_list(rt: db.RecordType): @@ -106,15 +114,15 @@ class JsonSchemaExporter: return required_list - def _make_segment_from_prop(self, prop: db.Property): - """Return the JSON Schema segment for the given property + def _make_segment_from_prop(self, prop: db.Property) -> Tuple[OrderedDict, dict]: + """Return the JSON Schema and ui schema segments for the given property. Parameters ---------- prop : db.Property The property to be transformed. """ - + ui_schema: dict = {} if prop.datatype == db.TEXT or prop.datatype == db.DATETIME: text_format = None text_pattern = None @@ -131,7 +139,7 @@ class JsonSchemaExporter: # options list. text_format = ["date", "date-time"] - return self._make_text_property(prop.description, text_format, text_pattern) + return self._make_text_property(prop.description, text_format, text_pattern), ui_schema json_prop = OrderedDict() if prop.description: @@ -154,13 +162,21 @@ class JsonSchemaExporter: json_prop["type"] = "array" list_element_prop = db.Property( name=prop.name, datatype=get_list_datatype(prop.datatype, strict=True)) - json_prop["items"] = self._make_segment_from_prop(list_element_prop) + json_prop["items"], inner_ui_schema = self._make_segment_from_prop(list_element_prop) + if prop.name in self._multiple_choice and prop.name in self._do_not_create: + json_prop["uniqueItems"] = True + ui_schema["ui:widget"] = "checkboxes" + ui_schema["ui:options"] = {"inline": True} + if inner_ui_schema: + ui_schema["items"] = inner_ui_schema elif prop.is_reference(): if prop.datatype == db.REFERENCE: # No Record creation since no RT is specified and we don't know what # schema to use, so only enum of all Records and all Files. values = self._retrieve_enum_values("RECORD") + self._retrieve_enum_values("FILE") 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" @@ -181,7 +197,9 @@ class JsonSchemaExporter: else: rt = db.execute_query(f"FIND RECORDTYPE WITH name='{prop_name}'", unique=True) - subschema = self._make_segment_from_recordtype(rt) + subschema, ui_schema = self._make_segment_from_recordtype(rt) + # if inner_ui_schema: + # ui_schema = inner_ui_schema if values: subschema["title"] = "Create new" json_prop["oneOf"] = [ @@ -198,7 +216,7 @@ class JsonSchemaExporter: raise ValueError( f"Unknown or no property datatype. Property {prop.name} with type {prop.datatype}") - return json_prop + return json_prop, ui_schema @staticmethod def _make_text_property(description="", text_format=None, text_pattern=None): @@ -239,22 +257,26 @@ class JsonSchemaExporter: vals = [] for val in possible_values: if val.name: - vals.append(f"{val.id}, {val.name}") + vals.append(f"{val.name}") else: vals.append(f"{val.id}") return vals - def _make_segment_from_recordtype(self, rt: db.RecordType): - """Return a Json schema segment for the given RecordType. + def _make_segment_from_recordtype(self, rt: db.RecordType) -> Tuple[OrderedDict, dict]: + """Return Json schema and uischema segments for the given RecordType. """ - schema: dict[str, Any] = { + schema: OrderedDict[str, Any] = OrderedDict({ "type": "object" - } + }) + ui_schema = {} schema["required"] = self._make_required_list(rt) schema["additionalProperties"] = self._additional_properties + if rt.name: + schema["title"] = rt.name + props = OrderedDict() if self._name_property_for_new_records: props["name"] = self._make_text_property("The name of the Record to be created") @@ -269,13 +291,16 @@ class JsonSchemaExporter: "Creating a schema for multi-properties is not specified. " f"Property {prop.name} occurs more than once." ) - props[prop.name] = self._make_segment_from_prop(prop) + props[prop.name], inner_ui_schema = self._make_segment_from_prop(prop) + if inner_ui_schema: + ui_schema[prop.name] = inner_ui_schema schema["properties"] = props - return schema + return schema, ui_schema - def recordtype_to_json_schema(self, rt: db.RecordType): + 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 validate a json specifying a record of the given type. @@ -283,22 +308,31 @@ class JsonSchemaExporter: ---------- rt : RecordType The RecordType from which a json schema will be created. + rjsf : bool, optional + If True, uiSchema definitions for react-jsonschema-forms will be output as the second + return value. Default is False Returns ------- schema : dict A dict containing the json schema created from the given RecordType's properties. + + ui_schema : dict, optional + A ui schema. Only if a parameter asks for it (e.g. ``rjsf``). """ if rt is None: raise ValueError( "recordtype_to_json_schema(...) cannot be called with a `None` RecordType.") - schema = self._make_segment_from_recordtype(rt) - schema["$schema"] = "https://json-schema.org/draft/2019-09/schema" - if rt.name: - schema["title"] = rt.name + schema, inner_uischema = self._make_segment_from_recordtype(rt) + schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" if rt.description: schema["description"] = rt.description + if rjsf: + uischema = {} + if inner_uischema: + uischema = inner_uischema + return schema, uischema return schema @@ -310,7 +344,9 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T do_not_create: List[str] = None, do_not_retrieve: List[str] = None, no_remote: bool = False, - ): + multiple_choice: List[str] = None, + rjsf: 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. @@ -338,22 +374,31 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T description of the corresponding schema entry. If set to false, an additional `unit` key is added to the schema itself which is purely annotational and ignored, e.g., in validation. Default is True. - do_not_create : list[str] + do_not_create : list[str], optional A list of RedcordType names, for which there should be no option to create them. Instead, only the choice of existing elements should be given. - do_not_retrieve : list[str] + do_not_retrieve : list[str], optional A list of RedcordType names, for which no Records shall be retrieved. Instead, only an object description should be given. If this list overlaps with the `do_not_create` parameter, the behavior is undefined. - no_remote : bool + no_remote : bool, optional If True, do not attempt to connect to a LinkAhead server at all. Default is False. + multiple_choice : list[str], optional + 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. + rjsf : bool, optional + If True, uiSchema definitions for react-jsonschema-forms will be output as the second return + value. Default is False. Returns ------- schema : dict A dict containing the json schema created from the given RecordType's properties. + ui_schema : dict, optional + A ui schema. Only if a parameter asks for it (e.g. ``rjsf``). """ exporter = JsonSchemaExporter( @@ -365,11 +410,12 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T do_not_create=do_not_create, do_not_retrieve=do_not_retrieve, no_remote=no_remote, + multiple_choice=multiple_choice, ) - return exporter.recordtype_to_json_schema(rt) + return exporter.recordtype_to_json_schema(rt, rjsf=rjsf) -def make_array(schema: dict) -> dict: +def make_array(schema: dict, rjsf_uischema: dict = None) -> Union[dict, Tuple[dict, dict]]: """Create an array of the given schema. The result will look like this: @@ -388,21 +434,33 @@ Parameters schema : dict The JSON schema which shall be packed into an array. +rjsf_uischema : dict, optional + A react-jsonschema-forms ui schema that shall be wrapped as well. + Returns ------- -out : dict +schema : dict A JSON schema dict with a top-level array which contains instances of the given schema. + +ui_schema : dict, optional + The wrapped ui schema. Only returned if ``rjsf_uischema`` is given as parameter. """ result = { "type": "array", "items": schema, - "$schema": "https://json-schema.org/draft/2019-09/schema", + "$schema": "https://json-schema.org/draft/2020-12/schema", } + + if rjsf_uischema is not None: + ui_schema = {"items": rjsf_uischema} + return result, ui_schema return result -def merge_schemas(schemas: Union[Dict[str, dict], Iterable[dict]]) -> dict: +def merge_schemas(schemas: Union[Dict[str, dict], Iterable[dict]], + rjsf_uischemas: Union[Dict[str, dict], Sequence[dict]] = None) -> ( + Union[dict, Tuple[dict, dict]]): """Merge the given schemata into a single schema. The result will look like this: @@ -429,24 +487,45 @@ schemas : dict[str, dict] | Iterable[dict] be used as property names, otherwise the titles of the submitted schemata. If they have no title, numbers will be used as a fallback. Note that even with a dict, the original schema's "title" is not changed. +rjsf_uischemas : dict[str, dict] | Iterable[dict], optional + If given, also merge the react-jsonschema-forms from this argument and return as the second return + value. If ``schemas`` is a dict, this parameter must also be a dict, if ``schemas`` is only an + iterable, this paramater must support numerical indexing. Returns ------- -out : dict +schema : dict A JSON schema dict with a top-level object which contains the given schemata as properties. + +uischema : dict + If ``rjsf_uischemas`` was given, this contains the merged UI schemata. """ sub_schemas: dict[str, dict] = OrderedDict() required = [] + ui_schema = None if isinstance(schemas, dict): sub_schemas = schemas required = [str(k) for k in schemas.keys()] + if rjsf_uischemas is not None: + if not isinstance(rjsf_uischemas, dict): + raise ValueError("Parameter `rjsf_uischemas` must be a dict, because `schemas` is " + f"as well, but it is a {type(rjsf_uischemas)}.") + ui_schema = {k: rjsf_uischemas[k] for k in schemas.keys()} else: for i, schema in enumerate(schemas, start=1): title = schema.get("title", str(i)) sub_schemas[title] = schema required.append(title) + if rjsf_uischemas is not None: + if not isinstance(rjsf_uischemas, Sequence): + raise ValueError("Parameter `rjsf_uischemas` must be a sequence, because `schemas` " + f"is as well, but it is a {type(rjsf_uischemas)}.") + ui_schema = {} + for i, title in enumerate(sub_schemas.keys()): + ui_schema[title] = rjsf_uischemas[i] + # ui_schema = {"index": ui_schema} result = { "type": "object", @@ -456,4 +535,6 @@ out : dict "$schema": "https://json-schema.org/draft/2020-12/schema", } + if ui_schema is not None: + return result, ui_schema return result diff --git a/unittests/test_json_schema_exporter.py b/unittests/test_json_schema_exporter.py index 95601ed9c07d6331d5340d63742d3e8dc5f74570..f7e84c38aee30bafc2d0bc80181b444cd3ea11e9 100644 --- a/unittests/test_json_schema_exporter.py +++ b/unittests/test_json_schema_exporter.py @@ -81,6 +81,19 @@ def _mock_execute_query(query_string, unique=False, **kwargs): 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: + model_str = """ +RT1: +RT21: + obligatory_properties: + RT1: + datatype: LIST<RT1> +RT3: + obligatory_properties: + RT21: + """ + model = parse_model_from_string(model_str) + return model.get_deep("RT21") elif query_string == "SELECT name, id FROM RECORD": return all_records elif query_string == "SELECT name, id FROM FILE": @@ -337,11 +350,11 @@ def test_rt_with_references(): assert "oneOf" not in props["RefProp"] example = { - "RefProp": "101, otherB" + "RefProp": "otherB" } validate(example, schema) example = { - "RefProp": "23, I don't exist" + "RefProp": "I don't exist" } with raises(ValidationError): # Wrong enum value @@ -372,8 +385,8 @@ def test_rt_with_references(): 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 "100, otherA" in props["RefProp"]["oneOf"][enum_index]["enum"] - assert "101, otherB" in props["RefProp"]["oneOf"][enum_index]["enum"] + 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" @@ -395,7 +408,7 @@ def test_rt_with_references(): validate(example, schema) example = { - "RefProp": "101, otherB", + "RefProp": "otherB", "OtherTextProp": "something" } validate(example, schema) @@ -423,17 +436,17 @@ def test_rt_with_references(): assert "description" not in items example = { - "RefProp": "101, otherB" + "RefProp": "otherB" } with raises(ValidationError): # Should be list but isn't validate(example, schema) example = { - "RefProp": ["101, otherB"] + "RefProp": ["otherB"] } validate(example, schema) example = { - "RefProp": ["101, otherB", "102", "104, referencing"] + "RefProp": ["otherB", "102", "referencing"] } validate(example, schema) @@ -461,8 +474,8 @@ def test_rt_with_references(): assert "enum" in items["oneOf"][enum_index] assert isinstance(items["oneOf"][enum_index]["enum"], list) assert len(items["oneOf"][enum_index]["enum"]) == 3 - assert "100, otherA" in items["oneOf"][enum_index]["enum"] - assert "101, otherB" in items["oneOf"][enum_index]["enum"] + 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" @@ -472,7 +485,7 @@ def test_rt_with_references(): assert "IntegerProp" in other_type["required"] example = { - "RefProp": ["101, otherB", "102", "104, referencing"] + "RefProp": ["otherB", "102", "referencing"] } with raises(ValidationError): # Wrong value in enum @@ -488,7 +501,7 @@ def test_rt_with_references(): # we have additional_properties=False which propagates to subschemas validate(example, schema) example = { - "RefProp": [{"IntegerProp": 12}, "101, otherB"] + "RefProp": [{"IntegerProp": 12}, "otherB"] } validate(example, schema) @@ -506,7 +519,7 @@ def test_rt_with_references(): enum_index = 1 - enum_index assert len(ref_ref["oneOf"][enum_index]["enum"]) == 2 assert "103" in ref_ref["oneOf"][enum_index]["enum"] - assert "104, referencing" 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" @@ -520,8 +533,8 @@ def test_rt_with_references(): assert "enum" in items["oneOf"][enum_index] assert isinstance(items["oneOf"][enum_index]["enum"], list) assert len(items["oneOf"][enum_index]["enum"]) == 3 - assert "100, otherA" in items["oneOf"][enum_index]["enum"] - assert "101, otherB" in items["oneOf"][enum_index]["enum"] + 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" @@ -532,7 +545,7 @@ def test_rt_with_references(): example = { "RefRefProp": { "OtherType": [ - "100, otherA", + "otherA", {"IntegerProp": 12} ] } @@ -602,6 +615,7 @@ RT2: "some_date" ], "additionalProperties": true, + "title": "RT1", "properties": { "some_date": { "description": "Just some date", @@ -617,8 +631,7 @@ RT2: ] } }, - "$schema": "https://json-schema.org/draft/2019-09/schema", - "title": "RT1", + "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "some description" }""" # Second test: with reference @@ -630,6 +643,7 @@ RT2: "RT1" ], "additionalProperties": true, + "title": "RT2", "properties": { "RT1": { "description": "some description", @@ -638,7 +652,7 @@ RT2: "title": "Existing entries", "enum": [ "103", - "104, referencing" + "referencing" ] }, { @@ -647,6 +661,7 @@ RT2: "some_date" ], "additionalProperties": true, + "title": "Create new", "properties": { "some_date": { "description": "Just some date", @@ -661,14 +676,12 @@ RT2: } ] } - }, - "title": "Create new" + } } ] } }, - "$schema": "https://json-schema.org/draft/2019-09/schema", - "title": "RT2" + "$schema": "https://json-schema.org/draft/2020-12/schema" }""" # Third test: Reference prop shall be only existing references, no option to create new ones. @@ -679,17 +692,17 @@ RT2: "RT1" ], "additionalProperties": true, + "title": "RT2", "properties": { "RT1": { "description": "some description", "enum": [ "103", - "104, referencing" + "referencing" ] } }, - "$schema": "https://json-schema.org/draft/2019-09/schema", - "title": "RT2" + "$schema": "https://json-schema.org/draft/2020-12/schema" }""" @@ -820,6 +833,99 @@ RT3: assert (schema_noexist_noremote["properties"]["NoRecords"].get("properties") == OrderedDict([('some_text', {'type': 'string'})])) - schema_noexist_noretrieve = rtjs(model.get_deep("RT2"), do_not_retrieve=["RT1"]) + 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:options': {'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:options': {'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:options': {'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:options': {'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:options': {'inline': True}}}")