diff --git a/aas_core3_2/jsonization.py b/aas_core3_2/jsonization.py index 33a8d71..1b1cb7c 100644 --- a/aas_core3_2/jsonization.py +++ b/aas_core3_2/jsonization.py @@ -204,10 +204,12 @@ def __init__(self, cause: str) -> None: # Recursive definitions are not yet available in mypy # (see https://github.com/python/mypy/issues/731). We have to use ``Any`` # here, instead of recursive type annotations. -Jsonable = Union[bool, int, float, str, Sequence[Any], Mapping[str, Any]] +Jsonable = Union[None, bool, int, float, str, Sequence[Any], Mapping[str, Any]] -MutableJsonable = Union[bool, int, float, str, List[Any], MutableMapping[str, Any]] +MutableJsonable = Union[ + None, bool, int, float, str, List[Any], MutableMapping[str, Any] +] # region De-serialization @@ -397,11 +399,10 @@ def _json_value_from_jsonable(jsonable: Jsonable) -> our_types.JsonValue: Parse :paramref:`jsonable` as a JSON-able value. A JSON-able value is, recursively, exactly as JSON itself is defined: - a boolean, a number, a string, an array of JSON-able values or an object - of JSON-able values with string keys. A ``None`` is rejected at any - depth, as the JSON ``null`` has no representation as a JSON-able value, - and so are an infinity and a not-a-number, which JSON can not represent - at all. + a ``None`` (JSON null), a boolean, a number, a string, an array of + JSON-able values or an object of JSON-able values with string keys. + An infinity and a not-a-number are rejected at any depth, as JSON can not + represent them at all. The result is a new structure, and never :paramref:`jsonable` itself, so that the parsed instance does not alias the document it came from. @@ -410,6 +411,9 @@ def _json_value_from_jsonable(jsonable: Jsonable) -> our_types.JsonValue: :return: parsed JSON-able value :raise: :py:class:`DeserializationException` if unexpected :paramref:`jsonable` """ + if jsonable is None: + return None + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would come out as a number. @@ -476,8 +480,8 @@ def _json_value_from_jsonable(jsonable: Jsonable) -> our_types.JsonValue: return items raise DeserializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(jsonable)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(jsonable)}" ) @@ -8672,7 +8676,7 @@ def get_submodel_elements_value_result_from_jsonable( mapping = _as_mapping(jsonable) the_paging_metadata: Optional[our_types.PagingMetadata] = None - the_result: Optional[our_types.JsonObject] = None + the_result: Optional[List[our_types.JsonObject]] = None unexpected_key: Optional[str] = None @@ -8681,7 +8685,7 @@ def get_submodel_elements_value_result_from_jsonable( if key == "paging_metadata": the_paging_metadata = paging_metadata_from_jsonable(jsonable_value) elif key == "result": - the_result = _json_object_from_jsonable(jsonable_value) + the_result = _list_of__json_object_from_jsonable(jsonable_value) else: unexpected_key = key break @@ -8842,7 +8846,7 @@ def get_submodels_value_result_from_jsonable( mapping = _as_mapping(jsonable) the_paging_metadata: Optional[our_types.PagingMetadata] = None - the_result: Optional[our_types.JsonObject] = None + the_result: Optional[List[our_types.JsonObject]] = None unexpected_key: Optional[str] = None @@ -8851,7 +8855,7 @@ def get_submodels_value_result_from_jsonable( if key == "paging_metadata": the_paging_metadata = paging_metadata_from_jsonable(jsonable_value) elif key == "result": - the_result = _json_object_from_jsonable(jsonable_value) + the_result = _list_of__json_object_from_jsonable(jsonable_value) else: unexpected_key = key break @@ -11195,10 +11199,9 @@ def _json_value_to_jsonable(value: our_types.JsonValue) -> MutableJsonable: Serialize :paramref:`value` as a JSON-able structure. A JSON-able value is almost JSON-able as it comes, but not quite: its type - rules out neither a ``None``, which the JSON ``null`` gives no - representation for, nor an infinity and a not-a-number, which JSON can not + rules out neither an infinity and a not-a-number, which JSON can not represent at all, nor a non-string key in an object. Each is refused here, - at any depth. + at any depth. A ``None`` is written out as a JSON null. The result is a new structure, and never :paramref:`value` itself, so that the serialized document does not alias the instance it came from. @@ -11207,6 +11210,9 @@ def _json_value_to_jsonable(value: our_types.JsonValue) -> MutableJsonable: :return: :paramref:`value`, as a JSON-able structure :raise: :py:class:`SerializationException` if :paramref:`value` is not JSON-able """ + if value is None: + return None + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would come out as a number. @@ -11269,8 +11275,8 @@ def _json_value_to_jsonable(value: our_types.JsonValue) -> MutableJsonable: return items raise SerializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) @@ -16766,7 +16772,7 @@ def _get_submodel_elements_value_result_to_jsonable( raise if that.result is not None: try: - jsonable["result"] = _json_value_to_jsonable(that.result) + jsonable["result"] = _list_of__json_object_to_jsonable(that.result) except SerializationException as exception: exception._prepend_property("result") raise @@ -16844,7 +16850,7 @@ def _get_submodels_value_result_to_jsonable( raise if that.result is not None: try: - jsonable["result"] = _json_value_to_jsonable(that.result) + jsonable["result"] = _list_of__json_object_to_jsonable(that.result) except SerializationException as exception: exception._prepend_property("result") raise diff --git a/aas_core3_2/jsonvalueverification.py b/aas_core3_2/jsonvalueverification.py index e8d2b6e..6165d72 100644 --- a/aas_core3_2/jsonvalueverification.py +++ b/aas_core3_2/jsonvalueverification.py @@ -2,8 +2,8 @@ Verify that a value is JSON-able. A JSON-able value is, recursively, exactly as JSON itself is defined: -a boolean, a finite number, a string, an array of JSON-able values or -an object of JSON-able values with string keys. Python offers no type which +a ``None`` (JSON null), a boolean, a finite number, a string, an array of +JSON-able values or an object of JSON-able values with string keys. Python offers no type which says exactly that, so a value has to be walked to make sure of it. This module depends on no class of the meta-model. It is invoked from @@ -42,6 +42,9 @@ def verify_json_value(value: our_types.JsonValue) -> Iterator[Error]: :param value: to be verified :yield: errors, if any """ + if value is None: + return + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would be taken for a number. @@ -107,8 +110,8 @@ def verify_json_value(value: our_types.JsonValue) -> Iterator[Error]: return yield Error( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/aas_core3_2/types.py b/aas_core3_2/types.py index 79f8940..f8664ac 100644 --- a/aas_core3_2/types.py +++ b/aas_core3_2/types.py @@ -107,10 +107,15 @@ # NOTE (mristin): # A JSON-able value is, recursively, exactly as JSON itself is defined: -# a boolean, a number, a string, an array of JSON-able values or an object -# of JSON-able values with string keys -- never a ``None``, and never +# a ``None`` (JSON null), a boolean, a number, a string, an array of JSON-able +# values or an object of JSON-able values with string keys -- never # an infinity or a not-a-number, neither of which JSON can represent. # +# A ``None`` might appear at any depth, including the value itself. Since +# an absent optional property is also ``None``, an optional JSON-able property +# which is a JSON null is indistinguishable from an absent one, and is treated +# as absent. +# # JSON knows a single numeric type. Python tells an ``int`` from a ``float``, # and a caller will naturally write either, so both are admitted here -- but # an ``int`` only so long as a ``float`` can hold it exactly. The @@ -121,7 +126,7 @@ # Recursive type aliases are not yet available in mypy # (see https://github.com/python/mypy/issues/731), so the item and the value # types have to be spelled ``Any`` instead of ``JsonValue``. -JsonValue = Union[bool, int, float, str, Sequence[Any], Mapping[str, Any]] +JsonValue = Union[None, bool, int, float, str, Sequence[Any], Mapping[str, Any]] #: Represent a JSON-able value which is known to be an array @@ -13267,11 +13272,16 @@ class GetSubmodelElementsValueResult(Class): .. note:: - The result is given in the ValueOnly serialization, *i.e.*, as - a JSON object whose keys are the ID-shorts of the submodel elements - and whose values are their ValueOnly serializations. Their shape - depends on the concrete submodel elements and can not be pinned down - statically, so the result is represented as an open JSON object. + Each item of the result is a submodel element given in the ValueOnly + serialization, *i.e.*, as a JSON object with a single key, the ID-short + of the submodel element, whose value is its ValueOnly serialization. + The shape of the value depends on the concrete submodel element and + can not be pinned down statically, so each item is represented as + an open JSON object. + + Unlike the OpenAPI schema, which types the result as a single JSON + object since V3.0.4, we follow BaSyx Go and represent the result as + an array of items. We suspect the OpenAPI schema to be buggy here. See: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.2/http-rest-api/http-rest-api.html#pagination @@ -13282,7 +13292,12 @@ class GetSubmodelElementsValueResult(Class): paging_metadata: "PagingMetadata" #: The returned items. - result: Optional[JsonObject] + result: Optional[List[JsonObject]] + + def over_result_or_empty(self) -> Iterator[JsonObject]: + """Yield from :py:attr:`.result` if set.""" + if self.result is not None: + yield from self.result def descend_once(self) -> Iterator[Class]: """ @@ -13342,7 +13357,9 @@ def transform_with_context( ) def __init__( - self, paging_metadata: "PagingMetadata", result: Optional[JsonObject] = None + self, + paging_metadata: "PagingMetadata", + result: Optional[List[JsonObject]] = None, ) -> None: """Initialize with the given values.""" self.paging_metadata = paging_metadata @@ -13639,11 +13656,17 @@ class GetSubmodelsValueResult(Class): .. note:: - The result is given in the ValueOnly serialization, *i.e.*, as - a JSON object whose keys are the ID-shorts of the submodel elements - and whose values are their ValueOnly serializations. Their shape - depends on the concrete submodel elements and can not be pinned down - statically, so the result is represented as an open JSON object. + Each item of the result is a submodel given in the ValueOnly + serialization, *i.e.*, as a JSON object whose keys are the ID-shorts + of the submodel elements and whose values are their ValueOnly + serializations. Their shape depends on the concrete submodel elements + and can not be pinned down statically, so each item is represented + as an open JSON object. + + Unlike the OpenAPI schema, which types the result as a single + SubmodelValue since V3.0.4, we follow BaSyx Go and represent + the result as an array of items. We suspect the OpenAPI schema to be + buggy here. See: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.2/http-rest-api/http-rest-api.html#pagination @@ -13654,7 +13677,12 @@ class GetSubmodelsValueResult(Class): paging_metadata: "PagingMetadata" #: The returned items. - result: Optional[JsonObject] + result: Optional[List[JsonObject]] + + def over_result_or_empty(self) -> Iterator[JsonObject]: + """Yield from :py:attr:`.result` if set.""" + if self.result is not None: + yield from self.result def descend_once(self) -> Iterator[Class]: """ @@ -13714,7 +13742,9 @@ def transform_with_context( ) def __init__( - self, paging_metadata: "PagingMetadata", result: Optional[JsonObject] = None + self, + paging_metadata: "PagingMetadata", + result: Optional[List[JsonObject]] = None, ) -> None: """Initialize with the given values.""" self.paging_metadata = paging_metadata diff --git a/aas_core3_2/verification.py b/aas_core3_2/verification.py index 5aed5d9..3dd8611 100644 --- a/aas_core3_2/verification.py +++ b/aas_core3_2/verification.py @@ -9266,20 +9266,10 @@ def transform_get_submodel_elements_value_result( yield error if that.result is not None: - for error in our_json_value_verification.verify_json_object(that.result): + for error in _verify_list_of__json_object(that.result): error.path._prepend(PropertySegment(that, "result")) yield error - for key in that.result: - # NOTE (mristin): - # The key segment names the member whose key is erroneous. The path - # thus leads to the member, and the cause says what is wrong with - # the key which names it. - for error in verify_id_short_type(key): - error.path._prepend(KeySegment(that.result, key)) - error.path._prepend(PropertySegment(that, "result")) - yield error - # noinspection PyMethodMayBeStatic def transform_get_all_submodels_recent_changes_result( self, that: our_types.GetAllSubmodelsRecentChangesResult @@ -9328,20 +9318,10 @@ def transform_get_submodels_value_result( yield error if that.result is not None: - for error in our_json_value_verification.verify_json_object(that.result): + for error in _verify_list_of__json_object(that.result): error.path._prepend(PropertySegment(that, "result")) yield error - for key in that.result: - # NOTE (mristin): - # The key segment names the member whose key is erroneous. The path - # thus leads to the member, and the cause says what is wrong with - # the key which names it. - for error in verify_id_short_type(key): - error.path._prepend(KeySegment(that.result, key)) - error.path._prepend(PropertySegment(that, "result")) - yield error - # noinspection PyMethodMayBeStatic def transform_claim_attribute( self, that: our_types.ClaimAttribute diff --git a/aas_core3_2/xmlcommon.py b/aas_core3_2/xmlcommon.py index 0c9acd4..9a96415 100644 --- a/aas_core3_2/xmlcommon.py +++ b/aas_core3_2/xmlcommon.py @@ -692,6 +692,20 @@ def write_end_element_in_no_namespace(self, name: str) -> None: self._open_in_no_namespace -= 1 self.stream.write(f"") + def write_empty_element_in_no_namespace(self, name: str) -> None: + """ + Write the empty element with the tag name :paramref:`name`, which resides + in no namespace at all. + + If it is the outermost such element, it undeclares the default namespace. + + :param name: of the element tag. Expected to contain no XML special characters. + """ + if self._open_in_no_namespace == 0: + self.stream.write(f'<{name} xmlns=""/>') + else: + self.stream.write(f"<{name}/>") + def _write_first_empty_element_with_namespace(self, name: str) -> None: """ Write the first (and only) empty element with the tag name :paramref:`name`. diff --git a/aas_core3_2/xmlization.py b/aas_core3_2/xmlization.py index b970c51..420ef6e 100644 --- a/aas_core3_2/xmlization.py +++ b/aas_core3_2/xmlization.py @@ -34894,7 +34894,7 @@ def _read_get_submodel_elements_value_result_as_sequence( the_paging_metadata: Optional[our_types.PagingMetadata] = values.get( "pagingMetadata" ) - the_result: Optional[our_types.JsonObject] = values.get("result") + the_result: Optional[List[our_types.JsonObject]] = values.get("result") if the_paging_metadata is None: raise DeserializationException( @@ -35121,7 +35121,7 @@ def _read_get_submodels_value_result_as_sequence( the_paging_metadata: Optional[our_types.PagingMetadata] = values.get( "pagingMetadata" ) - the_result: Optional[our_types.JsonObject] = values.get("result") + the_result: Optional[List[our_types.JsonObject]] = values.get("result") if the_paging_metadata is None: raise DeserializationException( @@ -39304,7 +39304,7 @@ def _read_as_element( #: :py:class:`.types.GetSubmodelElementsValueResult`, by the XML name of the property _READERS_FOR_GET_SUBMODEL_ELEMENTS_VALUE_RESULT: Mapping[str, _ContentReader[Any]] = { "pagingMetadata": _read_paging_metadata_as_sequence, - "result": _read_json_object_body, + "result": _read_list_of__json_object, } @@ -39338,7 +39338,7 @@ def _read_as_element( #: :py:class:`.types.GetSubmodelsValueResult`, by the XML name of the property _READERS_FOR_GET_SUBMODELS_VALUE_RESULT: Mapping[str, _ContentReader[Any]] = { "pagingMetadata": _read_paging_metadata_as_sequence, - "result": _read_json_object_body, + "result": _read_list_of__json_object, } @@ -45173,7 +45173,13 @@ def _write_get_submodel_elements_value_result_as_element( "pagingMetadata", "paging_metadata", that.paging_metadata, serializer ) if that.result is not None: - _write_json_object_as_element("result", "result", that.result, serializer) + _write_list_of_items( + "result", + "result", + that.result, + _write_json_object_as_element, + serializer, + ) serializer.writer.write_end_element(name) except Exception as exception: _attribute_to_property(exception, prop_name) @@ -45289,7 +45295,13 @@ def _write_get_submodels_value_result_as_element( "pagingMetadata", "paging_metadata", that.paging_metadata, serializer ) if that.result is not None: - _write_json_object_as_element("result", "result", that.result, serializer) + _write_list_of_items( + "result", + "result", + that.result, + _write_json_object_as_element, + serializer, + ) serializer.writer.write_end_element(name) except Exception as exception: _attribute_to_property(exception, prop_name) diff --git a/aas_core3_2/xmlrpc.py b/aas_core3_2/xmlrpc.py index cb0835c..6e35289 100644 --- a/aas_core3_2/xmlrpc.py +++ b/aas_core3_2/xmlrpc.py @@ -3,8 +3,10 @@ JSON prescribes no XML representation of its own, so we borrow the one of XML-RPC for it: a value is written as a single discriminator element -- -````, ````, ````, ```` or ```` -- -which says what the value is. +````, ````, ````, ````, ```` or +```` -- which says what the value is. XML-RPC itself knows no null, +so we follow the widespread extension, and write a JSON null as an empty +````. This module knows nothing of the meta-model beyond the JSON-able types, and it drives no document of its own. The caller hands over the very stream of @@ -57,9 +59,9 @@ def read_value_content( """ Read the content of :paramref:`element` as a JSON-able value. - The content is a single discriminator element -- ````, - ````, ````, ```` or ```` -- which says what - the value is. + The content is a single discriminator element -- ````, + ````, ````, ````, ```` or ```` -- + which says what the value is. The end element corresponding to :paramref:`element` will be read as well. @@ -77,7 +79,7 @@ def read_value_content( discriminator = read_next_start_element( iterator, "a discriminator " - "(one of , , , or )", + "(one of , , , , or )", ) result = _read_discriminator(discriminator, iterator) @@ -105,7 +107,7 @@ def _read_discriminator( element: Element, iterator: Iterator[Tuple[str, Element]] ) -> our_types.JsonValue: """ - Read :paramref:`element` as one of the five XML-RPC discriminators. + Read :paramref:`element` as one of the six XML-RPC discriminators. The end element corresponding to :paramref:`element` will be read as well. @@ -126,6 +128,19 @@ def _read_discriminator( # very element which does not belong here, so naming it in the path as well # as in the message would say nothing more. try: + if tag_wo_ns == "nil": + read_end_element(element, iterator) + + # NOTE (mristin): + # The text of an element is only complete at its end, so we can check + # it only now. + if element.text is not None and len(element.text.strip()) != 0: + raise DeserializationException( + f"Expected no text in a element, but got: {element.text!r}" + ) + + return None + if tag_wo_ns == "boolean": # NOTE (mristin): # Real XML-RPC tooling writes and expects a strict ``1``/``0``, and not @@ -192,7 +207,7 @@ def _read_discriminator( raise DeserializationException( f"Expected a discriminator element " - f"(one of , , , or ), " + f"(one of , , , , or ), " f"but got: {tag_wo_ns!r}" ) @@ -415,12 +430,16 @@ def _read_member( def write_discriminator(value: our_types.JsonValue, writer: Writer) -> None: """ - Write :paramref:`value` as one of the five XML-RPC discriminators. + Write :paramref:`value` as one of the six XML-RPC discriminators. :param value: to be serialized :param writer: to write to :raise: :py:class:`SerializationException` if the value could not be written """ + if value is None: + writer.write_empty_element_in_no_namespace("nil") + return + # NOTE (mristin): # A ``bool`` is an ``int`` in Python, so it has to be checked first -- # otherwise every boolean would be written as a number. @@ -497,8 +516,8 @@ def write_discriminator(value: our_types.JsonValue, writer: Writer) -> None: return raise SerializationException( - f"Expected a JSON-able value (a boolean, a number, a string, an array " - f"or an object), but got: {type(value)}" + f"Expected a JSON-able value (a null, a boolean, a number, a string, " + f"an array or an object), but got: {type(value)}" ) diff --git a/dev/dev_scripts/codegen/meta_model.py b/dev/dev_scripts/codegen/meta_model.py index 224938a..e248f7d 100644 --- a/dev/dev_scripts/codegen/meta_model.py +++ b/dev/dev_scripts/codegen/meta_model.py @@ -1,4 +1,4 @@ -# Downloaded from: https://raw.githubusercontent.com/eclipse-aascw/aas-core-meta/c5f6c49c227bc199980924e6a5c42d3632ea7051/aas_core_meta/v3_2.py +# Downloaded from: https://raw.githubusercontent.com/eclipse-aascw/aas-core-meta/543f3c05b88e61a4ec3ad90ec244055b5f9b65f0/aas_core_meta/v3_2.py """ --- WORK IN PROGRESS --- Provide an implementation of the Asset Administration Shell (AAS) V3.2. @@ -10331,11 +10331,28 @@ def __init__( # paging_metadata by hand (same reasoning as for Result/ # Base_operation_result/Operation_result above). # -# GetSubmodelElementsValueResult and GetSubmodelsValueResult type their -# "result" as ValueOnly-shaped JSON, so we represent it as an open JSON object -# keyed by ID-shorts. Mind that, unlike all the other GetXxxResult schemas, -# the OpenAPI schema types these results as a single JSON object, not as -# an array of items, and we follow it here. +# GetSubmodelElementsValueResult and GetSubmodelsValueResult type the items +# of their "result" as ValueOnly-shaped JSON, so we represent each item as +# an open JSON object keyed by ID-shorts. +# +# Since V3.0.4 (https://github.com/admin-shell-io/aas-specs-api/pull/414), +# the OpenAPI types these results as a single JSON object instead of an array +# of items. We deliberately deviate from the OpenAPI here and follow BaSyx Go +# (https://github.com/eclipse-basyx/basyx-go-components), which keeps both +# results as arrays, as did the OpenAPI up to V3.0.3, all the other GetXxxResult +# schemas, and the prose of the specification (cardinality 0..* of result). +# +# In particular, a single SubmodelValue can not hold a page of several +# submodels, since the ValueOnly serialization carries no identifier of +# the submodel, and the ID-shorts may repeat across the submodels. Each item +# of the result of GetSubmodelElementsValueResult is a JSON object with +# a single key, the ID-short of the submodel element, so that the ID-short is +# not lost, as it would be for a bare PropertyValue. +# +# We therefore suspect this to be a bug in the OpenAPI. The change in V3.0.4 +# refers to https://github.com/admin-shell-io/aas-specs-api/issues/251, +# which only concerns removing the wrapping "submodelElements" from +# SubmodelValue, and not the paging. # # See: # https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.2/http-rest-api/http-rest-api.html#pagination @@ -10711,11 +10728,16 @@ class Get_submodel_elements_value_result(DBC): .. note:: - The result is given in the ValueOnly serialization, *i.e.*, as - a JSON object whose keys are the ID-shorts of the submodel elements - and whose values are their ValueOnly serializations. Their shape - depends on the concrete submodel elements and can not be pinned down - statically, so the result is represented as an open JSON object. + Each item of the result is a submodel element given in the ValueOnly + serialization, *i.e.*, as a JSON object with a single key, the ID-short + of the submodel element, whose value is its ValueOnly serialization. + The shape of the value depends on the concrete submodel element and + can not be pinned down statically, so each item is represented as + an open JSON object. + + Unlike the OpenAPI schema, which types the result as a single JSON + object since V3.0.4, we follow BaSyx Go and represent the result as + an array of items. We suspect the OpenAPI schema to be buggy here. See: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.2/http-rest-api/http-rest-api.html#pagination @@ -10727,13 +10749,13 @@ class Get_submodel_elements_value_result(DBC): the result set. """ - result: Optional[JSONObject["ID_short_type"]] + result: Optional[List[JSONObject["ID_short_type"]]] """The returned items.""" def __init__( self, paging_metadata: "Paging_metadata", - result: Optional[JSONObject["ID_short_type"]] = None, + result: Optional[List[JSONObject["ID_short_type"]]] = None, ) -> None: self.paging_metadata = paging_metadata self.result = result @@ -10835,11 +10857,17 @@ class Get_submodels_value_result(DBC): .. note:: - The result is given in the ValueOnly serialization, *i.e.*, as - a JSON object whose keys are the ID-shorts of the submodel elements - and whose values are their ValueOnly serializations. Their shape - depends on the concrete submodel elements and can not be pinned down - statically, so the result is represented as an open JSON object. + Each item of the result is a submodel given in the ValueOnly + serialization, *i.e.*, as a JSON object whose keys are the ID-shorts + of the submodel elements and whose values are their ValueOnly + serializations. Their shape depends on the concrete submodel elements + and can not be pinned down statically, so each item is represented + as an open JSON object. + + Unlike the OpenAPI schema, which types the result as a single + SubmodelValue since V3.0.4, we follow BaSyx Go and represent + the result as an array of items. We suspect the OpenAPI schema to be + buggy here. See: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.2/http-rest-api/http-rest-api.html#pagination @@ -10851,13 +10879,13 @@ class Get_submodels_value_result(DBC): the result set. """ - result: Optional[JSONObject["ID_short_type"]] + result: Optional[List[JSONObject["ID_short_type"]]] """The returned items.""" def __init__( self, paging_metadata: "Paging_metadata", - result: Optional[JSONObject["ID_short_type"]] = None, + result: Optional[List[JSONObject["ID_short_type"]]] = None, ) -> None: self.paging_metadata = paging_metadata self.result = result @@ -12310,4 +12338,4 @@ def __init__( # endregion Part 2 -# Downloaded from: https://raw.githubusercontent.com/eclipse-aascw/aas-core-meta/c5f6c49c227bc199980924e6a5c42d3632ea7051/aas_core_meta/v3_2.py +# Downloaded from: https://raw.githubusercontent.com/eclipse-aascw/aas-core-meta/543f3c05b88e61a4ec3ad90ec244055b5f9b65f0/aas_core_meta/v3_2.py diff --git a/dev/pyproject.toml b/dev/pyproject.toml index f47eb79..a6f0bfc 100644 --- a/dev/pyproject.toml +++ b/dev/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "twine", "requests==2.32.4", "types-requests==2.32.4.20250913", - "aas-core-codegen==0.0.27rc4" + "aas-core-codegen==0.0.27rc5" ] [tool.setuptools] diff --git a/dev/tests/common_jsonization.py b/dev/tests/common_jsonization.py index afd3bb5..f44b5e7 100644 --- a/dev/tests/common_jsonization.py +++ b/dev/tests/common_jsonization.py @@ -51,7 +51,7 @@ def check_equal( :param got: obtained JSON-able value :yield: differences """ - if isinstance(expected, (bool, int, float, str, bytes)): + if expected is None or isinstance(expected, (bool, int, float, str, bytes)): if type(expected) is not type(got): yield Difference(f"Expected {type(expected)}, but got {type(got)}") diff --git a/dev/tests/test_for_over_x_or_empty.py b/dev/tests/test_for_over_x_or_empty.py index 6425d41..55025db 100644 --- a/dev/tests/test_for_over_x_or_empty.py +++ b/dev/tests/test_for_over_x_or_empty.py @@ -7642,6 +7642,43 @@ def test_over_result_or_empty_against_recorded(self) -> None: tests.common.record_or_check(expected_path, got_text) +class TestGetSubmodelElementsValueResult(unittest.TestCase): + def test_over_result_or_empty_against_recorded(self) -> None: + for minimal_or_maximal in ["minimal", "maximal"]: + instance = tests.common_xmlization.must_load( + pathlib.Path( + tests.common.TEST_DATA_DIR + / "Xml" + / "Expected" + / "getSubmodelElementsValueResult" + / f"{minimal_or_maximal}.xml" + ) + ) + + assert isinstance(instance, our_types.GetSubmodelElementsValueResult) + + log = [ + tests.common.trace( + [ + tests.common.trace_json(item) + for item in instance.over_result_or_empty() + ] + ) + ] + + got_text = tests.common.trace_log_as_text_file_content(log) + + expected_path = pathlib.Path( + tests.common.TEST_DATA_DIR + / "test_over_X_or_empty" + / "getSubmodelElementsValueResult" + / f"on_{minimal_or_maximal}.xml" + / "over_result_or_empty.trace" + ) + + tests.common.record_or_check(expected_path, got_text) + + class TestGetAllSubmodelsRecentChangesResult(unittest.TestCase): def test_over_result_or_empty_against_recorded(self) -> None: for minimal_or_maximal in ["minimal", "maximal"]: @@ -7732,6 +7769,43 @@ def test_over_result_or_empty_against_recorded(self) -> None: tests.common.record_or_check(expected_path, got_text) +class TestGetSubmodelsValueResult(unittest.TestCase): + def test_over_result_or_empty_against_recorded(self) -> None: + for minimal_or_maximal in ["minimal", "maximal"]: + instance = tests.common_xmlization.must_load( + pathlib.Path( + tests.common.TEST_DATA_DIR + / "Xml" + / "Expected" + / "getSubmodelsValueResult" + / f"{minimal_or_maximal}.xml" + ) + ) + + assert isinstance(instance, our_types.GetSubmodelsValueResult) + + log = [ + tests.common.trace( + [ + tests.common.trace_json(item) + for item in instance.over_result_or_empty() + ] + ) + ] + + got_text = tests.common.trace_log_as_text_file_content(log) + + expected_path = pathlib.Path( + tests.common.TEST_DATA_DIR + / "test_over_X_or_empty" + / "getSubmodelsValueResult" + / f"on_{minimal_or_maximal}.xml" + / "over_result_or_empty.trace" + ) + + tests.common.record_or_check(expected_path, got_text) + + class TestQuery(unittest.TestCase): def test_over_filters_or_empty_against_recorded(self) -> None: for minimal_or_maximal in ["minimal", "maximal"]: diff --git a/dev/tests/test_json_value_verification.py b/dev/tests/test_json_value_verification.py index c607683..7ee7a87 100644 --- a/dev/tests/test_json_value_verification.py +++ b/dev/tests/test_json_value_verification.py @@ -122,6 +122,14 @@ def test_an_integer_too_large_for_a_float_is_refused(self) -> None: "not exactly representable as a JSON number", paths_and_causes[0][1] ) + def test_a_null_is_accepted_at_any_depth(self) -> None: + for value in (None, [None], {"a": [None, {"b": None}]}): + self.assertEqual( + [], + _paths_and_causes(our_json_value_verification.verify_json_value(value)), + f"for the value {value!r}", + ) + def test_a_value_of_an_unexpected_type_is_refused(self) -> None: paths_and_causes = _paths_and_causes( our_json_value_verification.verify_json_value({"a": object()}) diff --git a/dev/tests/test_xml_rpc.py b/dev/tests/test_xml_rpc.py index 2156b45..6921053 100644 --- a/dev/tests/test_xml_rpc.py +++ b/dev/tests/test_xml_rpc.py @@ -43,17 +43,30 @@ def test_a_value_round_trips_through_every_discriminator(self) -> None: f"n1" f"f1.5" f"sx" + f"z" f"a" f"0" + f"" f"" f"" ) self.assertEqual( - {"b": True, "n": 1.0, "f": 1.5, "s": "x", "a": [0.0]}, + {"b": True, "n": 1.0, "f": 1.5, "s": "x", "z": None, "a": [0.0, None]}, our_xmlrpc.read_value_content(element, iterator), ) + def test_a_nil_with_text_is_refused(self) -> None: + element, iterator = _enter( + f'x' + ) + + with self.assertRaises(our_xmlcommon.DeserializationException) as context: + our_xmlrpc.read_value_content(element, iterator) + + self.assertEqual("nil", str(context.exception.path)) + self.assertIn("Expected no text in a element", context.exception.cause) + def test_a_discriminator_in_the_document_namespace_is_refused(self) -> None: # NOTE (mristin): # The XML-RPC elements reside in no namespace at all, so a discriminator @@ -212,6 +225,7 @@ def test_a_value_is_written_with_its_discriminator(self) -> None: (1, "1"), ("aa<b&c"), ([0.0], "0.0"), + ([None], ""), ( {"k": 0.0}, ( @@ -236,6 +250,14 @@ def test_a_value_is_written_with_its_discriminator(self) -> None: f"for the value {value!r}", ) + def test_a_null_is_written_as_an_empty_nil(self) -> None: + stream = io.StringIO() + writer = our_xmlcommon.Writer(stream) + + our_xmlrpc.write_discriminator(None, writer) + + self.assertEqual('', stream.getvalue()) + def test_the_path_of_a_failure_points_into_the_value(self) -> None: stream = io.StringIO() writer = our_xmlcommon.Writer(stream)