Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions aas_core3_2/jsonization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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)}"
)


Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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)}"
)


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 7 additions & 4 deletions aas_core3_2/jsonvalueverification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)}"
)


Expand Down
64 changes: 47 additions & 17 deletions aas_core3_2/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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]:
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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]:
"""
Expand Down Expand Up @@ -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
Expand Down
24 changes: 2 additions & 22 deletions aas_core3_2/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions aas_core3_2/xmlcommon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"</{name}>")

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`.
Expand Down
Loading
Loading