diff --git a/aas_test_engines/_file/generate.py b/aas_test_engines/_file/generate.py index befdcc4..b4e70ac 100644 --- a/aas_test_engines/_file/generate.py +++ b/aas_test_engines/_file/generate.py @@ -3,11 +3,7 @@ from fences.json_schema.parse import parse from fences.core.node import Node as FlowGraph - def generate_graph(schema) -> FlowGraph: - # TODO: remove these after fixing fences.core.exception.InternalException: Decision without valid leaf detected - del schema['$defs']['AssetInformation']['allOf'][1]['properties']['specificAssetIds']['items']['allOf'][0] - del schema['$defs']['Entity']['allOf'][1] schema_norm = normalize(schema, False) config = default_config() config.normalize = False diff --git a/aas_test_engines/_util.py b/aas_test_engines/_util.py new file mode 100644 index 0000000..fbfdfcc --- /dev/null +++ b/aas_test_engines/_util.py @@ -0,0 +1,53 @@ +from typing import Dict, List + +def _group(elements: list) -> dict: + assert isinstance(elements, list) + grouped: Dict[str, List[any]] = {} + for item in elements: + assert isinstance(item, dict) + key = item.get('idShort') + assert isinstance(key, str) + try: + grouped[key].append(item) + except KeyError: + grouped[key] = [item] + return grouped + +def group(data: any) -> any: + if isinstance(data, list): + data = [group(i) for i in data] + elif isinstance(data, dict): + data: dict = {key: group(value) for key, value in data.items()} + if data.get('modelType') == 'SubmodelElementCollection': + elements = data.get('value') + data['value'] = _group(elements) + elif data.get('modelType') == 'Submodel': + elements = data.get('submodelElements') + data['submodelElements'] = _group(elements) + return data + +def _un_group(elements: dict) -> list: + if not isinstance(elements, dict): + return elements + un_grouped: List[str, dict] = [] + for item in elements.values(): + if isinstance(item, list): + un_grouped.extend(item) + else: + un_grouped.append(item) + return un_grouped + +def un_group(data: any) -> any: + if isinstance(data, list): + data = [un_group(i) for i in data] + for idx, value in enumerate(data): + data[idx] = un_group(value) + elif isinstance(data, dict): + data = {key: un_group(value) for key, value in data.items()} + if data.get('modelType') == 'SubmodelElementCollection': + elements = data.get('value') + data['value'] = _un_group(elements) + elif data.get('modelType') == 'Submodel': + elements = data.get('submodelElements') + data['submodelElements'] = _un_group(elements) + return data diff --git a/aas_test_engines/data/file/3.0.yml b/aas_test_engines/data/file/3.0.yml index 4921bff..5f3394b 100644 --- a/aas_test_engines/data/file/3.0.yml +++ b/aas_test_engines/data/file/3.0.yml @@ -1430,15 +1430,157 @@ $defs: idShort: const: ContactInformations submodelElements: - contains: - $ref: '#/$defs/Internal/ContactInformationEntry' + groupBy: idShort + required: + - ContactInformation + properties: + ContactInformation: + items: + $ref: '#/$defs/Internal/ContactInformationEntry' + minItems: 1 + DigitalNameplate: + required: + - submodels + properties: + submodels: + contains: + required: + - submodelElements + - idShort + properties: + idShort: + const: DigitalNameplate + submodelElements: + groupBy: idShort + required: + - URIOfTheProduct + - ManufacturerName + - ManufacturerProductDesignation + - ContactInformation + - YearOfConstruction + anyOf: + - required: + - ManufacturerProductFamily + - required: + - ManufacturerProductType + properties: + URIOfTheProduct: + minItems: 1 + maxItems: 1 + items: + properties: + modelType: + const: Property + valueType: + const: xs:string + ManufacturerName: + minItems: 1 + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + ManufacturerProductDesignation: + minItems: 1 + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + ContactInformation: + minItems: 1 + maxItems: 1 + items: + $ref: '#/$defs/Internal/ContactInformationEntry' + ManufacturerProductRoot: + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + ManufacturerProductFamily: + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + ManufacturerProductType: + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + OrderCodeOfManufacturer: + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + ProductArticleNumberOfManufacturer: + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + SerialNumber: + maxItems: 1 + items: + properties: + modelType: + const: Property + valueType: + const: xs:string + YearOfConstruction: + minItems: 1 + maxItems: 1 + items: + properties: + modelType: + const: Property + valueType: + const: xs:string + DateOfManufacture: + maxItems: 1 + items: + properties: + modelType: + const: Property + valueType: + const: xs:date + HardwareVersion: + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + FirmwareVersion: + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + SoftwareVersion: + maxItems: 1 + items: + properties: + modelType: + const: MultiLanguageProperty + CountryOfOrigin: + maxItems: 1 + items: + properties: + modelType: + const: Property + valueType: + const: xs:string Internal: ContactInformationEntry: - $ref: '#/$defs/SubmodelElementCollection' required: - idShort - value properties: + modelType: + const: SubmodelElementCollection idShort: const: ContactInformation value: @@ -1446,8 +1588,9 @@ $defs: properties: RoleOfContactPerson: items: - $ref: '#/$defs/Property' properties: + modelType: + const: Property valueType: const: xs:string value: @@ -1460,31 +1603,41 @@ $defs: maxItems: 1 NationalCode: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 Language: items: - $ref: '#/$defs/Property' properties: + modelType: + const: Property valueType: const: xs:string TimeZone: items: - $ref: '#/$defs/Property' properties: + modelType: + const: Property valueType: const: xs:string CityTown: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 Company: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 Department: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 Phone: items: @@ -1504,169 +1657,218 @@ $defs: maxItems: 1 Street: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 Zipcode: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 POBox: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 ZipCodeOfPOBox: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 StateCounty: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 NameOfContact: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 FirstName: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 MiddleName: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 Title: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 AcademicTitle: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 FurtherDetailsOfContact: items: - $ref: '#/$defs/MultiLanguageProperty' + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 AddressOfAdditionalLink: items: - $ref: '#/$defs/Property' properties: + modelType: + const: Property valueType: const: xs:string maxItems: 1 Phone: - $ref: '#/$defs/SubmodelElementCollection' - value: - groupBy: idShort - required: - TelephoneNumber - properties: - TelephoneNumber: - items: - $ref: '#/$defs/MultiLanguageProperty' - minItems: 1 - maxItems: 1 - TypeOfTelephone: - items: - $ref: '#/$defs/Property' - properties: - valueType: - const: xs:string - value: - enum: - - 0173-1#07-AAS754#001 - - 0173-1#07-AAS755#001 - - 0173-1#07-AAS756#001 - - 0173-1#07-AAS757#001 - - 0173-1#07-AAS758#001 - - 0173-1#07-AAS759#001 - maxItems: 1 - AvailableTime: - items: - $ref: '#/$defs/MultiLanguageProperty' - maxItems: 1 + properties: + modelType: + const: SubmodelElementCollection + value: + groupBy: idShort + required: + - TelephoneNumber + properties: + TelephoneNumber: + items: + properties: + modelType: + const: MultiLanguageProperty + minItems: 1 + maxItems: 1 + TypeOfTelephone: + items: + properties: + modelType: + const: Property + valueType: + const: xs:string + value: + enum: + - 0173-1#07-AAS754#001 + - 0173-1#07-AAS755#001 + - 0173-1#07-AAS756#001 + - 0173-1#07-AAS757#001 + - 0173-1#07-AAS758#001 + - 0173-1#07-AAS759#001 + maxItems: 1 + AvailableTime: + items: + properties: + modelType: + const: MultiLanguageProperty + maxItems: 1 Fax: - $ref: '#/$defs/SubmodelElementCollection' - value: - groupBy: idShort - required: - - FaxNumber - properties: - FaxNumber: - items: - $ref: '#/$defs/MultiLanguageProperty' - minItems: 1 - maxItems: 1 - TypeOfFaxNumber: - items: - $ref: '#/$defs/Property' - properties: - valueType: - const: xs:string - value: - enum: - - 0173-1#07-AAS754#001 - - 0173-1#07-AAS756#001 - - 0173-1#07-AAS758#001 + properties: + modelType: + const: SubmodelElementCollection + value: + groupBy: idShort + required: + - FaxNumber + properties: + FaxNumber: + items: + properties: + modelType: + const: MultiLanguageProperty + minItems: 1 maxItems: 1 + TypeOfFaxNumber: + items: + properties: + modelType: + const: Property + valueType: + const: xs:string + value: + enum: + - 0173-1#07-AAS754#001 + - 0173-1#07-AAS756#001 + - 0173-1#07-AAS758#001 + maxItems: 1 Email: - $ref: '#/$defs/SubmodelElementCollection' - value: - groupBy: idShort - required: - - EmailAddress - properties: - EmailAddress: - items: - $ref: '#/$defs/Property' - properties: - valueType: - const: xs:string - minItems: 1 - maxItems: 1 - PublicKey: - items: - $ref: '#/$defs/MultiLanguageProperty' - maxItems: 1 - TypeOfFEmailAddress: - items: - $ref: '#/$defs/Property' - properties: - valueType: - const: xs:string - value: - enum: - - 0173-1#07-AAS754#001 - - 0173-1#07-AAS756#001 - - 0173-1#07-AAS757#001 - - 0173-1#07-AAS758#001 - maxItems: 1 - TypeOfPublicKey: - items: - $ref: '#/$defs/MultiLanguageProperty' - maxItems: 1 - IPCommunication: - $ref: '#/$defs/SubmodelElementCollection' - value: - groupBy: idShort - required: - - AddressOfAdditionalLink - properties: - AddressOfAdditionalLink: - items: - $ref: '#/$defs/Property' - properties: - valueType: - const: xs:string + properties: + modelType: + const: SubmodelElementCollection + value: + groupBy: idShort + required: + - EmailAddress + properties: + EmailAddress: + items: + properties: + modelType: + const: Property + valueType: + const: xs:string minItems: 1 maxItems: 1 - TypeOfCommunicatin: - items: - $ref: '#/$defs/Property' - properties: - valueType: - const: xs:string + PublicKey: + items: + properties: + modelType: + const: MultiLanguageProperty + maxItems: 1 + TypeOfFEmailAddress: + items: + properties: + modelType: + const: Property + valueType: + const: xs:string + value: + enum: + - 0173-1#07-AAS754#001 + - 0173-1#07-AAS756#001 + - 0173-1#07-AAS757#001 + - 0173-1#07-AAS758#001 + maxItems: 1 + TypeOfPublicKey: + items: + properties: + modelType: + const: MultiLanguageProperty + maxItems: 1 + IPCommunication: + properties: + modelType: + const: SubmodelElementCollection + value: + groupBy: idShort + required: + - AddressOfAdditionalLink + properties: + AddressOfAdditionalLink: + items: + properties: + modelType: + const: Property + valueType: + const: xs:string + minItems: 1 + maxItems: 1 + TypeOfCommunicatin: + items: + properties: + modelType: + const: Property + valueType: + const: xs:string + maxItems: 1 + AvailableTime: + items: + properties: + modelType: + const: MultiLanguageProperty maxItems: 1 - AvailableTime: - items: - $ref: '#/$defs/MultiLanguageProperty' - maxItems: 1 diff --git a/aas_test_engines/file.py b/aas_test_engines/file.py index e15ac5c..dcaaf11 100755 --- a/aas_test_engines/file.py +++ b/aas_test_engines/file.py @@ -14,6 +14,8 @@ from json_schema_tool.exception import PreprocessorException import zipfile +from ._util import un_group + JSON = Union[str, int, float, bool, None, Dict[str, Any], List[Any]] @@ -35,6 +37,9 @@ def _find_schemas() -> Dict[str, AasSchema]: if not i.endswith('.yml'): continue schema = safe_load(open(path, "rb")) + # TODO: remove these after fixing fences.core.exception.InternalException: Decision without valid leaf detected + del schema['$defs']['AssetInformation']['allOf'][1]['properties']['specificAssetIds']['items']['allOf'][0] + del schema['$defs']['Entity']['allOf'][1] config = ParseConfig( format_validators=validators ) @@ -92,7 +97,7 @@ def check_json_data(data: any, version: str = _DEFAULT_VERSION, submodel_templat result = AasTestResult('Check JSON', '', Level.INFO) error = schema.validator.validate(data) _map_error(result, error) - if submodel_templates: + if submodel_templates and result.ok(): def preprocess(data: ElementTree.Element, validator: SchemaValidator) -> JSON: try: @@ -367,9 +372,13 @@ def generate(version: str = _DEFAULT_VERSION, submodel_template: Optional[str] = if submodel_template is None: aas = _get_schema(version, set()) graph = generate_graph(aas.schema) + for i in graph.generate_paths(): + sample = graph.execute(i.path) + yield json.dumps(sample) else: aas = _get_schema(version, set([submodel_template])) graph = generate_graph(aas.submodel_schemas[submodel_template]) - for i in graph.generate_paths(): - sample = graph.execute(i.path) - yield json.dumps(sample) + for i in graph.generate_paths(): + sample = graph.execute(i.path) + sample = un_group(sample) + yield json.dumps(sample, indent=4) diff --git a/test/fixtures/submodel_templates/digital_nameplate.json b/test/fixtures/submodel_templates/digital_nameplate.json new file mode 100644 index 0000000..f481a5a --- /dev/null +++ b/test/fixtures/submodel_templates/digital_nameplate.json @@ -0,0 +1,203 @@ +{ + "submodels": [ + { + "id": "example", + "idShort": "DigitalNameplate", + "modelType": "Submodel", + "semanticId": { + "keys": [ + { + "type": "ConceptDescription", + "value": "https://admin-shell.io/zvei/nameplate/2/0/Nameplate" + } + ], + "type": "ExternalReference" + }, + "submodelElements": [ + { + "modelType": "Property", + "valueType": "xs:string", + "value": "https://www.domain-abc.com/Model-Nr-1234/Serial-Nr-5678", + "idShort": "URIOfTheProduct" + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "ManufacturerName", + "value": [ + { + "language": "de", + "text": "Muster AG" + } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "ManufacturerProductDesignation", + "value": [ + { + "language": "en", + "text": "ABC-123" + } + ] + }, + { + "modelType": "SubmodelElementCollection", + "idShort": "ContactInformation", + "value": [ + { + "modelType": "Property", + "idShort": "RoleOfContactPerson", + "valueType": "xs:string", + "value": "0173-1#07-AAS931#001" + } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "ManufacturerProductRoot", + "value": [ + { + "language": "en", + "text": "flow meter" + } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "ManufacturerProductFamily", + "value": [ + { + "language": "en", + "text": "Type ABC" + } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "ManufacturerProductType", + "value": [ + { + "language": "en", + "text": "FM-ABC-1234" + } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "OrderCodeOfManufacturer", + "value": [ + { + "language": "en", + "text": "FMABC1234" + } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "ProductArticleNumberOfManufacturer", + "value": [ + { + "language": "en", + "text": "FM11-ABC22-123456" + } + ] + }, + { + "modelType": "Property", + "idShort": "SerialNumber", + "value": "12345678", + "valueType": "xs:string" + }, + { + "modelType": "Property", + "idShort": "YearOfConstruction", + "value": "2022", + "valueType": "xs:string" + }, + { + "modelType": "Property", + "idShort": "DateOfManufacture", + "value": "2022-01-01", + "valueType": "xs:date" + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "HardwareVersion", + "value": [ + { + "language": "en", + "text": "1.0.0" + } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "FirmwareVersion", + "value": [ + { + "language": "en", + "text": "1.0" + } + ] + }, + { + "modelType": "MultiLanguageProperty", + "idShort": "SoftwareVersion", + "value": [ + { + "language": "en", + "text": "1.0.0" + } + ] + }, + { + "modelType": "Property", + "idShort": "CountryOfOrigin", + "value": "DE", + "valueType": "xs:string" + }, + { + "modelType": "File", + "idShort": "CompanyLogo", + "contentType": "image/png" + }, + { + "modelType": "SubmodelElementCollection", + "idShort": "Markings", + "value": [ + { + "modelType": "SubmodelElementCollection", + "idShort": "Marking", + "value": [ + { + "modelType": "Property", + "idShort": "MarkingName", + "valueType": "xs:string", + "value": "0173-1#07-DAA603#004" + } + ] + } + ] + }, + { + "modelType": "SubmodelElementCollection", + "idShort": "AssetSpecificProperties", + "value": [ + { + "modelType": "SubmodelElementCollection", + "idShort": "GuidelineSpecificProperties", + "value": [ + { + "modelType": "Property", + "idShort": "MarkingName", + "valueType": "xs:string", + "value": "GuidelineForConformityDeclaration" + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/test_file.py b/test/test_file.py index e2866b4..b9589b9 100644 --- a/test/test_file.py +++ b/test/test_file.py @@ -132,21 +132,25 @@ def test_invoke(self): class GenerateTest(TestCase): - def check(self, generator): + def check(self, generator, limit = float('inf')): i = 0 for _ in generator: i += 1 - if i > 10: + if i > limit: break def test_meta_model(self): generator = file.generate() - self.check(generator) + self.check(generator, 10) def test_generate_contact_info(self): generator = file.generate(submodel_template='ContactInformation') self.check(generator) + def test_generate_digital_nameplate(self): + generator = file.generate(submodel_template='DigitalNameplate') + self.check(generator) + def test_invalid_name(self): with self.assertRaises(exception.AasTestToolsException): generator = file.generate(submodel_template="xyz") @@ -167,6 +171,22 @@ def test_contact_info(self): result.dump() self.assertFalse(result.ok()) + def test_digital_nameplate(self): + templates = set(['DigitalNameplate']) + with open(os.path.join(script_dir, 'fixtures', 'submodel_templates', 'digital_nameplate.json')) as f: + data = json.load(f) + result = file.check_json_data(data, submodel_templates=templates) + result.dump() + self.assertTrue(result.ok()) + # either family or type must be present + elements = data['submodels'][0]['submodelElements'] + indices = [idx for idx, value in enumerate(elements) if value['idShort'] in ['ManufacturerProductFamily', 'ManufacturerProductType']] + for idx in indices: + elements[idx]['idShort'] = 'invalid' + result = file.check_json_data(data, submodel_templates=templates) + result.dump() + self.assertFalse(result.ok()) + def test_no_submodels(self): data = {} # is compliant to meta-model... diff --git a/test/test_util.py b/test/test_util.py new file mode 100644 index 0000000..eba3d0c --- /dev/null +++ b/test/test_util.py @@ -0,0 +1,77 @@ +from unittest import TestCase +from aas_test_engines._util import group, un_group + + +class GroupSmcTest(TestCase): + + def test_empty(self): + result = group({}) + self.assertDictEqual(result, {}) + result = un_group({}) + self.assertDictEqual(result, {}) + + def test_simple(self): + input = { + 'modelType': 'SubmodelElementCollection', + 'value': [ + { + 'idShort': 'x' + } + ] + } + result = group(input) + self.assertDictEqual(result, { + 'modelType': 'SubmodelElementCollection', + 'value': { + 'x': [{'idShort': 'x'}] + } + }) + output = un_group(result) + self.assertDictEqual(input, output) + + def test_multiple(self): + input = { + 'modelType': 'SubmodelElementCollection', + 'value': [ + { + 'idShort': 'x', + 'x': 1 + }, + { + 'idShort': 'z', + 'z': 2 + }, + { + 'idShort': 'x', + 'x': 3 + } + ] + } + result = group(input) + self.assertDictEqual(result, { + 'modelType': 'SubmodelElementCollection', + 'value': { + 'x': [{'idShort': 'x', 'x': 1}, {'idShort': 'x', 'x': 3}], + 'z': [{'idShort': 'z', 'z': 2}] + } + }) + output = un_group(result) + self.assertEqual(output['modelType'], 'SubmodelElementCollection') + self.assertEqual(len(output['value']), 3) + + def test_nested(self): + input = { + 'modelType': 'SubmodelElementCollection', + 'value': [ + { + 'modelType': 'SubmodelElementCollection', + 'idShort': 'foo', + 'value': [ + { + 'idShort': 'x' + } + ] + } + ] + } + result = group(input)