Skip to content
Open
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
23 changes: 14 additions & 9 deletions aas_core_codegen/protobuf/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from icontract import ensure, require

from aas_core_codegen import intermediate
from aas_core_codegen.common import Stripped, assert_never, Identifier
from aas_core_codegen.common import Stripped, assert_never
from aas_core_codegen.protobuf import naming as proto_naming


Expand Down Expand Up @@ -138,32 +138,37 @@ def generate_type(type_annotation: intermediate.TypeAnnotationUnion) -> Stripped
return PRIMITIVE_TYPE_MAP[our_type.constrainee]

elif isinstance(our_type, intermediate.Class):

message_name = proto_naming.class_name(our_type.name)

if len(our_type.concrete_descendants) > 0:
# NOTE (TomGneuss):
# We add the suffix "_choice" to the type name because this type
# (either abstract or concrete) has concrete subtypes.
# Thus, a choice-object (with that suffix) will be generated and must
# be used here.
message_name = Identifier(message_name + "_choice")
# If the current type is either interface, abstract, or concrete class
# and has concrete descendants, it must have the name of a
# choice-message to simulate polymorphism
message_name = proto_naming.interface_name(our_type.name)

return Stripped(message_name)

elif isinstance(type_annotation, intermediate.ListTypeAnnotation):
item_type = generate_type(type_annotation=type_annotation.items)

# NOTE (TomGneuss):
# Careful: This does not yet cover the hypothetical scenario where
# type_annotation.items is of type intermediate.OptionalTypeAnnotation.
# As below, constructs like "repeated optional <type> <name>" are invalid.
return Stripped(f"repeated {item_type}")

elif isinstance(type_annotation, intermediate.OptionalTypeAnnotation):
value = generate_type(type_annotation=type_annotation.value)
value_name = generate_type(type_annotation=type_annotation.value)

# NOTE (TomGneuss):
# Careful: do not generate "optional" keyword for list-type elements since
# otherwise we get invalid constructs like "optional repeated <type> <name>".
if isinstance(type_annotation.value, intermediate.ListTypeAnnotation):
return Stripped(f"{value}")
return Stripped(f"{value_name}")
else:
return Stripped(f"optional {value}")
return Stripped(f"optional {value_name}")

