From d55e10652b1996b966e9184d1559b80dc8c9ab07 Mon Sep 17 00:00:00 2001 From: Martin Stemmer <52048213+Martin187187@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:12:10 +0200 Subject: [PATCH 1/3] align api to security spec --- .gitignore | 2 + Part2-API-Schemas/openapi.yaml | 4 +- .../modules/ROOT/pages/changelog.adoc | 2 +- .../test/query/test_field_identifier_regex.py | 65 +++- .../test_fragment_field_identifier_regex.py | 11 + .../test/query/test_query_json_schema.py | 212 +++++++++++ .../modules/ROOT/pages/json-grammar.txt | 15 +- .../modules/ROOT/pages/schema.adoc | 79 ++-- .../modules/ROOT/partials/bnf/grammar.bnf | 30 +- .../ROOT/partials/query-json-schema.json | 85 ++++- tools/validate_spec_artifacts.py | 344 ++++++++++++++++++ 11 files changed, 786 insertions(+), 63 deletions(-) create mode 100644 .gitignore create mode 100644 documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_query_json_schema.py create mode 100644 tools/validate_spec_artifacts.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7a60b85e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/Part2-API-Schemas/openapi.yaml b/Part2-API-Schemas/openapi.yaml index 7b62a86f..29ae98a7 100644 --- a/Part2-API-Schemas/openapi.yaml +++ b/Part2-API-Schemas/openapi.yaml @@ -712,11 +712,11 @@ components: FieldIdentifier: type: string pattern: >- - ^(?:\$aas#(?:idShort|id|assetInformation\.assetKind|assetInformation\.assetType|assetInformation\.globalAssetId|assetInformation\.specificAssetIds\[(?:0|[1-9][0-9]*)\]\.(?:name|value|externalSubjectId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?)|submodels\[(?:0|[1-9][0-9]*)\]\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))|\$sm#(?:semanticId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)\])?(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?|idShort|id)|\$sme(?:\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\[(?:0|[1-9][0-9]*)\])*(?:\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\[(?:0|[1-9][0-9]*)\])*)*)?#(?:semanticId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)\])?(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?|idShort|value|valueType|language)|\$cd#(?:idShort|id)|\$aasdesc#(?:idShort|id|assetKind|assetType|globalAssetId|specificAssetIds\[(?:0|[1-9][0-9]*)\]\.(?:name|value|externalSubjectId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?)|endpoints\[(?:0|[1-9][0-9]*)\]\.(?:interface|protocolinformation\.href)|submodelDescriptors\[(?:0|[1-9][0-9]*)\]\.(?:semanticId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)\])?(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?|idShort|id|endpoints\[(?:0|[1-9][0-9]*)\]\.(?:interface|protocolinformation\.href)))|\$smdesc#(?:semanticId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)\])?(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)\]\.(?:type|value)))?|idShort|id|endpoints\[(?:0|[1-9][0-9]*)\]\.(?:interface|protocolinformation\.href)))$ + ^(?:\$aas#(?:idShort|id|assetInformation\.assetKind|assetInformation\.assetType|assetInformation\.globalAssetId|assetInformation\.specificAssetIds\[(?:0|[1-9][0-9]*)?\]\.(?:name|value|externalSubjectId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?)|submodels\[(?:0|[1-9][0-9]*)?\](?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?)|\$sm#(?:semanticId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)?\])?(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?|idShort|id)|\$sme(?:\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\[(?:0|[1-9][0-9]*)?\])*(?:\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\[(?:0|[1-9][0-9]*)?\])*)*)?#(?:semanticId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)?\])?(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?|idShort|value|valueType|language)|\$cd#(?:idShort|id)|\$aasdesc#(?:idShort|id|assetKind|assetType|globalAssetId|specificAssetIds\[(?:0|[1-9][0-9]*)?\]\.(?:name|value|externalSubjectId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?)|endpoints\[(?:0|[1-9][0-9]*)?\]\.(?:interface|protocolinformation\.href)|submodelDescriptors\[(?:0|[1-9][0-9]*)?\]\.(?:semanticId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)?\])?(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?|idShort|id|endpoints\[(?:0|[1-9][0-9]*)?\]\.(?:interface|protocolinformation\.href)))|\$smdesc#(?:semanticId(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)?\])?(?:\.(?:type|keys\[(?:0|[1-9][0-9]*)?\]\.(?:type|value)))?|idShort|id|endpoints\[(?:0|[1-9][0-9]*)?\]\.(?:interface|protocolinformation\.href)))$ FragmentFieldIdentifier: type: string pattern: >- - ^(?:\$aas#(?:idShort|assetInformation\.assetType|assetInformation\.globalAssetId|assetInformation\.specificAssetIds(?:\[[0-9]*\](?:\.externalSubjectId(?:\.keys(?:\[[0-9]*\])?)?)?)?|submodels(?:\[[0-9]*\](?:\.keys(?:\[[0-9]*\])?)?)?)|\$sm#(?:semanticId(?:\.keys(?:\[[0-9]*\])?)?|supplementalSemanticIds(?:\[[0-9]*\](?:\.keys(?:\[[0-9]*\])?)?)?|idShort)|\$sme(?:\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\[[0-9]*\])*(?:\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\[[0-9]*\])*)*)?(?:#(?:semanticId(?:\.keys(?:\[[0-9]*\])?)?|supplementalSemanticIds(?:\[[0-9]*\](?:\.keys(?:\[[0-9]*\])?)?)?|idShort|value|valueType|language))?|\$cd#idShort|\$aasdesc#(?:idShort|description|displayName|extension|administration|assetKind|assetType|globalAssetId|specificAssetIds(?:\[[0-9]*\](?:\.externalSubjectId(?:\.keys(?:\[[0-9]*\])?)?)?)?|endpoints(?:\[[0-9]*\])?|submodelDescriptors(?:\[[0-9]*\](?:\.(?:semanticId(?:\.keys(?:\[[0-9]*\])?)?|supplementalSemanticIds(?:\[[0-9]*\](?:\.keys(?:\[[0-9]*\])?)?)?|idShort|endpoints(?:\[[0-9]*\])?))?)?)|\$smdesc#(?:semanticId(?:\.keys(?:\[[0-9]*\])?)?|supplementalSemanticIds(?:\[[0-9]*\](?:\.keys(?:\[[0-9]*\])?)?)?|idShort|endpoints(?:\[[0-9]*\])?))$ + ^(?:\$aas#(?:idShort|assetInformation\.assetType|assetInformation\.globalAssetId|assetInformation\.specificAssetIds(?:\[(?:0|[1-9][0-9]*)?\](?:\.externalSubjectId(?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?)?)?|submodels(?:\[(?:0|[1-9][0-9]*)?\](?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?)?)|\$sm#(?:semanticId(?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)?\](?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?)?|idShort)|\$sme(?:\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\[(?:0|[1-9][0-9]*)?\])*(?:\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\[(?:0|[1-9][0-9]*)?\])*)*)?(?:#(?:semanticId(?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)?\](?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?)?|idShort|value|valueType|language))?|\$cd#idShort|\$aasdesc#(?:idShort|description|displayName|extension|administration|assetKind|assetType|globalAssetId|specificAssetIds(?:\[(?:0|[1-9][0-9]*)?\](?:\.externalSubjectId(?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?)?)?|endpoints(?:\[(?:0|[1-9][0-9]*)?\])?|submodelDescriptors(?:\[(?:0|[1-9][0-9]*)?\](?:\.(?:semanticId(?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)?\](?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?)?|idShort|endpoints(?:\[(?:0|[1-9][0-9]*)?\])?))?)?)|\$smdesc#(?:semanticId(?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?|supplementalSemanticIds(?:\[(?:0|[1-9][0-9]*)?\](?:\.keys(?:\[(?:0|[1-9][0-9]*)?\])?)?)?|idShort|endpoints(?:\[(?:0|[1-9][0-9]*)?\])?))$ MultiLanguagePropertyMetadata: allOf: - $ref: "#/components/schemas/SubmodelElementAttributes" diff --git a/documentation/IDTA-01002-3/modules/ROOT/pages/changelog.adoc b/documentation/IDTA-01002-3/modules/ROOT/pages/changelog.adoc index 088578e0..0906fbc4 100644 --- a/documentation/IDTA-01002-3/modules/ROOT/pages/changelog.adoc +++ b/documentation/IDTA-01002-3/modules/ROOT/pages/changelog.adoc @@ -37,6 +37,7 @@ Minor Changes: * docs: Added cross-spec terminology matrix reference to align Metamodel classes, API path segments, and Query/Access-Rule prefixes across specifications. * docs: Added cross-specification alignment matrix to document API compatibility with IDTA-01001 (Metamodel v3.2 instead of v3.1), DTA-01003-a, IDTA-01003-b, IDTA-01004 (Security v3.1), and IDTA-01005 (from v3.1 to v3.2). * docs: Established IDTA-01002 as authoritative source for formula grammar and JSON Schema shared between API and Security specifications. +* fix: Aligned Query Language BNF and JSON Schema with IDTA-01004 Security access-rule schema, including wildcard array indexes, stricter object/reference identifiers, and artifact validation tests. * docs: Added informative Conformance Test Corpus annex providing technology-neutral test cases for Query Language, HTTP/REST API, and error handling. * Added the `ServerNotImplemented` status code to the xref:specification/interfaces-payload.adoc#table-status-codes[Status Codes]. Note that the HTTP API mentioned this status code already, therefore, no change in OpenAPI or other related files. (https://github.com/admin-shell-io/aas-specs-api/issues/499[#499]) * Removed the `GetAllAssetAdministrationShellDescriptorsByAssetType` operation from the OpenAPI files for the Asset Administration Shell Registry Service Specification, as it was not intended to be included in the first place and is not implemented by any known implementation. (https://github.com/admin-shell-io/aas-specs-api/issues/515[#515]) @@ -771,4 +772,3 @@ PostConceptDescription |PutConceptDescription |PutConceptDescriptionById |Changed |Naming pattern byId | |PostConceptDescription |New | |=== - diff --git a/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_field_identifier_regex.py b/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_field_identifier_regex.py index 1db2a61f..b51a8a64 100644 --- a/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_field_identifier_regex.py +++ b/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_field_identifier_regex.py @@ -60,6 +60,7 @@ def test_allowed_field_identifiers(self): "$aas#assetInformation.specificAssetIds[].externalSubjectId.type", "$aas#assetInformation.specificAssetIds[].externalSubjectId.keys[].type", "$aas#assetInformation.specificAssetIds[].externalSubjectId.keys[0].value", + "$aas#submodels[]", "$aas#submodels[].type", "$aas#submodels[].keys[].type", "$aas#submodels[0].keys[0].value", @@ -141,14 +142,16 @@ def test_not_allowed_field_identifiers(self): not_allowed = [ "$aas#assetInformation.specificAssetIds", "$aas#assetInformation.specificAssetIds[]", + "$aas#assetInformation.specificAssetIds[01].name", "$aas#assetInformation.specificAssetIds[].externalSubjectId.keys", "$aas#assetInformation.specificAssetIds[].externalSubjectId.keys[]", "$aas#submodels", - "$aas#submodels[]", + "$aas#submodels[01].type", "$aas#submodels[].keys", "$aas#submodels[].keys[]", "$sm#semanticId.keys", "$sm#semanticId.keys[]", + "$sm#supplementalSemanticIds[01]", "$sm#supplementalSemanticIds.keys", "$sm#supplementalSemanticIds.keys[]", "$sm#supplementalSemanticIds[].keys", @@ -158,6 +161,7 @@ def test_not_allowed_field_identifiers(self): "$sme.AddressInformation[]", "$sme.AddressInformation#id", "$sme.AddressInformation#bogus", + "$sme.AddressInformation#supplementalSemanticIds[01]", "$sme.AddressInformation#supplementalSemanticIds.keys", "$sme.AddressInformation#supplementalSemanticIds[].keys", "$sme.1Invalid#value", @@ -173,9 +177,11 @@ def test_not_allowed_field_identifiers(self): "$aasdesc#specificAssetIds[].externalSubjectId.keys[]", "$aasdesc#endpoints", "$aasdesc#endpoints[]", + "$aasdesc#endpoints[01].interface", "$aasdesc#endpoints[].protocolinformation", "$aasdesc#submodelDescriptors", "$aasdesc#submodelDescriptors[]", + "$aasdesc#submodelDescriptors[01].idShort", "$aasdesc#submodelDescriptors[].supplementalSemanticIds.keys", "$aasdesc#submodelDescriptors[].supplementalSemanticIds[].keys", "$aasdesc#submodelDescriptors[].endpoints", @@ -183,9 +189,11 @@ def test_not_allowed_field_identifiers(self): "$smdesc#semanticId.keys", "$smdesc#semanticId.keys[]", "$smdesc#supplementalSemanticIds.keys", + "$smdesc#supplementalSemanticIds[01]", "$smdesc#supplementalSemanticIds[].keys", "$smdesc#endpoints", "$smdesc#endpoints[]", + "$smdesc#endpoints[01].interface", "$smdesc#endpoints[].protocolinformation", ] @@ -198,5 +206,60 @@ def test_documented_field_identifier_patterns_are_in_sync(self): self.assertEqual(self.pattern, openapi_component_pattern("FieldIdentifier")) +class ReferenceIdentifierRegexTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + schema = json.loads(QUERY_SCHEMA.read_text(encoding="utf-8-sig")) + pattern = schema["definitions"]["ReferenceIdentifier"]["pattern"] + cls.pattern = pattern + cls.reference_identifier = re.compile(pattern) + + def assert_allowed(self, value): + self.assertIsNotNone( + self.reference_identifier.fullmatch(value), + f"Expected ReferenceIdentifier to allow: {value}", + ) + + def assert_not_allowed(self, value): + self.assertIsNone( + self.reference_identifier.fullmatch(value), + f"Expected ReferenceIdentifier to reject: {value}", + ) + + def test_allowed_reference_identifiers(self): + allowed = [ + '$aas("aas-id")#assetInformation.specificAssetIds[].externalSubjectId.keys[0].value', + '$sm("SubmodelID")#id', + '$sm("SubmodelID")#supplementalSemanticIds[].keys[].value', + '$cd("ConceptDescriptionID")#idShort', + '$sme("SubmodelID-OperationalData").machineState#value', + '$sme("SubmodelID").AddressInformation[0].Zipcode#semanticId.keys[].value', + ] + + for value in allowed: + with self.subTest(value=value): + self.assert_allowed(value) + + def test_not_allowed_reference_identifiers(self): + not_allowed = [ + "$sm#id", + '$sm("SubmodelID")#bogus', + '$sm("SubmodelID")#supplementalSemanticIds[01]', + '$aas("aas-id")#assetInformation.specificAssetIds[]', + '$cd("ConceptDescriptionID")#description', + '$sme("SubmodelID").machineState', + '$sme("SubmodelID").machineState#id', + '$sme("SubmodelID").1Invalid#value', + '$aasdesc("aas-id")#idShort', + ] + + for value in not_allowed: + with self.subTest(value=value): + self.assert_not_allowed(value) + + def test_documented_reference_identifier_patterns_are_in_sync(self): + self.assertEqual(self.pattern, schema_page_pattern("ReferenceIdentifier")) + + if __name__ == "__main__": unittest.main() diff --git a/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_fragment_field_identifier_regex.py b/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_fragment_field_identifier_regex.py index a5f30686..cce03137 100644 --- a/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_fragment_field_identifier_regex.py +++ b/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_fragment_field_identifier_regex.py @@ -140,43 +140,54 @@ def test_not_allowed_fragment_field_identifiers(self): "$aas#id", "$aas#assetInformation.assetKind", "$aas#assetInformation.specificAssetIds.name", + "$aas#assetInformation.specificAssetIds[01]", "$aas#assetInformation.specificAssetIds[].name", "$aas#assetInformation.specificAssetIds[].value", "$aas#assetInformation.specificAssetIds.keys[]", "$aas#assetInformation.specificAssetIds.externalSubjectId.keys[]", + "$aas#submodels[01]", "$aas#submodels.keys[]", "$aas#submodels[].type", "$aas#submodels[].keys[].value", "$sm#semanticId.type", "$sm#semanticId.keys[].value", "$sm#id", + "$sm#supplementalSemanticIds[01]", "$sm#supplementalSemanticIds.type", "$sm#supplementalSemanticIds.keys", "$sm#supplementalSemanticIds[].type", "$sm#supplementalSemanticIds[].keys[].value", "$sme.1Invalid", + "$sme.AddressInformation[01]", "$sme.AddressInformation#id", "$sme.AddressInformation#bogus", "$sme.AddressInformation#semanticId.type", "$sme.AddressInformation#semanticId.keys[].value", + "$sme.AddressInformation#supplementalSemanticIds[01]", "$sme.AddressInformation#supplementalSemanticIds.type", "$sme.AddressInformation#supplementalSemanticIds.keys", "$sme.AddressInformation#supplementalSemanticIds[].keys[].value", "$cd#id", "$aasdesc#id", "$aasdesc#specificAssetIds.name", + "$aasdesc#specificAssetIds[01]", "$aasdesc#specificAssetIds[].name", + "$aasdesc#endpoints[01]", "$aasdesc#endpoints.protocolinformation.href", + "$aasdesc#submodelDescriptors[01]", "$aasdesc#submodelDescriptors.endpoints[]", "$aasdesc#submodelDescriptors[].id", + "$aasdesc#submodelDescriptors[].supplementalSemanticIds[01]", "$aasdesc#submodelDescriptors[].supplementalSemanticIds.type", "$aasdesc#submodelDescriptors[].supplementalSemanticIds.keys", "$aasdesc#submodelDescriptors[].supplementalSemanticIds[].keys[].value", "$aasdesc#submodelDescriptors[].endpoints.protocolinformation.href", "$smdesc#id", + "$smdesc#supplementalSemanticIds[01]", "$smdesc#supplementalSemanticIds.type", "$smdesc#supplementalSemanticIds.keys", "$smdesc#supplementalSemanticIds[].keys[].value", + "$smdesc#endpoints[01]", "$smdesc#endpoints.protocolinformation.href", ] diff --git a/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_query_json_schema.py b/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_query_json_schema.py new file mode 100644 index 00000000..5916d128 --- /dev/null +++ b/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_query_json_schema.py @@ -0,0 +1,212 @@ +import json +import unittest +from pathlib import Path + +from jsonschema import Draft7Validator, FormatChecker + + +ROOT_MODULE = Path(__file__).resolve().parents[4] +QUERY_SCHEMA = ROOT_MODULE / "partials" / "query-json-schema.json" + + +def format_errors(errors): + return "\n".join( + f"{'/'.join(str(part) for part in error.absolute_path) or ''}: {error.message}" + for error in errors + ) + + +class QueryJsonSchemaValidationTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.schema = json.loads(QUERY_SCHEMA.read_text(encoding="utf-8-sig")) + Draft7Validator.check_schema(cls.schema) + cls.validator = Draft7Validator(cls.schema, format_checker=FormatChecker()) + + def assert_valid(self, instance): + errors = sorted(self.validator.iter_errors(instance), key=lambda error: list(error.absolute_path)) + self.assertEqual([], errors, format_errors(errors)) + + def assert_invalid(self, instance): + errors = sorted(self.validator.iter_errors(instance), key=lambda error: list(error.absolute_path)) + self.assertTrue(errors, "Expected schema validation to fail") + + def test_complex_query_payload_is_valid(self): + query = { + "$select": "id", + "$condition": { + "$and": [ + { + "$match": [ + { + "$eq": [ + {"$field": "$sm#supplementalSemanticIds[].keys[].value"}, + {"$strVal": "https://example.org/semantic-id"}, + ] + }, + { + "$gt": [ + {"$numCast": {"$field": "$sme.OperationalData.Temperature#value"}}, + {"$numVal": 42.5}, + ] + }, + ] + }, + {"$not": {"$boolean": False}}, + ] + }, + "$filters": [ + { + "$fragment": "$aasdesc#submodelDescriptors[].supplementalSemanticIds[].keys[]", + "$condition": { + "$contains": [ + {"$field": "$smdesc#semanticId.keys[].value"}, + {"$strVal": "admin-shell"}, + ] + }, + } + ], + } + + self.assert_valid(query) + + def test_access_rule_payload_with_constrained_identifiers_is_valid(self): + access_rules = { + "DEFATTRIBUTES": [ + { + "name": "timeAttributes", + "USEATTRIBUTES": ["baseAttributes"], + } + ], + "rules": [ + { + "ACL": { + "ATTRIBUTES": [ + {"CLAIM": "bpn"}, + {"GLOBAL": "UTCNOW"}, + {"REFERENCE": '$sm("SubmodelID")#supplementalSemanticIds[].keys[].value'}, + ], + "RIGHTS": ["READ", "UPDATE"], + "ACCESS": "ALLOW", + }, + "OBJECTS": [ + {"IDENTIFIABLE": '$aas("aas-id")'}, + {"REFERABLE": '$sme("SubmodelID").AddressInformation[]'}, + {"FRAGMENT": "$aasdesc#submodelDescriptors[].semanticId.keys[]"}, + {"DESCRIPTOR": '$smdesc("submodel-id")'}, + ], + "FORMULA": { + "$eq": [ + {"$attribute": {"CLAIM": "bpn"}}, + {"$strVal": "BPNL123"}, + ] + }, + "FILTERLIST": [ + { + "FRAGMENT": "$sme#value", + "CONDITION": {"$boolean": True}, + }, + { + "FRAGMENT": "$sm#supplementalSemanticIds[]", + "USEFORMULA": "predefinedFormula", + }, + ], + } + ], + } + + self.assert_valid(access_rules) + + def test_invalid_query_payloads_are_rejected(self): + cases = [ + { + "$condition": { + "$eq": [ + {"$field": "$sm#supplementalSemanticIds[01]"}, + {"$strVal": "https://example.org/semantic-id"}, + ] + } + }, + { + "$condition": { + "$and": [{"$boolean": True}, {"$boolean": False}], + "$or": [{"$boolean": True}, {"$boolean": False}], + } + }, + { + "$condition": { + "$eq": [ + {"$dateTimeVal": "not-a-date-time"}, + {"$dateTimeVal": "2026-06-29T12:00:00Z"}, + ] + } + }, + { + "$condition": {"$boolean": True}, + "$filters": [ + { + "$fragment": "$sme#value", + } + ], + }, + { + "Query": { + "$condition": {"$boolean": True}, + } + }, + ] + + for case in cases: + with self.subTest(case=case): + self.assert_invalid(case) + + def test_invalid_access_rule_identifiers_are_rejected(self): + cases = [ + { + "rules": [ + { + "ACL": { + "ATTRIBUTES": [{"REFERENCE": "$sm#id"}], + "RIGHTS": ["READ"], + "ACCESS": "ALLOW", + }, + "OBJECTS": [{"IDENTIFIABLE": '$aas("aas-id")'}], + "FORMULA": {"$boolean": True}, + } + ] + }, + { + "rules": [ + { + "ACL": { + "ATTRIBUTES": [{"CLAIM": "bpn"}], + "RIGHTS": ["READ"], + "ACCESS": "ALLOW", + }, + "OBJECTS": [{"IDENTIFIABLE": '$aasdesc("aas-id")'}], + "FORMULA": {"$boolean": True}, + } + ] + }, + { + "rules": [ + { + "ACL": { + "ATTRIBUTES": [{"CLAIM": "bpn"}], + "RIGHTS": ["READ"], + "ACCESS": "ALLOW", + }, + "OBJECTS": [{"REFERABLE": '$sme("SubmodelID").AddressInformation[01]'}], + "FORMULA": {"$boolean": True}, + } + ] + }, + ] + + for case in cases: + with self.subTest(case=case): + self.assert_invalid(case) + + +if __name__ == "__main__": + unittest.main() diff --git a/documentation/IDTA-01002-3/modules/ROOT/pages/json-grammar.txt b/documentation/IDTA-01002-3/modules/ROOT/pages/json-grammar.txt index ce47e520..adbebf44 100644 --- a/documentation/IDTA-01002-3/modules/ROOT/pages/json-grammar.txt +++ b/documentation/IDTA-01002-3/modules/ROOT/pages/json-grammar.txt @@ -217,6 +217,7 @@ ::= + ::= [0-9] + ::= "0" | ( [1-9] [0-9]* ) ::= "\"" ( [A-Z] | [a-z] | [0-9] | "/" | "*" | "[" | "]" | "(" | ")" | " " | "_" | "@" | "#" | "\\" | "+" | "-" | "." | "," | ":" | "$" | "^" )+ "\"" ::= ::= @@ -230,19 +231,19 @@ ::= "true" | "false" ::= "\"" ( | | | | | ) "\"" - ::= "$aas#" ( "idShort" | "id" | "assetInformation.assetKind" | "assetInformation.assetType" | "assetInformation.globalAssetId" | "assetInformation." | "submodels" ( "[" ( [0-9]* ) "]" ) ("." )? ) + ::= "$aas#" ( "idShort" | "id" | "assetInformation.assetKind" | "assetInformation.assetType" | "assetInformation.globalAssetId" | "assetInformation." | "submodels" ( "[" ? "]" ) ("." )? ) ::= "$sm#" ( | | "idShort" | "id" ) ::= "$sme" ( "." )? "#" ( | | "idShort" | "value" | "valueType" | "language" ) ::= "$cd#" ( "idShort" | "id" ) - ::= "$aasdesc#" ( "idShort" | "id" | "assetKind" | "assetType" | "globalAssetId" | | "endpoints" ( "[" ( [0-9]* ) "]" ) "." | "submodelDescriptors" ( "[" ( [0-9]* ) "]" ) "." ) + ::= "$aasdesc#" ( "idShort" | "id" | "assetKind" | "assetType" | "globalAssetId" | | "endpoints" ( "[" ? "]" ) "." | "submodelDescriptors" ( "[" ? "]" ) "." ) ::= "$smdesc#" - ::= ( | | "idShort" | "id" | "endpoints" ( "[" ( [0-9]* ) "]" ) "." ) + ::= ( | | "idShort" | "id" | "endpoints" ( "[" ? "]" ) "." ) ::= "interface" | "protocolinformation.href" - ::= ( "type" | "keys" ( "[" ( [0-9]* ) "]" ) ( ".type" | ".value" ) ) + ::= ( "type" | "keys" ( "[" ? "]" ) ( ".type" | ".value" ) ) ::= ( "semanticId" | "semanticId." ) - ::= ( "supplementalSemanticIds" ( "[" ( [0-9]* ) "]" )? ( "." )? ) - ::= ( "specificAssetIds" ( "[" ( [0-9]* ) "]" ) ( ".name" | ".value" | ".externalSubjectId" | ".externalSubjectId." ) ) - ::= ( ("[" ( [0-9]* ) "]" )* ( "." )* ) + ::= ( "supplementalSemanticIds" ( "[" ? "]" )? ( "." )? ) + ::= ( "specificAssetIds" ( "[" ? "]" ) ( ".name" | ".value" | ".externalSubjectId" | ".externalSubjectId." ) ) + ::= ( ("[" ? "]" )* ( "." )* ) ::= ( ( [a-z] | [A-Z] ) (( [a-z] | [A-Z] | [0-9] | "_" | "-" )* ( [a-z] | [A-Z] | [0-9] | "_" ) )? ) ::= ( " " | "\t" | "\r" | "\n" )* diff --git a/documentation/IDTA-01002-3/modules/ROOT/pages/schema.adoc b/documentation/IDTA-01002-3/modules/ROOT/pages/schema.adoc index 3111631f..9badad39 100644 --- a/documentation/IDTA-01002-3/modules/ROOT/pages/schema.adoc +++ b/documentation/IDTA-01002-3/modules/ROOT/pages/schema.adoc @@ -3,18 +3,45 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Common JSON Schema for AAS Queries and Access Rules", "description": "This schema contains all classes that are shared between the AAS Query Language and the AAS Access Rule Language.", + "oneOf": [ + { + "$ref": "#/definitions/Query" + }, + { + "$ref": "#/definitions/AllAccessPermissionRules" + } + ], "definitions": { "standardString": { "type": "string", - "pattern": "^(?!\\$).*" + "pattern": "^[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+$" + }, + "ReferenceIdentifier": { + "type": "string", + "pattern": "^(?:\\$aas\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)#(?:idShort|id|assetInformation\\.assetKind|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds\\[(?:0|[1-9][0-9]*)?\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|submodels\\[(?:0|[1-9][0-9]*)?\\](?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|\\$sm\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|id)|\\$cd\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)#(?:idShort|id)|\\$sme\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*)*#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|value|valueType|language))$" + }, + "IdentifiableIdentifier": { + "type": "string", + "pattern": "^\\$(?:aas|sm|cd)\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)$" + }, + "ReferableIdentifier": { + "type": "string", + "pattern": "^\\$sme\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*)*$" + }, + "FragmentIdentifier": { + "$ref": "#/definitions/FragmentFieldIdentifier" + }, + "DescriptorIdentifier": { + "type": "string", + "pattern": "^\\$(?:aasdesc|smdesc)\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)$" }, "FieldIdentifier": { "type": "string", - "pattern": "^(?:\\$aas#(?:idShort|id|assetInformation\\.assetKind|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds\\[[0-9]*\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?)|submodels\\[[0-9]*\\]\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))|\\$sm#(?:semanticId(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[[0-9]*\\])?(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?|idShort|id)|\\$sme(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[[0-9]*\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[[0-9]*\\])*)*)?#(?:semanticId(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[[0-9]*\\])?(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?|idShort|value|valueType|language)|\\$cd#(?:idShort|id)|\\$aasdesc#(?:idShort|id|assetKind|assetType|globalAssetId|specificAssetIds\\[[0-9]*\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?)|endpoints\\[[0-9]*\\]\\.(?:interface|protocolinformation\\.href)|submodelDescriptors\\[[0-9]*\\]\\.(?:semanticId(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[[0-9]*\\])?(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?|idShort|id|endpoints\\[[0-9]*\\]\\.(?:interface|protocolinformation\\.href)))|\\$smdesc#(?:semanticId(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[[0-9]*\\])?(?:\\.(?:type|keys\\[[0-9]*\\]\\.(?:type|value)))?|idShort|id|endpoints\\[[0-9]*\\]\\.(?:interface|protocolinformation\\.href)))$" + "pattern": "^(?:\\$aas#(?:idShort|id|assetInformation\\.assetKind|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds\\[(?:0|[1-9][0-9]*)?\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|submodels\\[(?:0|[1-9][0-9]*)?\\](?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|\\$sm#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|id)|\\$sme(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*)*)?#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|value|valueType|language)|\\$cd#(?:idShort|id)|\\$aasdesc#(?:idShort|id|assetKind|assetType|globalAssetId|specificAssetIds\\[(?:0|[1-9][0-9]*)?\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|endpoints\\[(?:0|[1-9][0-9]*)?\\]\\.(?:interface|protocolinformation\\.href)|submodelDescriptors\\[(?:0|[1-9][0-9]*)?\\]\\.(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|id|endpoints\\[(?:0|[1-9][0-9]*)?\\]\\.(?:interface|protocolinformation\\.href)))|\\$smdesc#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|id|endpoints\\[(?:0|[1-9][0-9]*)?\\]\\.(?:interface|protocolinformation\\.href)))$" }, "FragmentFieldIdentifier": { "type": "string", - "pattern": "^(?:\\$aas#(?:idShort|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds(?:\\[[0-9]*\\](?:\\.externalSubjectId(?:\\.keys(?:\\[[0-9]*\\])?)?)?)?|submodels(?:\\[[0-9]*\\](?:\\.keys(?:\\[[0-9]*\\])?)?)?)|\\$sm#(?:semanticId(?:\\.keys(?:\\[[0-9]*\\])?)?|supplementalSemanticIds(?:\\[[0-9]*\\](?:\\.keys(?:\\[[0-9]*\\])?)?)?|idShort)|\\$sme(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[[0-9]*\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[[0-9]*\\])*)*)?(?:#(?:semanticId(?:\\.keys(?:\\[[0-9]*\\])?)?|supplementalSemanticIds(?:\\[[0-9]*\\](?:\\.keys(?:\\[[0-9]*\\])?)?)?|idShort|value|valueType|language))?|\\$cd#idShort|\\$aasdesc#(?:idShort|description|displayName|extension|administration|assetKind|assetType|globalAssetId|specificAssetIds(?:\\[[0-9]*\\](?:\\.externalSubjectId(?:\\.keys(?:\\[[0-9]*\\])?)?)?)?|endpoints(?:\\[[0-9]*\\])?|submodelDescriptors(?:\\[[0-9]*\\](?:\\.(?:semanticId(?:\\.keys(?:\\[[0-9]*\\])?)?|supplementalSemanticIds(?:\\[[0-9]*\\](?:\\.keys(?:\\[[0-9]*\\])?)?)?|idShort|endpoints(?:\\[[0-9]*\\])?))?)?)|\\$smdesc#(?:semanticId(?:\\.keys(?:\\[[0-9]*\\])?)?|supplementalSemanticIds(?:\\[[0-9]*\\](?:\\.keys(?:\\[[0-9]*\\])?)?)?|idShort|endpoints(?:\\[[0-9]*\\])?))$" + "pattern": "^(?:\\$aas#(?:idShort|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.externalSubjectId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?)?|submodels(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?)|\\$sm#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?|idShort)|\\$sme(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*)*)?(?:#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?|idShort|value|valueType|language))?|\\$cd#idShort|\\$aasdesc#(?:idShort|description|displayName|extension|administration|assetKind|assetType|globalAssetId|specificAssetIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.externalSubjectId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?)?|endpoints(?:\\[(?:0|[1-9][0-9]*)?\\])?|submodelDescriptors(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?|idShort|endpoints(?:\\[(?:0|[1-9][0-9]*)?\\])?))?)?)|\\$smdesc#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?|idShort|endpoints(?:\\[(?:0|[1-9][0-9]*)?\\])?))$" }, "hexLiteralPattern": { "type": "string", @@ -544,7 +571,7 @@ ] }, "REFERENCE": { - "type": "string" + "$ref": "#/definitions/ReferenceIdentifier" } }, "additionalProperties": false @@ -582,16 +609,16 @@ "type": "string" }, "IDENTIFIABLE": { - "type": "string" + "$ref": "#/definitions/IdentifiableIdentifier" }, "REFERABLE": { - "type": "string" + "$ref": "#/definitions/ReferableIdentifier" }, "FRAGMENT": { - "type": "string" + "$ref": "#/definitions/FragmentIdentifier" }, "DESCRIPTOR": { - "type": "string" + "$ref": "#/definitions/DescriptorIdentifier" } }, "additionalProperties": false @@ -823,11 +850,28 @@ "items": { "$ref": "#/definitions/attributeItem" } + }, + "USEATTRIBUTES": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "name", - "attributes" + "name" + ], + "oneOf": [ + { + "required": [ + "attributes" + ] + }, + { + "required": [ + "USEATTRIBUTES" + ] + } ], "additionalProperties": false } @@ -921,19 +965,6 @@ ], "additionalProperties": false } - }, - "oneOf": [ - { - "required": [ - "Query" - ] - }, - { - "required": [ - "AllAccessPermissionRules" - ] - } - ], - "additionalProperties": false + } } .... diff --git a/documentation/IDTA-01002-3/modules/ROOT/partials/bnf/grammar.bnf b/documentation/IDTA-01002-3/modules/ROOT/partials/bnf/grammar.bnf index 9014b071..650d14e9 100644 --- a/documentation/IDTA-01002-3/modules/ROOT/partials/bnf/grammar.bnf +++ b/documentation/IDTA-01002-3/modules/ROOT/partials/bnf/grammar.bnf @@ -107,7 +107,7 @@ ( "FILTERLIST:" ( )* )? ::= - ( "FRAGMENT:" ) + ( "FRAGMENT:" ) ( ( "CONDITION:" ) | ( ) ) ::= @@ -196,7 +196,7 @@ ::= + ::= [0-9] - ::= 0 | ([1-9][0-9]*) + ::= "0" | ( [1-9] [0-9]* ) ::= "\"" ( [A-Z] | [a-z] | [0-9] | "/" | "*" | "[" | "]" | "(" | ")" | " " | "_" | "@" | "#" | "\\" | "+" | "-" | "." | "," | ":" | "$" | "^" )+ "\"" ::= ::= @@ -217,18 +217,18 @@ ::= "$aasdesc#" ::= "$smdesc#" - ::= "idShort" | "id" | "assetInformation.assetKind" | "assetInformation.assetType" | "assetInformation.globalAssetId" | "assetInformation." | "submodels" ( "[" "]" ) ("." )? + ::= "idShort" | "id" | "assetInformation.assetKind" | "assetInformation.assetType" | "assetInformation.globalAssetId" | "assetInformation." | "submodels" ( "[" ? "]" ) ("." )? ::= | | "idShort" | "id" ::= | | "idShort" | "value" | "valueType" | "language" ::= "idShort" | "id" - ::= "idShort" | "id" | "assetKind" | "assetType" | "globalAssetId" | | "endpoints" ( "[" "]" ) "." | "submodelDescriptors" ( "[" "]" ) "." - ::= ( | | "idShort" | "id" | "endpoints" ( "[" "]" ) "." ) + ::= "idShort" | "id" | "assetKind" | "assetType" | "globalAssetId" | | "endpoints" ( "[" ? "]" ) "." | "submodelDescriptors" ( "[" ? "]" ) "." + ::= ( | | "idShort" | "id" | "endpoints" ( "[" ? "]" ) "." ) ::= "interface" | "protocolinformation.href" - ::= ( "type" | "keys" ( "[" "]" ) ( ".type" | ".value" ) ) + ::= ( "type" | "keys" ( "[" ? "]" ) ( ".type" | ".value" ) ) ::= ( "semanticId" | "semanticId." ) - ::= ( "supplementalSemanticIds" ( "[" "]" )? ( "." )? ) - ::= ( "specificAssetIds" ( "[" "]" ) ( ".name" | ".value" | ".externalSubjectId" | ".externalSubjectId." ) ) + ::= ( "supplementalSemanticIds" ( "[" ? "]" )? ( "." )? ) + ::= ( "specificAssetIds" ( "[" ? "]" ) ( ".name" | ".value" | ".externalSubjectId" | ".externalSubjectId." ) ) ::= | | | | | @@ -240,18 +240,18 @@ ::= "$aasdesc#" ( "idShort" | "description" | "displayName" | "extension" | "administration" | "assetKind" | "assetType" | "globalAssetId" | | | ) ::= "$smdesc#" - ::= "specificAssetIds" | "specificAssetIds" ( "[" "]" ) ( ".externalSubjectId" ( "." )? )? - ::= "submodels" | "submodels" ( "[" "]" ) ( "." )? - ::= "submodelDescriptors" | "submodelDescriptors" ( "[" "]" ) ( "." )? + ::= "specificAssetIds" | "specificAssetIds" ( "[" ? "]" ) ( ".externalSubjectId" ( "." )? )? + ::= "submodels" | "submodels" ( "[" ? "]" ) ( "." )? + ::= "submodelDescriptors" | "submodelDescriptors" ( "[" ? "]" ) ( "." )? ::= ( | | "idShort" | ) - ::= "endpoints" ( "[" "]" )? + ::= "endpoints" ( "[" ? "]" )? ::= "semanticId" | "semanticId." - ::= "supplementalSemanticIds" ( "[" "]" ( "." )? )? - ::= "keys" ( "[" "]" )? + ::= "supplementalSemanticIds" ( "[" ? "]" ( "." )? )? + ::= "keys" ( "[" ? "]" )? - ::= ( ("[" "]" )* ( "." )* ) + ::= ( ("[" ? "]" )* ( "." )* ) ::= ( ( [a-z] | [A-Z] ) ( [a-z] | [A-Z] | [0-9] | "_" | "-" )* ( [a-z] | [A-Z] | [0-9] | "_" ) ) ::= ( " " | "\t" | "\r" | "\n" )* diff --git a/documentation/IDTA-01002-3/modules/ROOT/partials/query-json-schema.json b/documentation/IDTA-01002-3/modules/ROOT/partials/query-json-schema.json index 27d07d6b..2d093327 100644 --- a/documentation/IDTA-01002-3/modules/ROOT/partials/query-json-schema.json +++ b/documentation/IDTA-01002-3/modules/ROOT/partials/query-json-schema.json @@ -1,20 +1,46 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "JSON Schema for AAS Queries", - "description": "This schema validates AAS Queries.", - "$ref": "#/definitions/Query", + "title": "Common JSON Schema for AAS Queries and Access Rules", + "description": "This schema contains all classes that are shared between the AAS Query Language and the AAS Access Rule Language.", + "oneOf": [ + { + "$ref": "#/definitions/Query" + }, + { + "$ref": "#/definitions/AllAccessPermissionRules" + } + ], "definitions": { "standardString": { "type": "string", - "pattern": "^(?!\\$).*" + "pattern": "^[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+$" + }, + "ReferenceIdentifier": { + "type": "string", + "pattern": "^(?:\\$aas\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)#(?:idShort|id|assetInformation\\.assetKind|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds\\[(?:0|[1-9][0-9]*)?\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|submodels\\[(?:0|[1-9][0-9]*)?\\](?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|\\$sm\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|id)|\\$cd\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)#(?:idShort|id)|\\$sme\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*)*#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|value|valueType|language))$" + }, + "IdentifiableIdentifier": { + "type": "string", + "pattern": "^\\$(?:aas|sm|cd)\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)$" + }, + "ReferableIdentifier": { + "type": "string", + "pattern": "^\\$sme\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*)*$" + }, + "FragmentIdentifier": { + "$ref": "#/definitions/FragmentFieldIdentifier" + }, + "DescriptorIdentifier": { + "type": "string", + "pattern": "^\\$(?:aasdesc|smdesc)\\(\"[A-Za-z0-9/\\*\\[\\]\\(\\) _@#\\\\+\\-\\.,:\\$\\^]+\"\\)$" }, "FieldIdentifier": { "type": "string", - "pattern": "^(?:\\$aas#(?:idShort|id|assetInformation\\.assetKind|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds\\[(?:0|[1-9][0-9]*)\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?)|submodels\\[(?:0|[1-9][0-9]*)\\](?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?)|\\$sm#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?|idShort|id)|\\$sme(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)\\])*)*)?#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?|idShort|value|valueType|language)|\\$cd#(?:idShort|id)|\\$aasdesc#(?:idShort|id|assetKind|assetType|globalAssetId|specificAssetIds\\[(?:0|[1-9][0-9]*)\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?)|endpoints\\[(?:0|[1-9][0-9]*)\\]\\.(?:interface|protocolinformation\\.href)|submodelDescriptors\\[(?:0|[1-9][0-9]*)\\]\\.(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?|idShort|id|endpoints\\[(?:0|[1-9][0-9]*)\\]\\.(?:interface|protocolinformation\\.href)))|\\$smdesc#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)\\]\\.(?:type|value)))?|idShort|id|endpoints\\[(?:0|[1-9][0-9]*)\\]\\.(?:interface|protocolinformation\\.href)))$" + "pattern": "^(?:\\$aas#(?:idShort|id|assetInformation\\.assetKind|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds\\[(?:0|[1-9][0-9]*)?\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|submodels\\[(?:0|[1-9][0-9]*)?\\](?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|\\$sm#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|id)|\\$sme(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*)*)?#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|value|valueType|language)|\\$cd#(?:idShort|id)|\\$aasdesc#(?:idShort|id|assetKind|assetType|globalAssetId|specificAssetIds\\[(?:0|[1-9][0-9]*)?\\]\\.(?:name|value|externalSubjectId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?)|endpoints\\[(?:0|[1-9][0-9]*)?\\]\\.(?:interface|protocolinformation\\.href)|submodelDescriptors\\[(?:0|[1-9][0-9]*)?\\]\\.(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|id|endpoints\\[(?:0|[1-9][0-9]*)?\\]\\.(?:interface|protocolinformation\\.href)))|\\$smdesc#(?:semanticId(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\])?(?:\\.(?:type|keys\\[(?:0|[1-9][0-9]*)?\\]\\.(?:type|value)))?|idShort|id|endpoints\\[(?:0|[1-9][0-9]*)?\\]\\.(?:interface|protocolinformation\\.href)))$" }, "FragmentFieldIdentifier": { "type": "string", - "pattern": "^(?:\\$aas#(?:idShort|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds(?:\\[(?:0|[1-9][0-9]*)\\](?:\\.externalSubjectId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?)?)?|submodels(?:\\[(?:0|[1-9][0-9]*)\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?)?)|\\$sm#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?)?|idShort)|\\$sme(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)\\])*)*)?(?:#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?)?|idShort|value|valueType|language))?|\\$cd#idShort|\\$aasdesc#(?:idShort|description|displayName|extension|administration|assetKind|assetType|globalAssetId|specificAssetIds(?:\\[(?:0|[1-9][0-9]*)\\](?:\\.externalSubjectId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?)?)?|endpoints(?:\\[(?:0|[1-9][0-9]*)\\])?|submodelDescriptors(?:\\[(?:0|[1-9][0-9]*)\\](?:\\.(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?)?|idShort|endpoints(?:\\[(?:0|[1-9][0-9]*)\\])?))?)?)|\\$smdesc#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)\\])?)?)?|idShort|endpoints(?:\\[(?:0|[1-9][0-9]*)\\])?))$" + "pattern": "^(?:\\$aas#(?:idShort|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.specificAssetIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.externalSubjectId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?)?|submodels(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?)|\\$sm#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?|idShort)|\\$sme(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*(?:\\.[A-Za-z](?:[A-Za-z0-9_-]*[A-Za-z0-9_])?(?:\\[(?:0|[1-9][0-9]*)?\\])*)*)?(?:#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?|idShort|value|valueType|language))?|\\$cd#idShort|\\$aasdesc#(?:idShort|description|displayName|extension|administration|assetKind|assetType|globalAssetId|specificAssetIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.externalSubjectId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?)?|endpoints(?:\\[(?:0|[1-9][0-9]*)?\\])?|submodelDescriptors(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?|idShort|endpoints(?:\\[(?:0|[1-9][0-9]*)?\\])?))?)?)|\\$smdesc#(?:semanticId(?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?|supplementalSemanticIds(?:\\[(?:0|[1-9][0-9]*)?\\](?:\\.keys(?:\\[(?:0|[1-9][0-9]*)?\\])?)?)?|idShort|endpoints(?:\\[(?:0|[1-9][0-9]*)?\\])?))$" }, "hexLiteralPattern": { "type": "string", @@ -37,6 +63,9 @@ "$strVal": { "$ref": "#/definitions/standardString" }, + "$attribute": { + "$ref": "#/definitions/attributeItem" + }, "$numVal": { "type": "number" }, @@ -122,6 +151,11 @@ "$strVal" ] }, + { + "required": [ + "$attribute" + ] + }, { "required": [ "$numVal" @@ -211,6 +245,9 @@ }, "$strCast": { "$ref": "#/definitions/Value" + }, + "$attribute": { + "$ref": "#/definitions/attributeItem" } }, "oneOf": [ @@ -228,6 +265,11 @@ "required": [ "$strCast" ] + }, + { + "required": [ + "$attribute" + ] } ], "additionalProperties": false @@ -528,7 +570,7 @@ ] }, "REFERENCE": { - "type": "string" + "$ref": "#/definitions/ReferenceIdentifier" } }, "additionalProperties": false @@ -566,16 +608,16 @@ "type": "string" }, "IDENTIFIABLE": { - "type": "string" + "$ref": "#/definitions/IdentifiableIdentifier" }, "REFERABLE": { - "type": "string" + "$ref": "#/definitions/ReferableIdentifier" }, "FRAGMENT": { - "type": "string" + "$ref": "#/definitions/FragmentIdentifier" }, "DESCRIPTOR": { - "type": "string" + "$ref": "#/definitions/DescriptorIdentifier" } }, "additionalProperties": false @@ -807,11 +849,28 @@ "items": { "$ref": "#/definitions/attributeItem" } + }, + "USEATTRIBUTES": { + "type": "array", + "items": { + "type": "string" + } } }, "required": [ - "name", - "attributes" + "name" + ], + "oneOf": [ + { + "required": [ + "attributes" + ] + }, + { + "required": [ + "USEATTRIBUTES" + ] + } ], "additionalProperties": false } diff --git a/tools/validate_spec_artifacts.py b/tools/validate_spec_artifacts.py new file mode 100644 index 00000000..057e62e8 --- /dev/null +++ b/tools/validate_spec_artifacts.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +"""Validate the query grammar, JSON schema, and synchronized schema copies.""" + +from __future__ import annotations + +import json +import re +import sys +import warnings +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Iterable + +try: + from jsonschema import Draft7Validator, FormatChecker + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + from jsonschema import RefResolver +except ImportError as exc: # pragma: no cover - exercised in CI setup failures. + print("Missing dependency: jsonschema. Install it with: python -m pip install jsonschema") + raise SystemExit(2) from exc + + +ROOT = Path(__file__).resolve().parents[1] +ROOT_MODULE = ROOT / "documentation/IDTA-01002-3/modules/ROOT" +QUERY_SCHEMA = ROOT_MODULE / "partials/query-json-schema.json" +SCHEMA_PAGE = ROOT_MODULE / "pages/schema.adoc" +GRAMMAR_FILES = [ + ROOT_MODULE / "partials/bnf/grammar.bnf", + ROOT_MODULE / "pages/json-grammar.txt", +] +OPENAPI_SCHEMA = ROOT / "Part2-API-Schemas/openapi.yaml" +QUERY_EXAMPLES = [ + ROOT_MODULE / "pages/http-rest-api/test/query/test1.json", +] + +RULE_RE = re.compile(r"^\s*<([^<>]+)>\s*::=\s*(.*)$") +REF_RE = re.compile(r"<([^<>]+)>") + + +@dataclass(frozen=True) +class BnfRule: + name: str + path: Path + line: int + rhs: str + + +class Validation: + def __init__(self) -> None: + self.errors: list[str] = [] + + def fail(self, path: Path | str, message: str) -> None: + self.errors.append(f"{display_path(path)}: {message}") + + def assert_ok(self) -> None: + if not self.errors: + return + print("Validation failed:") + for error in self.errors: + print(f" - {error}") + raise SystemExit(1) + + +def display_path(path: Path | str) -> str: + path = Path(path) + try: + return path.relative_to(ROOT).as_posix() + except ValueError: + return path.as_posix() + + +def load_json(path: Path, validation: Validation) -> Any | None: + try: + return json.loads(path.read_text(encoding="utf-8-sig")) + except Exception as exc: # noqa: BLE001 - keep parser detail in output. + validation.fail(path, f"invalid JSON: {exc}") + return None + + +def schema_page_json(validation: Validation) -> dict[str, Any] | None: + try: + lines = SCHEMA_PAGE.read_text(encoding="utf-8-sig").splitlines() + except Exception as exc: # noqa: BLE001 + validation.fail(SCHEMA_PAGE, f"could not read schema page: {exc}") + return None + + if len(lines) < 3 or lines[0] != "...." or lines[-1] != "....": + validation.fail(SCHEMA_PAGE, "expected schema page to be wrapped in .... delimiters") + return None + + try: + return json.loads("\n".join(lines[1:-1])) + except Exception as exc: # noqa: BLE001 + validation.fail(SCHEMA_PAGE, f"invalid embedded JSON schema: {exc}") + return None + + +def iter_refs(node: Any) -> Iterable[str]: + if isinstance(node, dict): + ref = node.get("$ref") + if isinstance(ref, str): + yield ref + for value in node.values(): + yield from iter_refs(value) + elif isinstance(node, list): + for value in node: + yield from iter_refs(value) + + +def resolve_pointer(document: Any, pointer: str) -> Any: + current = document + if pointer in ("", "/"): + return current + for raw_part in pointer.lstrip("/").split("/"): + part = raw_part.replace("~1", "/").replace("~0", "~") + current = current[part] + return current + + +def validate_json_schema(validation: Validation) -> dict[str, Any] | None: + schema = load_json(QUERY_SCHEMA, validation) + if not isinstance(schema, dict): + validation.fail(QUERY_SCHEMA, "schema root is not an object") + return None + + try: + Draft7Validator.check_schema(schema) + except Exception as exc: # noqa: BLE001 - keep schema detail in output. + validation.fail(QUERY_SCHEMA, f"invalid draft-07 schema: {exc}") + + for ref in iter_refs(schema): + if not ref.startswith("#/"): + validation.fail(QUERY_SCHEMA, f"unexpected external $ref: {ref}") + continue + try: + resolve_pointer(schema, ref[1:]) + except Exception as exc: # noqa: BLE001 + validation.fail(QUERY_SCHEMA, f"unresolved $ref {ref}: {exc}") + + embedded_schema = schema_page_json(validation) + if embedded_schema is not None and embedded_schema != schema: + validation.fail(SCHEMA_PAGE, "embedded schema differs from partials/query-json-schema.json") + + validate_query_examples(schema, validation) + validate_schema_smoke_tests(schema, validation) + validate_openapi_pattern_sync(schema, validation) + return schema + + +def schema_validator(schema: dict[str, Any], ref: str) -> Draft7Validator: + resolver = RefResolver(base_uri=QUERY_SCHEMA.resolve().as_uri(), referrer=schema) + return Draft7Validator({"$ref": ref}, resolver=resolver, format_checker=FormatChecker()) + + +def definition_validator(schema: dict[str, Any], definition_name: str) -> Draft7Validator: + return schema_validator(schema, f"#/definitions/{definition_name}") + + +def validate_query_examples(schema: dict[str, Any], validation: Validation) -> None: + validator = Draft7Validator(schema, format_checker=FormatChecker()) + for path in QUERY_EXAMPLES: + data = load_json(path, validation) + if data is None: + continue + errors = sorted(validator.iter_errors(data), key=lambda error: list(error.absolute_path)) + for error in errors: + location = "/".join(str(part) for part in error.absolute_path) or "" + validation.fail(path, f"{location}: {error.message}") + + +def validate_schema_smoke_tests(schema: dict[str, Any], validation: Validation) -> None: + cases = [ + ("FieldIdentifier", "$aas#submodels[]", True), + ("FieldIdentifier", "$aas#submodels[01]", False), + ("FieldIdentifier", "$sm#supplementalSemanticIds[]", True), + ("FieldIdentifier", "$sm#supplementalSemanticIds[01]", False), + ("FragmentFieldIdentifier", "$sm#supplementalSemanticIds[]", True), + ("FragmentFieldIdentifier", "$sm#supplementalSemanticIds[0].keys[]", True), + ("FragmentFieldIdentifier", "$sm#supplementalSemanticIds[01]", False), + ("ReferenceIdentifier", '$sm("SubmodelID")#id', True), + ("ReferenceIdentifier", '$sme("SubmodelID").machineState#value', True), + ("ReferenceIdentifier", "$sm#id", False), + ("ReferenceIdentifier", '$sm("SubmodelID")#bogus', False), + ("IdentifiableIdentifier", '$aas("aas-id")', True), + ("IdentifiableIdentifier", '$aasdesc("aas-id")', False), + ("ReferableIdentifier", '$sme("submodel-id").AddressInformation[]', True), + ("ReferableIdentifier", '$sme("submodel-id").AddressInformation[01]', False), + ("DescriptorIdentifier", '$aasdesc("aas-id")', True), + ("DescriptorIdentifier", '$aas("aas-id")', False), + ] + + validators: dict[str, Draft7Validator] = {} + for definition_name, value, should_pass in cases: + validators.setdefault(definition_name, definition_validator(schema, definition_name)) + is_valid = validators[definition_name].is_valid(value) + if is_valid != should_pass: + expected = "valid" if should_pass else "invalid" + validation.fail(QUERY_SCHEMA, f"{definition_name} smoke test expected {expected}: {value}") + + +def openapi_component_pattern(component: str, validation: Validation) -> str | None: + lines = OPENAPI_SCHEMA.read_text(encoding="utf-8-sig").splitlines() + for index, line in enumerate(lines): + if line.strip() != f"{component}:": + continue + for candidate_index, candidate in enumerate(lines[index + 1 :], start=index + 1): + if candidate.strip() == "pattern: >-": + return lines[candidate_index + 1].strip() + break + validation.fail(OPENAPI_SCHEMA, f"OpenAPI component pattern not found: {component}") + return None + + +def validate_openapi_pattern_sync(schema: dict[str, Any], validation: Validation) -> None: + for definition_name in ["FieldIdentifier", "FragmentFieldIdentifier"]: + expected = schema["definitions"][definition_name]["pattern"] + actual = openapi_component_pattern(definition_name, validation) + if actual is not None and actual != expected: + validation.fail(OPENAPI_SCHEMA, f"{definition_name} pattern differs from query-json-schema.json") + + +def is_escaped(text: str, index: int) -> bool: + backslashes = 0 + cursor = index - 1 + while cursor >= 0 and text[cursor] == "\\": + backslashes += 1 + cursor -= 1 + return backslashes % 2 == 1 + + +def without_quoted_literals(text: str) -> tuple[str, bool]: + result: list[str] = [] + in_string = False + for index, char in enumerate(text): + if char == '"' and not is_escaped(text, index): + in_string = not in_string + result.append(" ") + elif in_string: + result.append(" ") + else: + result.append(char) + return "".join(result), in_string + + +def validate_balanced_delimiters(path: Path, text: str, validation: Validation) -> None: + stack: list[tuple[str, int, int]] = [] + opening = {"(": ")", "[": "]"} + closing = {")": "(", "]": "["} + + for line_number, line in enumerate(text.splitlines(), start=1): + clean_line, unclosed_string = without_quoted_literals(line) + if unclosed_string: + validation.fail(path, f"line {line_number}: unclosed quoted literal") + + for column, char in enumerate(clean_line, start=1): + if char in opening: + stack.append((char, line_number, column)) + elif char in closing: + if not stack or stack[-1][0] != closing[char]: + validation.fail(path, f"line {line_number}: unmatched {char}") + continue + stack.pop() + + for char, line_number, column in stack: + validation.fail(path, f"line {line_number}, column {column}: unmatched {char}") + + +def parse_bnf_rules(path: Path, validation: Validation) -> list[BnfRule]: + rules: list[BnfRule] = [] + seen_in_file: dict[str, int] = {} + current_name: str | None = None + current_line = 0 + current_rhs: list[str] = [] + + def finish_current() -> None: + nonlocal current_name, current_line, current_rhs + if current_name is None: + return + rhs = "\n".join(current_rhs).strip() + if not rhs: + validation.fail(path, f"line {current_line}: rule <{current_name}> has no production") + rules.append(BnfRule(current_name, path, current_line, rhs)) + current_name = None + current_line = 0 + current_rhs = [] + + for line_number, line in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), start=1): + if not line.strip(): + continue + + match = RULE_RE.match(line) + if match: + finish_current() + current_name = match.group(1).strip() + current_line = line_number + current_rhs = [match.group(2)] + previous_line = seen_in_file.get(current_name) + if previous_line is not None: + validation.fail( + path, + f"line {line_number}: duplicate rule <{current_name}> previously defined on line {previous_line}", + ) + seen_in_file[current_name] = line_number + continue + + if "::=" in line: + validation.fail(path, f"line {line_number}: malformed rule definition") + elif current_name is None: + validation.fail(path, f"line {line_number}: content before first rule") + else: + current_rhs.append(line) + + finish_current() + return rules + + +def validate_bnf_grammar_files(validation: Validation) -> None: + for path in GRAMMAR_FILES: + text = path.read_text(encoding="utf-8-sig") + validate_balanced_delimiters(path, text, validation) + rules = parse_bnf_rules(path, validation) + defined_rules = {rule.name for rule in rules} + for rule in rules: + for referenced_rule in sorted(set(REF_RE.findall(rule.rhs))): + if referenced_rule not in defined_rules: + validation.fail( + rule.path, + f"line {rule.line}: <{rule.name}> references undefined <{referenced_rule}>", + ) + + +def main() -> int: + validation = Validation() + validate_json_schema(validation) + validate_bnf_grammar_files(validation) + validation.assert_ok() + print("OK: spec artifact validation completed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From a9cca731dfdec9dadcde583c3c4523f0d4f7da6c Mon Sep 17 00:00:00 2001 From: Martin Stemmer <52048213+Martin187187@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:17:19 +0200 Subject: [PATCH 2/3] add ci workflow --- .github/workflows/validate-spec-artifacts.yml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/validate-spec-artifacts.yml diff --git a/.github/workflows/validate-spec-artifacts.yml b/.github/workflows/validate-spec-artifacts.yml new file mode 100644 index 00000000..50f56f5c --- /dev/null +++ b/.github/workflows/validate-spec-artifacts.yml @@ -0,0 +1,30 @@ +name: Validate spec artifacts + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - name: Fetch sources + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install validation dependencies + run: python -m pip install jsonschema==4.25.1 + + - name: Validate JSON schemas, examples, and BNF artifacts + run: python tools/validate_spec_artifacts.py + + - name: Run query schema and regex tests + run: python -m unittest discover -s documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query From 34284563d142852c704f4557002d49339d10c6b3 Mon Sep 17 00:00:00 2001 From: Martin Stemmer <52048213+Martin187187@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:22:27 +0200 Subject: [PATCH 3/3] fix testcases --- .github/workflows/validate-spec-artifacts.yml | 2 +- .../test/query/test_query_json_schema.py | 14 ++++++- tools/validate_spec_artifacts.py | 39 +++++++++++++------ 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/workflows/validate-spec-artifacts.yml b/.github/workflows/validate-spec-artifacts.yml index 50f56f5c..cc0c3278 100644 --- a/.github/workflows/validate-spec-artifacts.yml +++ b/.github/workflows/validate-spec-artifacts.yml @@ -21,7 +21,7 @@ jobs: python-version: "3.12" - name: Install validation dependencies - run: python -m pip install jsonschema==4.25.1 + run: python -m pip install "jsonschema[format]==4.25.1" - name: Validate JSON schemas, examples, and BNF artifacts run: python tools/validate_spec_artifacts.py diff --git a/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_query_json_schema.py b/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_query_json_schema.py index 5916d128..a121933d 100644 --- a/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_query_json_schema.py +++ b/documentation/IDTA-01002-3/modules/ROOT/pages/http-rest-api/test/query/test_query_json_schema.py @@ -7,6 +7,7 @@ ROOT_MODULE = Path(__file__).resolve().parents[4] QUERY_SCHEMA = ROOT_MODULE / "partials" / "query-json-schema.json" +FORMAT_CHECKER_MESSAGE = 'jsonschema date-time format checking is inactive; install "jsonschema[format]"' def format_errors(errors): @@ -21,7 +22,18 @@ class QueryJsonSchemaValidationTest(unittest.TestCase): def setUpClass(cls): cls.schema = json.loads(QUERY_SCHEMA.read_text(encoding="utf-8-sig")) Draft7Validator.check_schema(cls.schema) - cls.validator = Draft7Validator(cls.schema, format_checker=FormatChecker()) + cls.format_checker = FormatChecker() + cls.ensure_date_time_format_checking_is_active() + cls.validator = Draft7Validator(cls.schema, format_checker=cls.format_checker) + + @classmethod + def ensure_date_time_format_checking_is_active(cls): + validator = Draft7Validator( + {"type": "string", "format": "date-time"}, + format_checker=cls.format_checker, + ) + if not list(validator.iter_errors("not-a-date-time")): + raise RuntimeError(FORMAT_CHECKER_MESSAGE) def assert_valid(self, instance): errors = sorted(self.validator.iter_errors(instance), key=lambda error: list(error.absolute_path)) diff --git a/tools/validate_spec_artifacts.py b/tools/validate_spec_artifacts.py index 057e62e8..85ccb6e5 100644 --- a/tools/validate_spec_artifacts.py +++ b/tools/validate_spec_artifacts.py @@ -18,7 +18,7 @@ warnings.simplefilter("ignore", DeprecationWarning) from jsonschema import RefResolver except ImportError as exc: # pragma: no cover - exercised in CI setup failures. - print("Missing dependency: jsonschema. Install it with: python -m pip install jsonschema") + print('Missing dependency: jsonschema. Install it with: python -m pip install "jsonschema[format]"') raise SystemExit(2) from exc @@ -34,6 +34,7 @@ QUERY_EXAMPLES = [ ROOT_MODULE / "pages/http-rest-api/test/query/test1.json", ] +FORMAT_CHECKER_MESSAGE = 'jsonschema date-time format checking is inactive; install "jsonschema[format]"' RULE_RE = re.compile(r"^\s*<([^<>]+)>\s*::=\s*(.*)$") REF_RE = re.compile(r"<([^<>]+)>") @@ -143,23 +144,39 @@ def validate_json_schema(validation: Validation) -> dict[str, Any] | None: if embedded_schema is not None and embedded_schema != schema: validation.fail(SCHEMA_PAGE, "embedded schema differs from partials/query-json-schema.json") - validate_query_examples(schema, validation) - validate_schema_smoke_tests(schema, validation) + format_checker = active_format_checker(validation) + validate_query_examples(schema, validation, format_checker) + validate_schema_smoke_tests(schema, validation, format_checker) validate_openapi_pattern_sync(schema, validation) return schema -def schema_validator(schema: dict[str, Any], ref: str) -> Draft7Validator: +def active_format_checker(validation: Validation) -> FormatChecker: + format_checker = FormatChecker() + validator = Draft7Validator( + {"type": "string", "format": "date-time"}, + format_checker=format_checker, + ) + if not list(validator.iter_errors("not-a-date-time")): + validation.fail(QUERY_SCHEMA, FORMAT_CHECKER_MESSAGE) + return format_checker + + +def schema_validator(schema: dict[str, Any], ref: str, format_checker: FormatChecker) -> Draft7Validator: resolver = RefResolver(base_uri=QUERY_SCHEMA.resolve().as_uri(), referrer=schema) - return Draft7Validator({"$ref": ref}, resolver=resolver, format_checker=FormatChecker()) + return Draft7Validator({"$ref": ref}, resolver=resolver, format_checker=format_checker) -def definition_validator(schema: dict[str, Any], definition_name: str) -> Draft7Validator: - return schema_validator(schema, f"#/definitions/{definition_name}") +def definition_validator( + schema: dict[str, Any], + definition_name: str, + format_checker: FormatChecker, +) -> Draft7Validator: + return schema_validator(schema, f"#/definitions/{definition_name}", format_checker) -def validate_query_examples(schema: dict[str, Any], validation: Validation) -> None: - validator = Draft7Validator(schema, format_checker=FormatChecker()) +def validate_query_examples(schema: dict[str, Any], validation: Validation, format_checker: FormatChecker) -> None: + validator = Draft7Validator(schema, format_checker=format_checker) for path in QUERY_EXAMPLES: data = load_json(path, validation) if data is None: @@ -170,7 +187,7 @@ def validate_query_examples(schema: dict[str, Any], validation: Validation) -> N validation.fail(path, f"{location}: {error.message}") -def validate_schema_smoke_tests(schema: dict[str, Any], validation: Validation) -> None: +def validate_schema_smoke_tests(schema: dict[str, Any], validation: Validation, format_checker: FormatChecker) -> None: cases = [ ("FieldIdentifier", "$aas#submodels[]", True), ("FieldIdentifier", "$aas#submodels[01]", False), @@ -193,7 +210,7 @@ def validate_schema_smoke_tests(schema: dict[str, Any], validation: Validation) validators: dict[str, Draft7Validator] = {} for definition_name, value, should_pass in cases: - validators.setdefault(definition_name, definition_validator(schema, definition_name)) + validators.setdefault(definition_name, definition_validator(schema, definition_name, format_checker)) is_valid = validators[definition_name].is_valid(value) if is_valid != should_pass: expected = "valid" if should_pass else "invalid"