From dc2d21cd89a47622e82c44cc6781ff17cee3036e Mon Sep 17 00:00:00 2001 From: Marko Ristin Date: Sun, 27 Oct 2024 10:45:01 +0100 Subject: [PATCH] Add choices for concrete classes in Protobuf (#531) We add additional choice classes corresponding to concrete classes with concrete descendants in Protobuf. So far, we omitted this case as the concrete classes with concrete descendants do not appear in the AAS meta-models. However, we decide to make the implementation future-proof in that regard as it is very possible that in the near future we do face concrete classes with descendants in properties. --- aas_core_codegen/protobuf/common.py | 43 +++-- aas_core_codegen/protobuf/main.py | 4 +- aas_core_codegen/protobuf/naming.py | 6 + .../protobuf/structure/_generate.py | 116 ++++++-------- .../expected_output/types.proto | 151 +++++++++++++++++- .../expected_output/stdout.txt | 1 + .../expected_output/types.proto | 37 +++++ .../input/snippets/namespace.txt | 1 + .../meta_model.py | 30 ++++ 9 files changed, 292 insertions(+), 97 deletions(-) create mode 100644 test_data/proto/test_main/expected/concrete_class_with_descendants/expected_output/stdout.txt create mode 100644 test_data/proto/test_main/expected/concrete_class_with_descendants/expected_output/types.proto create mode 100644 test_data/proto/test_main/expected/concrete_class_with_descendants/input/snippets/namespace.txt create mode 100644 test_data/proto/test_main/expected/concrete_class_with_descendants/meta_model.py diff --git a/aas_core_codegen/protobuf/common.py b/aas_core_codegen/protobuf/common.py index 9f68232e1..b6b84dbcf 100644 --- a/aas_core_codegen/protobuf/common.py +++ b/aas_core_codegen/protobuf/common.py @@ -119,23 +119,12 @@ def _assert_all_primitive_types_are_mapped() -> None: _assert_all_primitive_types_are_mapped() -# fmt: off -@require( - lambda our_type_qualifier: - not (our_type_qualifier is not None) - or not our_type_qualifier.endswith('.') -) -# fmt: on -def generate_type( - type_annotation: intermediate.TypeAnnotationUnion, - our_type_qualifier: Optional[Stripped] = None, -) -> Stripped: +def generate_type(type_annotation: intermediate.TypeAnnotationUnion) -> Stripped: """ Generate the ProtoBuf type for the given type annotation. ``our_type_prefix`` is appended to all our types, if specified. """ - our_type_prefix = "" if our_type_qualifier is None else f"{our_type_qualifier}." if isinstance(type_annotation, intermediate.PrimitiveTypeAnnotation): return PRIMITIVE_TYPE_MAP[type_annotation.a_type] @@ -143,30 +132,36 @@ def generate_type( our_type = type_annotation.our_type if isinstance(our_type, intermediate.Enumeration): - return Stripped( - our_type_prefix + proto_naming.enum_name(type_annotation.our_type.name) - ) + return Stripped(proto_naming.enum_name(type_annotation.our_type.name)) elif isinstance(our_type, intermediate.ConstrainedPrimitive): return PRIMITIVE_TYPE_MAP[our_type.constrainee] elif isinstance(our_type, intermediate.Class): - return Stripped(our_type_prefix + proto_naming.class_name(our_type.name)) + message_name = proto_naming.class_name(our_type.name) + if ( + isinstance(our_type, intermediate.ConcreteClass) + and len(our_type.concrete_descendants) > 0 + ): + # NOTE (mristin): + # We have to add the suffix ``_choice`` since this field points + # to one of the concrete descendants of the class as well as + # the concrete class itself. + message_name += "_choice" + + return Stripped(message_name) elif isinstance(type_annotation, intermediate.ListTypeAnnotation): - item_type = generate_type( - type_annotation=type_annotation.items, our_type_qualifier=our_type_qualifier - ) + item_type = generate_type(type_annotation=type_annotation.items) return Stripped(f"repeated {item_type}") elif isinstance(type_annotation, intermediate.OptionalTypeAnnotation): - value = generate_type( - type_annotation=type_annotation.value, our_type_qualifier=our_type_qualifier - ) + value = generate_type(type_annotation=type_annotation.value) - # careful: do not generate "optional" keyword for list-type elements since otherwise we get invalid - # constructs like "optional repeated " + # NOTE (TomGneuss): + # Careful: do not generate "optional" keyword for list-type elements since + # otherwise we get invalid constructs like "optional repeated ". if isinstance(type_annotation.value, intermediate.ListTypeAnnotation): return Stripped(f"{value}") else: diff --git a/aas_core_codegen/protobuf/main.py b/aas_core_codegen/protobuf/main.py index b2e8b74db..298f379b3 100644 --- a/aas_core_codegen/protobuf/main.py +++ b/aas_core_codegen/protobuf/main.py @@ -84,9 +84,7 @@ def execute(context: run.Context, stdout: TextIO, stderr: TextIO) -> int: # region Structure code, errors = proto_structure.generate( - symbol_table=verified_ir_table, - namespace=namespace, - spec_impls=context.spec_impls, + symbol_table=verified_ir_table, namespace=namespace ) if errors is not None: diff --git a/aas_core_codegen/protobuf/naming.py b/aas_core_codegen/protobuf/naming.py index 4e1b5627b..d0761b507 100644 --- a/aas_core_codegen/protobuf/naming.py +++ b/aas_core_codegen/protobuf/naming.py @@ -2,6 +2,8 @@ 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 @@ -42,6 +44,10 @@ 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``. diff --git a/aas_core_codegen/protobuf/structure/_generate.py b/aas_core_codegen/protobuf/structure/_generate.py index 6d6ccd256..c641c6304 100644 --- a/aas_core_codegen/protobuf/structure/_generate.py +++ b/aas_core_codegen/protobuf/structure/_generate.py @@ -21,6 +21,7 @@ Identifier, assert_never, Stripped, + indent_but_first_line, ) from aas_core_codegen.protobuf import ( common as proto_common, @@ -301,17 +302,14 @@ def _generate_enum( @ensure(lambda result: (result[0] is None) ^ (result[1] is None)) def _generate_class( cls: intermediate.ConcreteClass, -) -> Tuple[Optional[Stripped], Optional[Error], List[intermediate.AbstractClass],]: +) -> Tuple[Optional[Stripped], Optional[Error]]: """Generate ProtoBuf code for the given concrete class ``cls``.""" # Code blocks to be later joined by double newlines and indented once blocks = [] # type: List[Stripped] - required_choice_object = [] # type: List[intermediate.AbstractClass] - # region Getters and setters for i, prop in enumerate(cls.properties): prop_type = proto_common.generate_type(type_annotation=prop.type_annotation) - prop_name = proto_naming.property_name(prop.name) prop_blocks = [] # type: List[Stripped] @@ -330,57 +328,13 @@ def _generate_class( f"for the property {prop.name!r}", prop_comment_errors, ), - [], ) assert prop_comment is not None prop_blocks.append(prop_comment) - # lists of our types where our types are abstract/interfaces - if ( - isinstance(prop.type_annotation, intermediate.ListTypeAnnotation) - and isinstance(prop.type_annotation.items, intermediate.OurTypeAnnotation) - and isinstance( - prop.type_annotation.items.our_type, - (intermediate.Interface, intermediate.AbstractClass), - ) - ): - # -> must create a new message (choice object) since "oneof" - # and "repeated" do not go together - prop_blocks.append(Stripped(f"{prop_type} {prop_name} = {i + 1};")) - required_choice_object.append(prop.type_annotation.items.our_type) - - # optional lists of our types where our types are abstract/interfaces - elif ( - isinstance(prop.type_annotation, intermediate.OptionalTypeAnnotation) - and isinstance(prop.type_annotation.value, intermediate.ListTypeAnnotation) - and isinstance( - prop.type_annotation.value.items, intermediate.OurTypeAnnotation - ) - and isinstance( - prop.type_annotation.value.items.our_type, - (intermediate.Interface, intermediate.AbstractClass), - ) - ): - # -> same as the case before - prop_blocks.append(Stripped(f"{prop_type} {prop_name} = {i + 1};")) - required_choice_object.append(prop.type_annotation.value.items.our_type) - - # our types where our types are abstract/interfaces - elif isinstance( - prop.type_annotation, intermediate.OurTypeAnnotation - ) and isinstance( - prop.type_annotation.our_type, - (intermediate.Interface, intermediate.AbstractClass), - ): - # -> must use "oneof" - prop_blocks.append(Stripped(f"{prop_type} {prop_name} = {i + 1};")) - required_choice_object.append(prop.type_annotation.our_type) - - else: - # just a normal property with type - prop_blocks.append(Stripped(f"{prop_type} {prop_name} = {i + 1};")) + prop_blocks.append(Stripped(f"{prop_type} {prop_name} = {i + 1};")) blocks.append(Stripped("\n".join(prop_blocks))) @@ -402,7 +356,6 @@ def _generate_class( "Failed to generate the comment description", comment_errors, ), - [], ) assert comment is not None @@ -420,19 +373,52 @@ def _generate_class( writer.write("\n}") - return Stripped(writer.getvalue()), None, required_choice_object + return Stripped(writer.getvalue()), None + + +@require( + lambda cls: len(cls.concrete_descendants) > 0, + "No choice possible if no concrete descendants", +) +def _generate_choice_class(cls: intermediate.ClassUnion) -> Stripped: + fields = [] # type: List[Stripped] + concrete_classes = [] + if isinstance(cls, intermediate.ConcreteClass): + concrete_classes.append(cls) -def _generate_choice_class(cls: intermediate.AbstractClass) -> str: - msg_header = f"message {proto_naming.class_name(cls.name)} {{\n" - msg_body = f"{I}oneof value {{\n" + concrete_classes.extend(cls.concrete_descendants) - for j, subtype in enumerate(cls.concrete_descendants): + for j, subtype in enumerate(concrete_classes): subtype_type = proto_naming.class_name(subtype.name) subtype_name = proto_naming.property_name(subtype.name) - msg_body += f"{II}{subtype_type} {subtype_name} = {j + 1};\n" + fields.append(Stripped(f"{subtype_type} {subtype_name} = {j + 1};")) - return msg_header + msg_body + f"{I}}}\n}}" + if isinstance(cls, intermediate.AbstractClass): + message_name = proto_naming.class_name(cls.name) + elif isinstance(cls, intermediate.ConcreteClass): + # NOTE (mristin): + # We have to append the ``_choice`` suffix for concrete classes with descendants + # to distinguish between the concrete classes and one-of choice classes. + # Protocol Buffers do not support inheritance, so we have to work around that + # circumstance. + + message_name = proto_naming.class_name(cls.name) + "_choice" + + else: + assert_never(cls) + raise AssertionError("Unexpected execution path") + + fields_joined = "\n".join(fields) + + return Stripped( + f"""\ +message {message_name} {{ +{I}oneof value {{ +{II}{indent_but_first_line(fields_joined, II)} +{I}}} +}}""" + ) @ensure(lambda result: (result[0] is not None) ^ (result[1] is not None)) @@ -444,7 +430,6 @@ def _generate_choice_class(cls: intermediate.AbstractClass) -> str: def generate( symbol_table: VerifiedIntermediateSymbolTable, namespace: proto_common.NamespaceIdentifier, - spec_impls: specific_implementations.SpecificImplementations, ) -> Tuple[Optional[str], Optional[List[Error]]]: """ Generate the ProtoBuf code of the structures based on the symbol table. @@ -460,15 +445,12 @@ def generate( for our_type in symbol_table.our_types: if not isinstance( our_type, - ( - intermediate.Enumeration, - intermediate.ConcreteClass, - ), + (intermediate.Enumeration, intermediate.ConcreteClass), ): continue if isinstance(our_type, intermediate.ConcreteClass): - code, error, choice_obj = _generate_class(cls=our_type) + code, error = _generate_class(cls=our_type) if error is not None: errors.append( Error( @@ -482,7 +464,6 @@ def generate( assert code is not None code_blocks.append(code) - required_choice_objects = required_choice_objects.union(set(choice_obj)) elif isinstance(our_type, intermediate.Enumeration): code, error = _generate_enum(enum=our_type) @@ -503,10 +484,9 @@ def generate( else: assert_never(our_type) - # generate the necessary classes for choice (i.e. a class for every property that - # was like "repeated ") - for cls in required_choice_objects: - code_blocks.append(Stripped(_generate_choice_class(cls))) + for cls in symbol_table.classes: + if len(cls.concrete_descendants) > 0: + code_blocks.append(_generate_choice_class(cls)) if len(errors) > 0: return None, errors diff --git a/test_data/proto/test_main/expected/aas_core_meta.v3/expected_output/types.proto b/test_data/proto/test_main/expected/aas_core_meta.v3/expected_output/types.proto index 2603ab22e..ea60bcfa6 100644 --- a/test_data/proto/test_main/expected/aas_core_meta.v3/expected_output/types.proto +++ b/test_data/proto/test_main/expected/aas_core_meta.v3/expected_output/types.proto @@ -3738,6 +3738,130 @@ message DataSpecificationIec61360 { optional LevelType level_type = 12; } +message HasSemantics { + oneof value { + RelationshipElement relationship_element = 1; + AnnotatedRelationshipElement annotated_relationship_element = 2; + BasicEventElement basic_event_element = 3; + Blob blob = 4; + Capability capability = 5; + Entity entity = 6; + Extension extension = 7; + File file = 8; + MultiLanguageProperty multi_language_property = 9; + Operation operation = 10; + Property property = 11; + Qualifier qualifier = 12; + Range range = 13; + ReferenceElement reference_element = 14; + SpecificAssetId specific_asset_id = 15; + Submodel submodel = 16; + SubmodelElementCollection submodel_element_collection = 17; + SubmodelElementList submodel_element_list = 18; + } +} + +message HasExtensions { + oneof value { + RelationshipElement relationship_element = 1; + AnnotatedRelationshipElement annotated_relationship_element = 2; + AssetAdministrationShell asset_administration_shell = 3; + BasicEventElement basic_event_element = 4; + Blob blob = 5; + Capability capability = 6; + ConceptDescription concept_description = 7; + Entity entity = 8; + File file = 9; + MultiLanguageProperty multi_language_property = 10; + Operation operation = 11; + Property property = 12; + Range range = 13; + ReferenceElement reference_element = 14; + Submodel submodel = 15; + SubmodelElementCollection submodel_element_collection = 16; + SubmodelElementList submodel_element_list = 17; + } +} + +message Referable { + oneof value { + RelationshipElement relationship_element = 1; + AnnotatedRelationshipElement annotated_relationship_element = 2; + AssetAdministrationShell asset_administration_shell = 3; + BasicEventElement basic_event_element = 4; + Blob blob = 5; + Capability capability = 6; + ConceptDescription concept_description = 7; + Entity entity = 8; + File file = 9; + MultiLanguageProperty multi_language_property = 10; + Operation operation = 11; + Property property = 12; + Range range = 13; + ReferenceElement reference_element = 14; + Submodel submodel = 15; + SubmodelElementCollection submodel_element_collection = 16; + SubmodelElementList submodel_element_list = 17; + } +} + +message Identifiable { + oneof value { + AssetAdministrationShell asset_administration_shell = 1; + ConceptDescription concept_description = 2; + Submodel submodel = 3; + } +} + +message HasKind { + oneof value { + Submodel submodel = 1; + } +} + +message HasDataSpecification { + oneof value { + AdministrativeInformation administrative_information = 1; + RelationshipElement relationship_element = 2; + AnnotatedRelationshipElement annotated_relationship_element = 3; + AssetAdministrationShell asset_administration_shell = 4; + BasicEventElement basic_event_element = 5; + Blob blob = 6; + Capability capability = 7; + ConceptDescription concept_description = 8; + Entity entity = 9; + File file = 10; + MultiLanguageProperty multi_language_property = 11; + Operation operation = 12; + Property property = 13; + Range range = 14; + ReferenceElement reference_element = 15; + Submodel submodel = 16; + SubmodelElementCollection submodel_element_collection = 17; + SubmodelElementList submodel_element_list = 18; + } +} + +message Qualifiable { + oneof value { + RelationshipElement relationship_element = 1; + AnnotatedRelationshipElement annotated_relationship_element = 2; + BasicEventElement basic_event_element = 3; + Blob blob = 4; + Capability capability = 5; + Entity entity = 6; + File file = 7; + MultiLanguageProperty multi_language_property = 8; + Operation operation = 9; + Property property = 10; + Range range = 11; + ReferenceElement reference_element = 12; + Submodel submodel = 13; + SubmodelElementCollection submodel_element_collection = 14; + SubmodelElementList submodel_element_list = 15; + } +} + message SubmodelElement { oneof value { RelationshipElement relationship_element = 1; @@ -3757,9 +3881,10 @@ message SubmodelElement { } } -message DataSpecificationContent { +message RelationshipElement_choice { oneof value { - DataSpecificationIec61360 data_specification_iec_61360 = 1; + RelationshipElement relationship_element = 1; + AnnotatedRelationshipElement annotated_relationship_element = 2; } } @@ -3774,6 +3899,28 @@ message DataElement { } } +message EventElement { + oneof value { + BasicEventElement basic_event_element = 1; + } +} + +message AbstractLangString { + oneof value { + LangStringDefinitionTypeIec61360 lang_string_definition_type_iec_61360 = 1; + LangStringNameType lang_string_name_type = 2; + LangStringPreferredNameTypeIec61360 lang_string_preferred_name_type_iec_61360 = 3; + LangStringShortNameTypeIec61360 lang_string_short_name_type_iec_61360 = 4; + LangStringTextType lang_string_text_type = 5; + } +} + +message DataSpecificationContent { + oneof value { + DataSpecificationIec61360 data_specification_iec_61360 = 1; + } +} + /* * This code has been automatically generated by aas-core-codegen. * Do NOT edit or append. diff --git a/test_data/proto/test_main/expected/concrete_class_with_descendants/expected_output/stdout.txt b/test_data/proto/test_main/expected/concrete_class_with_descendants/expected_output/stdout.txt new file mode 100644 index 000000000..2d755fc5a --- /dev/null +++ b/test_data/proto/test_main/expected/concrete_class_with_descendants/expected_output/stdout.txt @@ -0,0 +1 @@ +Code generated to: diff --git a/test_data/proto/test_main/expected/concrete_class_with_descendants/expected_output/types.proto b/test_data/proto/test_main/expected/concrete_class_with_descendants/expected_output/types.proto new file mode 100644 index 000000000..fe12f6be9 --- /dev/null +++ b/test_data/proto/test_main/expected/concrete_class_with_descendants/expected_output/types.proto @@ -0,0 +1,37 @@ +/* + * This code has been automatically generated by aas-core-codegen. + * Do NOT edit or append. + */ + +syntax = "proto3"; + +package dummy; + + +message Something { + string some_str = 1; +} + +message MoreConcrete { + string some_str = 1; + + string another_str = 2; +} + +message Container { + Something_choice something = 1; + + repeated Something_choice list_of_somethings = 2; +} + +message Something_choice { + oneof value { + Something something = 1; + MoreConcrete more_concrete = 2; + } +} + +/* + * This code has been automatically generated by aas-core-codegen. + * Do NOT edit or append. + */ diff --git a/test_data/proto/test_main/expected/concrete_class_with_descendants/input/snippets/namespace.txt b/test_data/proto/test_main/expected/concrete_class_with_descendants/input/snippets/namespace.txt new file mode 100644 index 000000000..2995a4d0e --- /dev/null +++ b/test_data/proto/test_main/expected/concrete_class_with_descendants/input/snippets/namespace.txt @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/test_data/proto/test_main/expected/concrete_class_with_descendants/meta_model.py b/test_data/proto/test_main/expected/concrete_class_with_descendants/meta_model.py new file mode 100644 index 000000000..01fab5912 --- /dev/null +++ b/test_data/proto/test_main/expected/concrete_class_with_descendants/meta_model.py @@ -0,0 +1,30 @@ +@serialization(with_model_type=True) +class Something: + some_str: str + + def __init__(self, some_str: str) -> None: + self.some_str = some_str + + +class More_concrete(Something): + another_str: str + + def __init__(self, some_str: str, another_str: str) -> None: + Something.__init__(self, some_str) + self.another_str = another_str + + +class Container: + something: Something + list_of_somethings: List[Something] + + def __init__( + self, something: Something, list_of_somethings: List[Something] + ) -> None: + self.something = something + self.list_of_somethings = list_of_somethings + + +__version__ = "V198.4" + +__xml_namespace__ = "https://dummy/198/4"