else:
assert_never(type_annotation)
Expand Down
6 changes: 4 additions & 2 deletions aas_core_codegen/protobuf/description.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def transform_reference_to_our_type_in_doc(
# NOTE (mristin, 2021-12-25):
# We do not generate ProtoBuf code for abstract classes, so we have to refer
# to the interface.
name = proto_naming.class_name(element.our_type.name)
name = proto_naming.interface_name(element.our_type.name)

elif isinstance(element.our_type, intermediate.ConcreteClass):
# NOTE (mristin, 2021-12-25):
Expand Down Expand Up @@ -227,7 +227,9 @@ def transform_reference_to_attribute_in_doc(
if isinstance(element.reference.cls, intermediate.AbstractClass):
# We do not generate ProtoBuf code for abstract classes, so we have to refer
# to the interface.
name_of_our_type = proto_naming.class_name(element.reference.cls.name)
name_of_our_type = proto_naming.interface_name(
element.reference.cls.name
)
elif isinstance(element.reference.cls, intermediate.ConcreteClass):
# NOTE (mristin, 2021-12-25):
# Though a concrete class can have multiple descendants and the writer
Expand Down
6 changes: 3 additions & 3 deletions aas_core_codegen/protobuf/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,16 @@ def execute(context: run.Context, stdout: TextIO, stderr: TextIO) -> int:
)
return 1

namespace_key = specific_implementations.ImplementationKey("namespace.txt")
namespace_key = specific_implementations.ImplementationKey("package.txt")
namespace_text = context.spec_impls.get(namespace_key, None)
if namespace_text is None:
stderr.write(f"The namespace snippet is missing: {namespace_key}\n")
stderr.write(f"The package snippet is missing: {namespace_key}\n")
return 1

if not proto_common.NAMESPACE_IDENTIFIER_RE.fullmatch(namespace_text):
stderr.write(
f"The text from the snippet {namespace_key} "
f"is not a valid namespace identifier: {namespace_text!r}\n"
f"is not a valid package identifier: {namespace_text!r}\n"
)
return 1

Expand Down
13 changes: 5 additions & 8 deletions aas_core_codegen/protobuf/naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

from typing import Union

from icontract import ensure

import aas_core_codegen.naming
from aas_core_codegen import intermediate
from aas_core_codegen.common import Identifier, assert_never
Expand All @@ -13,9 +11,12 @@ def interface_name(identifier: Identifier) -> Identifier:
"""
Generate a ProtoBuf name for an interface based on its meta-model ``identifier``.

This method is not to be used because proto3 does not support interfaces.
Since proto3 does not directly support interfaces, but only one-of messages
(commonly suffixed "_choice"), these names are generated here.
"""
raise NotImplementedError("Interfaces are not supported by proto3.")
return Identifier(
aas_core_codegen.naming.capitalized_camel_case(identifier) + "_choice"
)


def enum_name(identifier: Identifier) -> Identifier:
Expand Down Expand Up @@ -44,10 +45,6 @@ def enum_literal_name(identifier: Identifier) -> Identifier:
return aas_core_codegen.naming.upper_snake_case(identifier)


@ensure(
lambda result: "_" not in result,
"No underscode allowed so that we can attached our own suffixes such as ``_choice``",
)
def class_name(identifier: Identifier) -> Identifier:
"""
Generate a ProtoBuf name for a class based on its meta-model ``identifier``.
Expand Down
28 changes: 19 additions & 9 deletions aas_core_codegen/protobuf/structure/_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ def _generate_enum(

# write enum and its name
writer.write(f"enum {name} {{\n")
# write at least the unspecified enum entry
# write at least the default enum literal
writer.write(textwrap.indent(f"{proto_naming.enum_name(name)}_UNSPECIFIED = 0;", I))

if len(enum.literals) == 0:
Expand Down Expand Up @@ -275,13 +275,16 @@ def _generate_enum(
writer.write(textwrap.indent(literal_comment, I))
writer.write("\n")

# Enums cannot have string-values assigned to them in proto3. Instead, they each get assigned
# an ID that is used for (de-)serialization.
# If that ID is re-assigned to another literal in the same enum in a later version, a system using the
# old version will (de-)serialize that literal differently. Hence, hope that the order of writing the literals
# stays the same in each build so that one literal always gets the same ID. Otherwise, don't mix versions.
# With each version, compare to the previous one and assign same ID.
# With each version, add a `reserved`-statement for deleted literals and their IDs.
# Note (TomGneuss):
# Enums cannot have string-values assigned to them in proto3. Instead, they
# each get an ID assigned that is used for (de-)serialization. If that ID is
# re-assigned to another literal in the same enum in a later version, a system
# using the old version will (de-)serialize that literal differently.
# Hence, hope that the order of writing the literals stays the same in each
# build so that existing literals always get the same ID (backward compatible).
# Otherwise, don't mix versions. Ideally, with each version, compare to the
# previous one and assign the same ID. With every new version, add a
# `reserved`-statement for deleted literals and their IDs.
writer.write(
textwrap.indent(
f"""\
Expand Down Expand Up @@ -383,6 +386,10 @@ def _generate_choice_class(cls: intermediate.ClassUnion) -> Stripped:

concrete_classes = []
if isinstance(cls, intermediate.ConcreteClass):
# NOTE (TomGneuss):
# The type cls has concrete descendants but is itself concrete, so it must
# be listed as one of its own subtypes in the choice message because that
# is how proto3 tries to model polymorphism
concrete_classes.append(cls)

concrete_classes.extend(cls.concrete_descendants)
Expand All @@ -392,7 +399,7 @@ def _generate_choice_class(cls: intermediate.ClassUnion) -> Stripped:
subtype_name = proto_naming.property_name(subtype.name)
fields.append(Stripped(f"{subtype_type} {subtype_name} = {j + 1};"))

message_name = Identifier(proto_naming.class_name(cls.name) + "_choice")
message_name = Identifier(proto_naming.interface_name(cls.name))

fields_joined = "\n".join(fields)

Expand Down Expand Up @@ -425,6 +432,7 @@ def generate(

errors = [] # type: List[Error]

# generate message definitions for concrete classes and enums first
for our_type in symbol_table.our_types:
if not isinstance(
our_type,
Expand Down Expand Up @@ -467,6 +475,8 @@ def generate(
else:
assert_never(our_type)

# generate choice messages for all interfaces and abstract or concrete classes
# that have concrete descendants
for cls in symbol_table.classes:
if len(cls.concrete_descendants) > 0:
code_blocks.append(_generate_choice_class(cls))
Expand Down
1 change: 1 addition & 0 deletions dev_scripts/run_tests_with_rerecord.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def main() -> int:
"tests.smoke.test_main.Test_against_recorded",
"tests.typescript.test_main.Test_against_recorded",
"tests.xsd.test_main.Test_against_recorded",
"tests.proto.test_main.Test_against_recorded",
]

parser = argparse.ArgumentParser(description=__doc__)
Expand Down
Loading