From 90f17ecd0c19a972fa62ebe847b2efd7730a246a Mon Sep 17 00:00:00 2001 From: timatifey Date: Wed, 16 Nov 2022 09:34:52 +0300 Subject: [PATCH] properties class properties class declaration 1) properties class declaration 2) config header update 3) getJsonProperties function declaration 4) mergeWith function declaration --- api_generator/api_generator/config.py | 1 + .../generators/divan/divan_entities.py | 266 ++++++++++++++++++ .../generators/divan/generator.py | 91 +++++- api_generator/api_generator/utils.py | 4 + api_generator/tests/configs/divan_config.json | 6 +- 5 files changed, 359 insertions(+), 9 deletions(-) create mode 100644 api_generator/api_generator/generators/divan/divan_entities.py diff --git a/api_generator/api_generator/config.py b/api_generator/api_generator/config.py index 9c878b0c8..3b7106a56 100644 --- a/api_generator/api_generator/config.py +++ b/api_generator/api_generator/config.py @@ -57,6 +57,7 @@ class Config: self.header: Text = dictionary.get('header') or '' self.errors_collectors: List[str] = dictionary.get('errorsCollectors') or [] self.kotlin_annotations: List[str] = dictionary.get('kotlinAnnotations') or [] + self.top_level_annotations: List[str] = dictionary.get('topLevelAnnotations') or [] self.generate_equality: bool = dictionary.get('generateEquality') or False def __init__(self, generator_path: Optional[str], config_path: str, schema_path: str, output_path: str): diff --git a/api_generator/api_generator/generators/divan/divan_entities.py b/api_generator/api_generator/generators/divan/divan_entities.py new file mode 100644 index 000000000..41ddc6c37 --- /dev/null +++ b/api_generator/api_generator/generators/divan/divan_entities.py @@ -0,0 +1,266 @@ +from __future__ import annotations +from typing import List, cast + +from ...schema.modeling.entities import ( + Entity, + EntityEnumeration, + StringEnumeration, + Declarable, + Property, + PropertyType, + Int, + Bool, + BoolInt, + Double, + StaticString, + Object, + Array, + Url, + Color, + String, + Dictionary, +) +from ... import utils +from ...schema.modeling.text import Text + + +def update_base(obj: Declarable) -> Declarable: + if isinstance(obj, Entity): + obj.__class__ = DivanEntity + cast(DivanEntity, obj).update_base() + elif isinstance(obj, EntityEnumeration): + obj.__class__ = DivanEntityEnumeration + cast(DivanEntityEnumeration, obj).update_base() + elif isinstance(obj, StringEnumeration): + obj.__class__ = DivanStringEnumeration + return obj + + +def comment(*lines) -> Text: + comment_block = Text('/**') + for line in lines: + comment_block += f' * {line}' + comment_block += ' */' + return comment_block + + +def default_value_comment(value) -> Text: + return comment(f'Значение по умолчанию: {value}') + + +def update_property_type_base(property_type: PropertyType): + if isinstance(property_type, Array): + property_type.__class__ = DivanArray + property_type = cast(DivanArray, property_type) + property_type.update_base() + elif isinstance(property_type, Bool): + property_type.__class__ = DivanBool + elif isinstance(property_type, BoolInt): + property_type.__class__ = DivanBoolInt + elif isinstance(property_type, Color): + property_type.__class__ = DivanColor + elif isinstance(property_type, Dictionary): + property_type.__class__ = DivanDictionary + elif isinstance(property_type, Double): + property_type.__class__ = DivanDouble + elif isinstance(property_type, Int): + property_type.__class__ = DivanInt + elif isinstance(property_type, Object): + property_type.__class__ = DivanObject + property_type = cast(DivanObject, property_type) + property_type.update_base() + elif isinstance(property_type, StaticString): + property_type.__class__ = DivanStaticString + elif isinstance(property_type, String): + property_type.__class__ = DivanString + elif isinstance(property_type, Url): + property_type.__class__ = DivanUrl + else: + raise NotImplementedError + + +class DivanEntity(Entity): + def update_base(self): + for prop in self.properties: + prop.__class__ = DivanProperty + cast(DivanProperty, prop).update_base() + + @property + def supertype_declaration(self) -> str: + supertypes = [] + enumeration = self.enclosing_enumeration + if enumeration is not None: + supertypes.append(f'{utils.capitalize_snake_case(enumeration.name)}()') + if self.root_entity: + supertypes.append('Root') + additional_supertypes = self.protocol_plus_super_entities + if additional_supertypes is not None: + supertypes.append(additional_supertypes) + if not supertypes: + return '' + return f' : {", ".join(supertypes)}' + + @property + def header_comment_block(self) -> Text: + required_prop_names = sorted([prop.name for prop in self.properties if not prop.optional], reverse=True) + factory_method_name = utils.snake_case(self.name) # TODO(use parent naming) + lines = [ + '', + f'Можно создать при помощи метода [{factory_method_name}].' + '', + f'Обязательные поля: {", ".join(required_prop_names)}' + ] + return comment(*lines) + + @property + def serialization_declaration(self) -> Text: + result = Text() + static_type = self.static_type + if static_type is None: + result += '@JsonAnyGetter' + result += 'internal fun getJsonProperties(): Map = properties.mergeWith(emptyMap())' + else: + result += '@JsonAnyGetter' + result += 'internal fun getJsonProperties(): Map = properties.mergeWith(' + result += f' mapOf("type" to "{static_type}")' + result += ')' + return result + + @property + def properties_class_declaration(self) -> Text: + declaration = Text() + declaration += 'class Properties internal constructor(' + for prop in cast(List[DivanProperty], self.instance_properties): + declaration += f'{prop.constructor_parameter_declaration.indented(indent_width=4)},' + declaration += ') {' + merge_with_declaration = Text('internal fun mergeWith(properties: Map): Map {') + merge_with_declaration += ' val result = mutableMapOf()' + merge_with_declaration += ' result.putAll(properties)' + for prop in cast(List[DivanProperty], self.instance_properties): + merge_with_declaration += f' result.tryPutProperty("{prop.name}", {prop.name_declaration})' + merge_with_declaration += ' return result' + merge_with_declaration += '}' + declaration += merge_with_declaration.indented(indent_width=4) + declaration += '}' + return declaration + + +class DivanEntityEnumeration(EntityEnumeration): + def update_base(self): + self._entities = list(map(lambda e: (e[0], update_base(e[1])), self._entities)) + + +class DivanStringEnumeration(StringEnumeration): + pass + + +class DivanProperty(Property): + def update_base(self): + update_property_type_base(self.property_type) + + @property + def constructor_parameter_declaration(self) -> Text: + prop_type = cast(DivanPropertyType, self.property_type) + type_declaration = f'Property<{cast(DivanPropertyType, prop_type.declaration(prefixed=False))}>' + default_value = self.default_value + declaration = Text() + if default_value is not None: + declaration += default_value_comment(default_value) + declaration += f'val {self.name_declaration}: {type_declaration}?' + return declaration + + @property + def name_declaration(self) -> str: + name = utils.lower_camel_case(self.name) + if isinstance(self.property_type, StaticString): + name = utils.constant_upper_case(self.name) + return utils.fixing_first_digit(name) + + +class DivanPropertyType(PropertyType): + def declaration(self, prefixed: bool) -> str: + if isinstance(self, Int): + return 'Int' + elif isinstance(self, Double): + return 'Double' + elif isinstance(self, Bool): + return 'Boolean' + elif isinstance(self, BoolInt): + return 'BoolInt' + elif isinstance(self, (String, StaticString)): + return 'String' + elif isinstance(self, Color): + return 'Color' + elif isinstance(self, Url): + return 'URI' + elif isinstance(self, Dictionary): + return 'Map' + elif isinstance(self, Array): + element_type = cast(DivanPropertyType, self.property_type).declaration(prefixed) + return f'List<{element_type}>' + elif isinstance(self, Object): + if self.name.startswith('$predefined_'): + return self.name.replace('$predefined_', '') + obj_name = utils.capitalize_camel_case(self.name) + obj = self.object + if obj is not None: + if prefixed: + obj_name = obj.resolved_prefixed_declaration + else: + obj_name = utils.capitalize_camel_case(obj.resolved_name) + return obj_name + else: + raise NotImplementedError + + +class DivanArray(DivanPropertyType, Array): + def update_base(self): + if not isinstance(self.property_type, DivanPropertyType): + update_property_type_base(self.property_type) + + @property + def property_type_dsl(self) -> DivanPropertyType: + return cast(DivanPropertyType, self.property_type) + + +class DivanBool(DivanPropertyType, Bool): + pass + + +class DivanBoolInt(DivanPropertyType, BoolInt): + pass + + +class DivanColor(DivanPropertyType, Color): + pass + + +class DivanDictionary(DivanPropertyType, Dictionary): + pass + + +class DivanDouble(DivanPropertyType, Double): + pass + + +class DivanInt(DivanPropertyType, Int): + pass + + +class DivanObject(DivanPropertyType, Object): + def update_base(self): + if isinstance(self.object, Entity): + self.object.__class__ = DivanEntity + cast(DivanEntity, self.object).update_base() + + +class DivanStaticString(DivanPropertyType, StaticString): + pass + + +class DivanString(DivanPropertyType, String): + pass + + +class DivanUrl(DivanPropertyType, Url): + pass diff --git a/api_generator/api_generator/generators/divan/generator.py b/api_generator/api_generator/generators/divan/generator.py index d21ac16a2..b4fd7f34c 100644 --- a/api_generator/api_generator/generators/divan/generator.py +++ b/api_generator/api_generator/generators/divan/generator.py @@ -1,19 +1,94 @@ +from typing import List + +from .divan_entities import ( + update_base, + DivanEntity, + DivanEntityEnumeration, + DivanStringEnumeration, +) from ..base import Generator -from ...schema.modeling.entities import StringEnumeration, EntityEnumeration, Entity -from ...schema.modeling.text import Text +from ...schema.modeling.text import Text, EMPTY +from ...schema.modeling.entities import ( + StringEnumeration, + EntityEnumeration, + Entity, + Declarable, +) from ... import utils class DivanGenerator(Generator): + def __init__(self, config): + super(DivanGenerator, self).__init__(config) + self.kotlin_annotations = config.generation.kotlin_annotations + self.top_level_annotations = config.generation.top_level_annotations + + def generate(self, objects: List[Declarable]): + super(DivanGenerator, self).generate(list(map(lambda obj: update_base(obj), objects))) + def filename(self, name: str) -> str: - return f'{utils.snake_case(name).capitalize()}.kt' + return f'{utils.capitalize_snake_case(name)}.kt' - def _entity_declaration(self, entity: Entity) -> Text: + def _entity_declaration(self, entity: DivanEntity) -> Text: + return self.__entity_declaration_with(entity, with_factory_methods=True) + + def __entity_declaration_with(self, entity: DivanEntity, with_factory_methods: bool) -> Text: + result_declaration = Text() + result_declaration += entity.header_comment_block + for annotation in self.top_level_annotations + self.kotlin_annotations: + result_declaration += annotation + result_declaration += f'class {utils.capitalize_snake_case(entity.name)} internal constructor(' + result_declaration += ' @JsonIgnore' + result_declaration += ' val properties: Properties,' + result_declaration += f'){entity.supertype_declaration} {{' + serialization_declaration = entity.serialization_declaration.indented(indent_width=4) + if serialization_declaration.lines: + result_declaration += serialization_declaration + result_declaration += EMPTY + result_declaration += entity.properties_class_declaration.indented(indent_width=4) + + nested_classes_declaration = self.__nested_classes_declaration(entity) + if nested_classes_declaration.lines: + result_declaration += EMPTY + result_declaration += nested_classes_declaration.indented(indent_width=4) + + result_declaration += '}' + return result_declaration + + def __nested_classes_declaration(self, entity: DivanEntity) -> Text: + def declaration_of(decl_type, decl_method) -> Text: + def sort_predicate(d: Declarable): + return d.name + + inner_types_decl = Text() + inner_types = sorted(filter(lambda t: isinstance(t, decl_type), entity.inner_types), key=sort_predicate) + for ind, p in enumerate(inner_types): + if isinstance(p, decl_type): + inner_types_decl += decl_method(p) + if ind != len(inner_types) - 1: + inner_types_decl += EMPTY + + return inner_types_decl + + declarations = [declaration_of(StringEnumeration, self._string_enumeration_declaration), + declaration_of(EntityEnumeration, self._entity_enumeration_declaration), + declaration_of(Entity, + lambda ent: self.__entity_declaration_with(ent, + with_factory_methods=False) + ) + ] + result = Text() + declarations = list(filter(lambda d: d.lines, declarations)) + for ind, decl in enumerate(declarations): + result += decl + if ind != len(declarations) - 1: + result += EMPTY + + return result + + def _entity_enumeration_declaration(self, entity_enumeration: DivanEntityEnumeration) -> Text: return Text() - def _entity_enumeration_declaration(self, entity_enumeration: EntityEnumeration) -> Text: - return Text() - - def _string_enumeration_declaration(self, string_enumeration: StringEnumeration) -> Text: + def _string_enumeration_declaration(self, string_enumeration: DivanStringEnumeration) -> Text: return Text() diff --git a/api_generator/api_generator/utils.py b/api_generator/api_generator/utils.py index 5288887b5..a61539424 100644 --- a/api_generator/api_generator/utils.py +++ b/api_generator/api_generator/utils.py @@ -35,6 +35,10 @@ def snake_case(string: str) -> str: return '_'.join(map(lambda component: component.lower(), filter(lambda s: s, name_components(string)))) +def capitalize_snake_case(string: str) -> str: + return snake_case(string).capitalize() + + def upper_snake_case(string: str) -> str: return '_'.join(map(lambda component: component.upper(), filter(lambda s: s, name_components(string)))) diff --git a/api_generator/tests/configs/divan_config.json b/api_generator/tests/configs/divan_config.json index a7846d9b1..48fa70951 100644 --- a/api_generator/tests/configs/divan_config.json +++ b/api_generator/tests/configs/divan_config.json @@ -1,3 +1,7 @@ { - "lang": "divan" + "lang": "divan", + "header": "@file:Suppress(\n \"unused\",\n \"UNUSED_PARAMETER\",\n)\n\npackage divan\n\nimport com.fasterxml.jackson.annotation.JsonAnyGetter\nimport com.fasterxml.jackson.annotation.JsonIgnore\nimport divan.annotation.Generated\nimport divan.core.Guard\nimport divan.core.Property\nimport divan.core.ReferenceProperty\nimport divan.core.tryPutProperty\nimport divan.core.valueOrNull\nimport divan.scope.DivScope\nimport divan.scope.TemplateScope\nimport kotlin.Any\nimport kotlin.String\nimport kotlin.Suppress\nimport kotlin.collections.List\nimport kotlin.collections.Map\n", + "topLevelAnnotations": [ + "@Generated" + ] } \ No newline at end of file