Documentation on toAron/fromAron
As a follow up from a discussion with @plewnia in February, I would suggest to add the following to the documentation of Aron:
toAron/fromAron
There are several versions of functions named toAron
/fromAron
or similar.
To avoid confusion, their differences are explained in the following.
First, note that there are 3 representations being involved when doing conversions to or from ARON:
- The business object,
bo
, is the C++/Python/... class that you actually use in your code. Typically, it does not only contain the data, but provide methods to act on them. It is not specific to ARON in any way. - The data transfer object,
dto
, is an autogenerated C++/Python/... class that is created from the type definition in the ARON .xml file. It is the representation of the type submitted via network in the high-level language. In C++, they typically reside in anarondto::
(sub)namespace - The "aron dto"/"ice" representation (the wording is less clear here), which is the low-level serialized form that can actually be transmitted of the network.
In C++
The toAron/fromAron functions defined in aron_conversions.h convert between the first and second layer. They typically take two arguments, namely toAron(dto, const bo)
respectively fromAron(const dto, bo)
. In some cases, methods returning the result instead of writing it to one of the parameters exist as well, i.e., dto = toAron(const bo)
respectively bo = fromAron(const dto)
, which take only one argument.
In contrast, there are toAron/fromAron functions that do not take any arguments. The convert between the second and third layer. In C++, they are autogenerated methods of the second layer's dto types.
This means, myDto.toAron()
of a dto defined in arondto::
converts from the second to the third layer. If you instead want to convert a first-layer business object to a second-layer data transfer object, this is not what you need. You might think of an bo.toAron()
method, but this usually does not exist - and shall not exist, as it would result in the business object having to be aware of the existence of aron. Instead, use the arondto::toAron(dto, const bo)
.
In Python
The first layer types again are the normal python classes defined in the application code. For the second layer, standardization in terms of where to put it in the code is currently lower than in the C++ case. However, the second layer types are always python data classes, which shall inherit from AronDataclass
defined in the armarx_memory.aron.aron_dataclass
module.
To convert between the first and second layer, one option is to define class methods on the second layer type. They take one argument, and have the signatures to_aron(dto) -> bo
/from_aron(bo) -> dto
.
To convert between the second and third layer, AronDataclass types inherit from_aron_ice(aron_ice_data)
and to_aron_ice()
methods. The aron_ice_data are the ones provided to the callback in the for_each_instance_data_ice()
function.
Example for toAron/fromAron in python [going beyond what was discussed yet]
Defining the types
Business object, i.e., first layer:
class TheType:
def __init__(some_attribute: int, some_optional_attribute: float or None = None):
self.some_attribute: int = some_attribute
self.some_optional_attribute: float or None = some_optional_attribute
# not all variables of the bo need to be reflected in the dataclass, e.g.:
is_ready: bool = False
def do_something():
pass
Data transfer object, i.e., second layer:
# if the following is impossible in your python version, leave it out and
# put type annotations referring to the own class in "quotation" marks
from __future__ import annotations
from dataclasses import dataclass
from armarx_memory.aron.aron_dataclass import AronDataclass
# "as ...Bo" is recommended for clarity
from path.of.the.bo_module import TheType as TheTypeBo
@dataclass
class TheTypeDto(AronDataclass):
# for optionals, it is important to use the Union notation here, not `or`!
some_attribute: int
some_optional_attribute: Union[float, None]
@classmethod
def from_aron(cls, dto: TheTypeDto) -> TheTypeBo:
bo = TheTypeBo(
dto.some_attribute,
dto.some_optional_attribute
# if a parameter is a complex type itself, use something like:
# TheOtherTypeDto.from_aron(dto.the_complex_attribute)
)
return bo
@classmethod
def to_aron(cls, bo: TheTypeBo) -> TheTypeDto:
dto = cls(
bo.some_attribute,
bo.some_optional_attribute
)
return dto
Conversions
In the following examples, the usage of to_aron and from_aron conversions is embedded into memory operations:
Reading from a memory
from armarx_memory.client import MemoryNameSystem, Reader
from armarx_memory.core import MemoryID
# Despite typically being imported as "dto", it refers to the
# third layer, not the second one!
from armarx.armem import data as dto
from armarx_memory.aron.aron_ice_types import AronIceTypes
core_segment_id: MemoryID = MemoryID('MemoryName', 'CoreSegmentName')
mns = MemoryNameSystem.wait_for_mns()
reader = mns.get_reader(core_segment_id)
query_result = reader.query_core_segment(
some_memory_id.core_segment_name, latest_snapshot=True
)
bo_results: List[TheType] = []
# covering all conversions from layer three to layer one
# (the second parameter, "metadata", can be omitted, if
# provide_metadata is set to False.)
def from_aron_ice_to_bo(
memory_id: dto.MemoryID,
metadata: dto.EntityInstanceMetadata,
aron_ice_data: AronIceTypes.Dict
) -> None:
# here, from_ice not only converts to a dto, which
# needs to be passed to from_aron afterwards, but
# directly yields the bo
memory_id: MemoryID = MemoryID.from_ice(id)
dto = TheTypeDto.from_aron_ice(aron_ice_data)
bo = TheTypeDto.from_aron(dto)
bo_results.append(bo)
reader.for_each_instance_data_ice(
from_aron_ice_to_bo, query_result, provide_metadata=True
)
As a python speciality, there is not only the dictionary representing the third layer, but a pythonic
representation between the second and third layer.
Usually, it is used only internally, but can be useful for debugging, if conversions fail. It is much easier to read than the verbose dictionary of the third layer.
from armarx_memory.aron.conversion.pythonic_from_to_aron_ice import pythonic_from_aron_ice
...
def from_aron_ice_to_bo(
memory_id: dto.MemoryID,
metadata: dto.EntityInstanceMetadata,
aron_ice_data: AronIceTypes.Dict
) -> None:
print(
f'Data for memory id {MemoryID.from_ice(memory_id)} are:\n'
f'{pythonic_from_aron_ice(aron_ice_data)}'
)
...
...
Writing to a memory
from armarx_memory.client import MemoryNameSystem, Writer, Commit
from armarx_memory.core import MemoryID, time_usec
core_segment_id: MemoryID = MemoryID('MemoryName', 'CoreSegmentName')
mns = MemoryNameSystem.wait_for_mns()
writer = mns.get_writer(core_segment_id)
bo: TheType = TheType(5, 5.2)
commmit: Commit = Commit()
dto = TheTypeDto.to_aron(bo)
commit.add(
entity_id=MemoryID(
core_segment_id.memory_name,
core_segment_id.core_segment_name,
"ProviderSegmentName",
"EntityName"
),
referenced_time_usec=time_usec(),
instances_data=[dto.to_aron_ice()]
)
writer.commit(commit)
- Can these explanations please get approved?
- Empirically, it seems that the core segment of a reader/writer can be changed. Is it perhaps sufficient to provide a memory ID containing only the memory name?
- Could we use a less ambiguous name for the import of "from armarx.armem import data as dto"? However,
aron_ice
orice_types
would also be ambiguous, due to the use in armarx_memory.aron.aron_ice_types.