diff options
author | Aleksandr Mishchenko <AMishchenko@luxoft.com> | 2020-01-15 19:37:22 +0200 |
---|---|---|
committer | Vladyslav Mustafin <vmustafin@luxoft.com> | 2020-02-19 18:45:58 +0200 |
commit | ce593107c5fda551012c5e5a4698c1ceaef7034c (patch) | |
tree | 5430e7250c898bd44f47dc63971fd6520bfdebd1 | |
parent | 9e389ad7668b8ac5d47873f26cf314459a670d15 (diff) | |
download | sdl_ios-ce593107c5fda551012c5e5a4698c1ceaef7034c.tar.gz |
SDL-0234 Proxy Library RPC Generation
36 files changed, 2351 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore index 58a919ead..cd964170f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,10 @@ infer-out Carthage/Build docs/docsets/ +.idea +*venv* +*__pycache__ +*.pytest_cache +**htmlcov +**.coverage +_debug* diff --git a/.gitmodules b/.gitmodules index 684da2889..6336d3034 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "bson_c_lib"] path = bson_c_lib url = https://github.com/smartdevicelink/bson_c_lib.git +[submodule "generator/rpc_spec"] + path = generator/rpc_spec + url = https://github.com/smartdevicelink/rpc_spec.git + branch = develop diff --git a/generator/README.md b/generator/README.md new file mode 100644 index 000000000..623f46d94 --- /dev/null +++ b/generator/README.md @@ -0,0 +1,47 @@ +# Proxy Library RPC Generator + +## Overview + +This script provides the possibility to auto-generate iOS code based on a given SDL MOBILE_API XML specification. + +## Requirements + +The script requires Python 3.5 pre-installed in the system. This is the minimal Python 3 version that has not reached the end-of-life (https://devguide.python.org/devcycle/#end-of-life-branches). + +Some required libraries are described in `requirements.txt` and should be pre-installed by the command: +```shell script +pip install -r requirements.txt +``` +Please also make sure before usage the 'generator/rpc_spec' Git submodule is successfully initialized, because the script uses the XML parser provided there. + +## Usage +```shell script +usage: runner.py [-h] [-v] [-xml SOURCE_XML] [-xsd SOURCE_XSD] + [-d OUTPUT_DIRECTORY] [-t [TEMPLATES_DIRECTORY]] + [-r REGEX_PATTERN] [--verbose] [-e] [-s] [-m] [-y] [-n] + +Proxy Library RPC Generator + +optional arguments: + -h, --help show this help message and exit + -v, --version print the version and exit + -xml SOURCE_XML, --source-xml SOURCE_XML, --input-file SOURCE_XML + should point to MOBILE_API.xml + -xsd SOURCE_XSD, --source-xsd SOURCE_XSD + -d OUTPUT_DIRECTORY, --output-directory OUTPUT_DIRECTORY + define the place where the generated output should be + placed + -t [TEMPLATES_DIRECTORY], --templates-directory [TEMPLATES_DIRECTORY] + path to directory with templates + -r REGEX_PATTERN, --regex-pattern REGEX_PATTERN + only elements matched with defined regex pattern will + be parsed and generated + --verbose display additional details like logs etc + -e, --enums only specified elements will be generated, if present + -s, --structs only specified elements will be generated, if present + -m, -f, --functions only specified elements will be generated, if present + -y, --overwrite force overwriting of existing files in output + directory, ignore confirmation message + -n, --skip skip overwriting of existing files in output + directory, ignore confirmation message +``` diff --git a/generator/__init__.py b/generator/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/generator/__init__.py diff --git a/generator/generator.py b/generator/generator.py new file mode 100644 index 000000000..1b2a06aa4 --- /dev/null +++ b/generator/generator.py @@ -0,0 +1,479 @@ +""" +Generator +""" +import asyncio +import json +import logging +import re +import sys +from argparse import ArgumentParser +from collections import namedtuple, OrderedDict +from datetime import datetime, date +from inspect import getfile +from json import JSONDecodeError +from os.path import basename, join +from re import findall + +from jinja2 import UndefinedError, TemplateNotFound, FileSystemLoader, Environment, ChoiceLoader, \ + TemplateAssertionError, TemplateSyntaxError, TemplateRuntimeError +from pathlib2 import Path + +ROOT = Path(__file__).absolute().parents[0] + +sys.path.append(ROOT.joinpath('rpc_spec/InterfaceParser').as_posix()) + +try: + from parsers.rpc_base import ParseError + from parsers.sdl_rpc_v2 import Parser + from model.interface import Interface + from transformers.common_producer import InterfaceProducerCommon as Common + from transformers.enums_producer import EnumsProducer + from transformers.functions_producer import FunctionsProducer + from transformers.structs_producer import StructsProducer +except ImportError as error: + print('%s.\nprobably you did not initialize submodule', error) + ParseError = Parser = Interface = Common = EnumsProducer = FunctionsProducer = StructsProducer = None + sys.exit(1) + + +class Generator: + """ + This class contains only technical features, as follow: + - parsing command-line arguments, or evaluating required container interactively; + - calling parsers to get Model from xml; + - calling producers to transform initial Model to dict used in jinja2 templates + Not required to be covered by unit tests cause contains only technical features. + """ + + def __init__(self): + self.logger = logging.getLogger(self.__class__.__name__) + self._env = None + self._output_directory = None + self.loop = asyncio.get_event_loop() + self.paths_named = namedtuple('paths_named', 'enum_class struct_class request_class response_class ' + 'notification_class function_names parameter_names') + + @property + def output_directory(self) -> Path: + """ + + :return: + """ + return self._output_directory + + @output_directory.setter + def output_directory(self, output_directory): + """ + + :param output_directory: + :return: + """ + if output_directory.startswith('/'): + path = Path(output_directory).absolute().resolve() + else: + path = ROOT.joinpath(output_directory).resolve() + if not path.exists(): + self.logger.warning('Directory not found: %s, trying to create it', path) + try: + path.mkdir(parents=True, exist_ok=True) + except OSError as message1: + self.logger.critical('Failed to create directory %s, %s', path.as_posix(), message1) + sys.exit(1) + self._output_directory = path + + @property + def env(self) -> Environment: + """ + + :return: + """ + return self._env + + @env.setter + def env(self, paths): + """ + + :param paths: + :return: + """ + loaders = list(filter(lambda l: Path(l).exists(), paths)) + if not loaders: + self.logger.error('Directory with templates not found %s', str(paths)) + sys.exit(1) + loaders = [FileSystemLoader(l) for l in loaders] + + self._env = Environment(loader=ChoiceLoader(loaders)) + self._env.filters['title'] = self.title + self._env.globals['year'] = date.today().year + + @staticmethod + def title(name): + """ + + :param name: + :return: + """ + return name[:1].upper() + name[1:] + + @property + def get_version(self): + """ + + :return: + """ + return Common.version + + def config_logging(self, verbose): + """ + + :param verbose: + :return: + """ + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter(fmt='%(asctime)s.%(msecs)03d - %(levelname)s - %(message)s', + datefmt='%H:%M:%S')) + root_logger = logging.getLogger() + + if verbose: + handler.setLevel(logging.DEBUG) + self.logger.setLevel(logging.DEBUG) + root_logger.setLevel(logging.DEBUG) + else: + handler.setLevel(logging.ERROR) + self.logger.setLevel(logging.ERROR) + root_logger.setLevel(logging.ERROR) + logging.getLogger().handlers.clear() + root_logger.addHandler(handler) + + def get_parser(self): + """ + Parsing command-line arguments, or evaluating required container interactively. + :return: an instance of argparse.ArgumentParser + """ + if len(sys.argv) == 2 and sys.argv[1] in ('-v', '--version'): + print(self.get_version) + sys.exit(0) + + container = namedtuple('container', 'name path') + xml = container('source_xml', ROOT.joinpath('rpc_spec/MOBILE_API.xml')) + required_source = not xml.path.exists() + + out = container('output_directory', ROOT.parents[0].joinpath('SmartDeviceLink')) + output_required = not out.path.exists() + + parser = ArgumentParser(description='Proxy Library RPC Generator') + parser.add_argument('-v', '--version', action='store_true', help='print the version and sys.exit') + parser.add_argument('-xml', '--source-xml', '--input-file', required=required_source, + help='should point to MOBILE_API.xml') + parser.add_argument('-xsd', '--source-xsd', required=False) + parser.add_argument('-d', '--output-directory', required=output_required, + help='define the place where the generated output should be placed') + parser.add_argument('-t', '--templates-directory', nargs='?', default=ROOT.joinpath('templates').as_posix(), + help='path to directory with templates') + parser.add_argument('-r', '--regex-pattern', required=False, + help='only elements matched with defined regex pattern will be parsed and generated') + parser.add_argument('--verbose', action='store_true', help='display additional details like logs etc') + parser.add_argument('-e', '--enums', required=False, action='store_true', + help='only specified elements will be generated, if present') + parser.add_argument('-s', '--structs', required=False, action='store_true', + help='only specified elements will be generated, if present') + parser.add_argument('-m', '-f', '--functions', required=False, action='store_true', + help='only specified elements will be generated, if present') + parser.add_argument('-y', '--overwrite', action='store_true', + help='force overwriting of existing files in output directory, ignore confirmation message') + parser.add_argument('-n', '--skip', action='store_true', + help='skip overwriting of existing files in output directory, ignore confirmation message') + + args, unknown = parser.parse_known_args() + + if unknown: + print('found unknown arguments: ' + ' '.join(unknown)) + parser.print_help(sys.stderr) + sys.exit(1) + + if args.skip and args.overwrite: + print('please select only one option skip "-n" or overwrite "-y"') + sys.exit(1) + + if not args.enums and not args.structs and not args.functions: + args.enums = args.structs = args.functions = True + + for kind in (xml, out): + if not getattr(args, kind.name) and kind.path.exists(): + while True: + try: + confirm = input('Confirm default path {} for {} Y/Enter = yes, N = no' + .format(kind.path, kind.name)) + if confirm.lower() == 'y' or not confirm: + print('{} set to {}'.format(kind.name, kind.path)) + setattr(args, kind.name, kind.path.as_posix()) + break + if confirm.lower() == 'n': + print('provide argument ' + kind.name) + sys.exit(1) + except KeyboardInterrupt: + print('\nThe user interrupted the execution of the program') + sys.exit(1) + + self.logger.debug('parsed arguments:\n%s', vars(args)) + + return args + + def versions_compatibility_validating(self): + """version of generator script requires the same or lesser version of parser script. + if the parser script needs to fix a bug (and becomes, e.g. 1.0.1) and the generator script stays at 1.0.0. + As long as the generator script is the same or greater major version, it should be parsable. + This requires some level of backward compatibility. E.g. they have to be the same major version. + + """ + + regex = r'(\d+\.\d+).(\d)' + + parser_origin = Parser().get_version + generator_origin = self.get_version + parser_split = findall(regex, parser_origin).pop() + generator_split = findall(regex, generator_origin).pop() + + parser_major = float(parser_split[0]) + generator_major = float(generator_split[0]) + + if parser_major > generator_major: + self.logger.critical('Generator (%s) requires the same or lesser version of Parser (%s)', + generator_origin, parser_origin) + sys.exit(1) + + self.logger.info('Parser type: %s, version %s,\tGenerator version %s', + basename(getfile(Parser().__class__)), parser_origin, generator_origin) + + async def get_paths(self, file_name=ROOT.joinpath('paths.ini')): + """ + :param file_name: path to file with container + :return: namedtuple with container to key elements + """ + data = OrderedDict() + try: + with file_name.open('r') as file: + for line in file: + if line.startswith('#'): + self.logger.warning('commented property %s, which will be skipped', line.strip()) + continue + if re.match(r'^(\w+)\s?=\s?(.+)', line): + if len(line.split('=')) > 2: + self.logger.critical('can not evaluate value, too many separators %s', str(line)) + sys.exit(1) + name, var = line.partition('=')[::2] + if name.strip() in data: + self.logger.critical('duplicate key %s', name) + sys.exit(1) + data[name.strip().lower()] = var.strip() + except FileNotFoundError as message1: + self.logger.critical(message1) + sys.exit(1) + + missed = list(set(self.paths_named._fields) - set(data.keys())) + if missed: + self.logger.critical('in %s missed fields: %s ', file, str(missed)) + sys.exit(1) + + return self.paths_named(**data) + + async def get_mappings(self, file=ROOT.joinpath('mapping.json')): + """ + The key name in *.json is equal to property named in jinja2 templates + :param file: path to file with manual mappings + :return: dictionary with custom manual mappings + """ + try: + with file.open('r') as handler: + content = handler.readlines() + return json.loads(''.join(content)) + except (FileNotFoundError, JSONDecodeError) as error1: + self.logger.error('Failure to get mappings %s', error1) + return OrderedDict() + + def write_file(self, file, templates, data): + """ + Calling producer/transformer instance to transform initial Model to dict used in jinja2 templates. + Applying transformed dict to jinja2 templates and writing to appropriate file + :param file: output js file + :param templates: name of template + :param data: an instance of transformer for particular item + """ + try: + render = self.env.get_or_select_template(templates).render(data) + with file.open('w', encoding='utf-8') as handler: + handler.write(render) + except (TemplateNotFound, UndefinedError, TemplateAssertionError, TemplateSyntaxError, TemplateRuntimeError) \ + as error1: + self.logger.error('skipping %s, template not found %s', file.as_posix(), error1) + + async def process_main(self, skip, overwrite, items, transformer): + """ + Process each item from initial Model. According to provided arguments skipping, overriding or asking what to to. + :param skip: if file exist skip it + :param overwrite: if file exist overwrite it + :param items: elements initial Model + :param transformer: producer/transformer instance + """ + tasks = [] + for item in items.values(): + render = transformer.transform(item) + file = self.output_directory.joinpath(render.get('name', item.name)) + for extension in ('.h', '.m'): + data = render.copy() + data['imports'] = data['imports'][extension] + file_with_suffix = file.with_suffix(extension) + templates = ['{}s/template{}'.format(type(item).__name__.lower(), extension)] + if 'template' in data: + templates.insert(0, data['template'] + extension) + tasks.append(self.process_common(skip, overwrite, file_with_suffix, data, templates)) + + await asyncio.gather(*tasks) + + async def process_function_name(self, skip, overwrite, functions, structs, transformer): + """ + + :param skip: + :param overwrite: + :param functions: + :param structs: + :param transformer: + :return: + """ + tasks = [] + for name in [transformer.function_names, transformer.parameter_names]: + file = self.output_directory.joinpath(name) + if name == transformer.function_names: + data = transformer.get_function_names(functions) + elif name == transformer.parameter_names: + data = transformer.get_simple_params(functions, structs) + else: + self.logger.error('No "data" for %s', name) + continue + for extension in ('.h', '.m'): + templates = [name + extension] + file_with_suffix = file.with_suffix(extension) + tasks.append(self.process_common(skip, overwrite, file_with_suffix, data, templates)) + + await asyncio.gather(*tasks) + + async def process_common(self, skip, overwrite, file_with_suffix, data, templates): + """ + + :param skip: + :param overwrite: + :param file_with_suffix: + :param data: + :param templates: + :return: + """ + if file_with_suffix.is_file(): + if skip: + self.logger.info('Skipping %s', file_with_suffix.name) + return + if overwrite: + self.logger.info('Overriding %s', file_with_suffix.name) + self.write_file(file_with_suffix, templates, data) + else: + while True: + try: + confirm = input('File already exists {}. Overwrite? Y/Enter = yes, N = no\n' + .format(file_with_suffix.name)) + if confirm.lower() == 'y' or not confirm: + self.logger.info('Overriding %s', file_with_suffix.name) + self.write_file(file_with_suffix, templates, data) + break + if confirm.lower() == 'n': + self.logger.info('Skipping %s', file_with_suffix.name) + break + except KeyboardInterrupt: + print('\nThe user interrupted the execution of the program') + sys.exit(1) + else: + self.logger.info('Writing new %s', file_with_suffix.name) + self.write_file(file_with_suffix, templates, data) + + @staticmethod + def filter_pattern(interface, pattern): + """ + + :param interface: + :param pattern: + :return: + """ + names = tuple(interface.enums.keys()) + tuple(interface.structs.keys()) + + if pattern: + match = {key: OrderedDict() for key in vars(interface).keys()} + match['params'] = interface.params + for key, value in vars(interface).items(): + if key == 'params': + continue + match[key].update({name: item for name, item in value.items() if re.match(pattern, item.name)}) + return Interface(**match), names + return interface, names + + async def parser(self, source_xml, source_xsd): + """ + + :param source_xml: + :param source_xsd: + :return: + """ + try: + start = datetime.now() + model = self.loop.run_in_executor(None, Parser().parse, source_xml, source_xsd) + model = await model + self.logger.debug('finish getting model in %s milisec,', (datetime.now() - start).microseconds / 1000.0) + return model + except ParseError as error1: + self.logger.error(error1) + sys.exit(1) + + async def get_all_async(self, source_xml, source_xsd): + """ + + :param source_xml: + :param source_xsd: + :return: + """ + return await asyncio.gather(self.parser(source_xml, source_xsd), self.get_paths(), self.get_mappings()) + + def main(self): + """ + Entry point for parser and generator + :return: None + """ + args = self.get_parser() + self.config_logging(args.verbose) + self.versions_compatibility_validating() + self.output_directory = args.output_directory + + interface, paths, mappings = self.loop.run_until_complete(self.get_all_async(args.source_xml, args.source_xsd)) + + self.env = [args.templates_directory] + [join(args.templates_directory, k) for k in vars(interface).keys()] + + filtered, names = self.filter_pattern(interface, args.regex_pattern) + + tasks = [] + functions_transformer = FunctionsProducer(paths, names, mappings) + if args.enums and filtered.enums: + tasks.append(self.process_main(args.skip, args.overwrite, filtered.enums, + EnumsProducer(paths.enum_class, mappings))) + if args.structs and filtered.structs: + tasks.append(self.process_main(args.skip, args.overwrite, filtered.structs, + StructsProducer(paths.struct_class, names, mappings))) + if args.functions and filtered.functions: + tasks.append(self.process_main(args.skip, args.overwrite, filtered.functions, functions_transformer)) + tasks.append(self.process_function_name(args.skip, args.overwrite, interface.functions, + interface.structs, functions_transformer)) + if tasks: + self.loop.run_until_complete(asyncio.wait(tasks)) + else: + self.logger.warning('Nothing matched with "%s"', args.regex_pattern) + + self.loop.close() + + +if __name__ == '__main__': + Generator().main() diff --git a/generator/mapping.json b/generator/mapping.json new file mode 100644 index 000000000..e90187f04 --- /dev/null +++ b/generator/mapping.json @@ -0,0 +1,11 @@ +{ + "enums": { + "FunctionID": { + "template": true + } + }, + "structs": { + }, + "functions": { + } +}
\ No newline at end of file diff --git a/generator/paths.ini b/generator/paths.ini new file mode 100644 index 000000000..9ea5bb800 --- /dev/null +++ b/generator/paths.ini @@ -0,0 +1,7 @@ +ENUM_CLASS = SDLEnum +STRUCT_CLASS = SDLRPCStruct +REQUEST_CLASS = SDLRPCRequest +RESPONSE_CLASS = SDLRPCResponse +NOTIFICATION_CLASS = SDLRPCNotification +FUNCTION_NAMES = SDLRPCFunctionNames +PARAMETER_NAMES = SDLRPCParameterNames
\ No newline at end of file diff --git a/generator/requirements.txt b/generator/requirements.txt new file mode 100644 index 000000000..12a4e48f1 --- /dev/null +++ b/generator/requirements.txt @@ -0,0 +1,5 @@ +xmlschema +pylint +Jinja2 +coverage +pathlib2
\ No newline at end of file diff --git a/generator/rpc_spec b/generator/rpc_spec new file mode 160000 +Subproject bf14662066e3d9c2e7b32b56f1fa8e9dad8dceb diff --git a/generator/templates/SDLRPCFunctionNames.h b/generator/templates/SDLRPCFunctionNames.h new file mode 100644 index 000000000..18de36e12 --- /dev/null +++ b/generator/templates/SDLRPCFunctionNames.h @@ -0,0 +1,26 @@ +{% include 'copyright.txt' %} +// SDLRPCFunctionNames.h + +#import "SDLEnum.h" + +/** + * All RPC request / response / notification names + */ +typedef SDLEnum SDLRPCFunctionName SDL_SWIFT_ENUM; +{% for param in params %} +{%- if param.description or param.since %} +/** + {%- if param.description %} + {%- for d in param.description %} + * {{d}} + {%- endfor %}{% endif -%} + {%- if param.description and param.since %} + * + {%- endif %} + {%- if param.since %} + * @since SDL {{param.since}} + {%- endif %} + */ +{%- endif %} +extern SDLRPCFunctionName const SDLRPCFunctionName{{ param.name }}; +{% endfor -%}
\ No newline at end of file diff --git a/generator/templates/SDLRPCFunctionNames.m b/generator/templates/SDLRPCFunctionNames.m new file mode 100644 index 000000000..2ae5f21a6 --- /dev/null +++ b/generator/templates/SDLRPCFunctionNames.m @@ -0,0 +1,7 @@ +{% include 'copyright.txt' %} +// SDLRPCFunctionNames.m + +#import "SDLRPCFunctionNames.h" +{% for param in params %} +SDLRPCFunctionName const SDLRPCFunctionName{{ param.name }} = @"{{ param.origin }}"; +{%- endfor %} diff --git a/generator/templates/SDLRPCParameterNames.h b/generator/templates/SDLRPCParameterNames.h new file mode 100644 index 000000000..4a00ce029 --- /dev/null +++ b/generator/templates/SDLRPCParameterNames.h @@ -0,0 +1,14 @@ +{% include 'copyright.txt' %} +// SDLRPCParameterNames.h + +#import <Foundation/Foundation.h> +#import "SDLMacros.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NSString* SDLRPCParameterName SDL_SWIFT_ENUM; +{% for param in params %} +extern SDLRPCParameterName const SDLRPCParameterName{{ param.name }}; +{%- endfor %} + +NS_ASSUME_NONNULL_END
\ No newline at end of file diff --git a/generator/templates/SDLRPCParameterNames.m b/generator/templates/SDLRPCParameterNames.m new file mode 100644 index 000000000..ae6bdace7 --- /dev/null +++ b/generator/templates/SDLRPCParameterNames.m @@ -0,0 +1,12 @@ +{% include 'copyright.txt' %} +// SDLRPCParameterNames.h + +#import "NSMutableDictionary+Store.h" +#import "SDLRPCParameterNames.h" + +NS_ASSUME_NONNULL_BEGIN +{% for param in params %} +SDLRPCParameterName const SDLRPCParameterName{{ param.name }} = @"{{ param.origin }}"; +{%- endfor %} + +NS_ASSUME_NONNULL_END
\ No newline at end of file diff --git a/generator/templates/base_struct_function.h b/generator/templates/base_struct_function.h new file mode 100644 index 000000000..c8a99cfd1 --- /dev/null +++ b/generator/templates/base_struct_function.h @@ -0,0 +1,93 @@ +{% include 'copyright.txt' %} +// {{name}}.h +{% block imports %} +{%- for import in imports.enum %} +#import "{{import}}.h" +{%- endfor %} +{%- if imports.struct %} +{% endif -%} +{%- for import in imports.struct %} +@class {{import}}; +{%- endfor %} +{%- endblock %} +{%- if deprecated and deprecated is sameas true -%} +{%- set ending = ' __deprecated' -%} +{%- elif deprecated and deprecated is string -%} +{%- set ending = ' __deprecated_msg("{}")'.format(deprecated) -%} +{%- endif %} + +NS_ASSUME_NONNULL_BEGIN +{% if description or since %} +/** + {%- if description %} + {%- for d in description %} + * {{d}} + {%- endfor %}{% endif -%} + {%- if description and since %} + * + {%- endif %} + {%- if deprecated %} + * @deprecated + {%- endif %} + {%- if since %} + * @since SDL {{since}} + {%- endif %} + */ +{%- endif %} +@interface {{name}} : {{extends_class}}{{ending}} +{%- block constructors %} +{% for c in constructors %} +/** + {%- if c.description %} + {%- for d in c.description %} + * {{d}} + {%- endfor %} + * + {%- endif %} + {%- for a in c.all %} + * @param {{a.variable}} - {{a.constructor_argument}} + {%- endfor %} + * @return A {{name}} object + */ +{%- if c.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +- (instancetype)initWith{{c.init}}; +{%- if c.deprecated %} +#pragma clang diagnostic pop +{%- endif %} +{% endfor -%} +{%- endblock -%} +{%- block methods %} +{%- for param in params %} +/** + {%- if param.description %} + {%- for d in param.description %} + * {{d}} + {%- endfor %}{% endif -%} + {%- if param.description and param.since %} + * + {%- endif %} + {%- if param.deprecated %} + * @deprecated + {%- endif %} + {%- if param.since %} + * @since SDL {{param.since}} + {%- endif %} + {%- if param.description or param.since %} + * + {%- endif %} + * {{'Required, ' if param.mandatory else 'Optional, '}}{{param.type_native}} + */ +{%- if param.deprecated and param.deprecated is sameas true -%} +{%- set ending = ' __deprecated' -%} +{%- elif param.deprecated and param.deprecated is string -%} +{%- set ending = ' __deprecated_msg("{}")'.format(param.deprecated) -%} +{%- endif %} +@property ({{'nullable, ' if not param.mandatory}}{{param.modifier}}, nonatomic) {{param.type_sdl}}{{param.origin}}{{ending}}; +{% endfor -%} +{%- endblock %} +@end + +NS_ASSUME_NONNULL_END diff --git a/generator/templates/base_struct_function.m b/generator/templates/base_struct_function.m new file mode 100644 index 000000000..addc1cb77 --- /dev/null +++ b/generator/templates/base_struct_function.m @@ -0,0 +1,65 @@ +{% include 'copyright.txt' %} +// {{name}}.m +{%- block imports %} +#import "{{name}}.h" +#import "NSMutableDictionary+Store.h" +{%- for import in imports %} +#import "{{import}}.h" +{%- endfor %} +{%- endblock %} + +NS_ASSUME_NONNULL_BEGIN + +@implementation {{name}} +{% block constructors %} +{%- for c in constructors %} +- (instancetype)initWith{{c.init}} { +{%- if c.deprecated %} + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} + self = [{{'self' if c.self else 'super'}} init{{'With'+c.self if c.self}}]; +{%- if c.deprecated %} + #pragma clang diagnostic pop +{%- endif %} + if (!self) { + return nil; + } + {%- for a in c.arguments %} + {%- if a.deprecated %} + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + {%- endif %} + self.{{a.origin}} = {{a.constructor_argument}}; + {%- if a.deprecated %} + #pragma clang diagnostic pop + {%- endif %} + {%- endfor %} + return self; +} +{% endfor -%} +{% endblock -%} +{%- block methods %} +{%- for p in params %} +{%- if p.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +- (void)set{{p.origin|title}}:({{'nullable ' if not p.mandatory}}{{p.type_generic}}{{p.type_sdl|trim}}){{p.origin}} { + [self.{{parameters_store}} sdl_setObject:{{p.origin}} forName:SDLRPCParameterName{{p.method_suffix}}]; +} + +- ({{'nullable ' if not p.mandatory}}{{p.type_generic}}{{p.type_sdl|trim}}){{p.origin}} { + {% if p.mandatory -%} + NSError *error = nil; + {% endif -%} + return [self.{{parameters_store}} sdl_{{p.for_name}}ForName:SDLRPCParameterName{{p.method_suffix}}{{' ofClass:'+p.of_class if p.of_class}} error:{{'&error' if p.mandatory else 'nil'}}]; +} +{%- if p.deprecated %} +#pragma clang diagnostic pop +{%- endif %} +{% endfor %} +{%- endblock %} +@end + +NS_ASSUME_NONNULL_END diff --git a/generator/templates/copyright.txt b/generator/templates/copyright.txt new file mode 100644 index 000000000..abb41f0da --- /dev/null +++ b/generator/templates/copyright.txt @@ -0,0 +1,31 @@ +/* +* Copyright (c) {{year}}, SmartDeviceLink Consortium, Inc. +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* Redistributions of source code must retain the above copyright notice, this +* list of conditions and the following disclaimer. +* +* Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following +* disclaimer in the documentation and/or other materials provided with the +* distribution. +* +* Neither the name of the SmartDeviceLink Consortium Inc. nor the names of +* its contributors may be used to endorse or promote products derived +* from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +* POSSIBILITY OF SUCH DAMAGE. +*/
\ No newline at end of file diff --git a/generator/templates/enums/FunctionID.h b/generator/templates/enums/FunctionID.h new file mode 100644 index 000000000..92e36ac29 --- /dev/null +++ b/generator/templates/enums/FunctionID.h @@ -0,0 +1,29 @@ +{% include 'copyright.txt' %} +// {{name}}.h + +#import <Foundation/Foundation.h> +#import "NSNumber+NumberType.h" +#import "SDLRPCFunctionNames.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A function ID for an SDL RPC +@interface {{name}} : NSObject + +/// The shared object for pulling function id information ++ (instancetype)sharedInstance; + +/// Gets the function name for a given SDL RPC function ID +/// +/// @param functionID A function ID +/// @returns An SDLRPCFunctionName +- (nullable SDLRPCFunctionName)functionNameForId:(UInt32)functionID; + +/// Gets the function ID for a given SDL RPC function name +/// +/// @param functionName The RPC function name +- (nullable NSNumber<SDLInt> *)functionIdForName:(SDLRPCFunctionName)functionName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/generator/templates/enums/FunctionID.m b/generator/templates/enums/FunctionID.m new file mode 100644 index 000000000..92ddacafc --- /dev/null +++ b/generator/templates/enums/FunctionID.m @@ -0,0 +1,52 @@ +{% include 'copyright.txt' %} +// {{name}}.m + +#import "{{name}}.h" + +#import "NSMutableDictionary+Store.h" +#import "SDLRPCFunctionNames.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface {{name}} () + +@property (nonatomic, strong, nonnull) NSDictionary* functionIds; + +@end + +@implementation {{name}} + ++ (instancetype)sharedInstance { + static {{name}}* functionId = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + functionId = [[{{name}} alloc] init]; + }); + return functionId; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.functionIds = @{ + {%- for param in params %} + @{{param.value}}: SDLRPCFunctionName{{param.name}}{{ ',' if not loop.last }} + {%- endfor %} + }; + return self; +} + +- (nullable SDLRPCFunctionName)functionNameForId:(UInt32)functionID { + return self.functionIds[@(functionID)]; +} + +- (nullable NSNumber<SDLInt> *)functionIdForName:(SDLRPCFunctionName)functionName { + return [[self.functionIds allKeysForObject:functionName] firstObject]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/generator/templates/enums/template.h b/generator/templates/enums/template.h new file mode 100644 index 000000000..f3ee55c9d --- /dev/null +++ b/generator/templates/enums/template.h @@ -0,0 +1,52 @@ +{% include 'copyright.txt' %} +// {{ name }}.h +{% block imports -%} +{%- for import in imports %} +#import "{{import}}.h" +{%- endfor %} +{%- endblock -%} +{%- if deprecated and deprecated is sameas true -%} +{%- set ending = ' __deprecated' -%} +{%- elif deprecated and deprecated is string -%} +{%- set ending = ' __deprecated_msg("{}")'.format(deprecated) -%} +{%- endif %} +{%- block body %} +{% if description or since %} +/** + {%- if description %} + {%- for d in description %} + * {{d}} + {%- endfor %}{% endif -%} + {%- if description and since %} + * + {%- endif %} + {%- if deprecated %} + * @deprecated + {%- endif %} + {%- if since %} + * @since SDL {{since}} + {%- endif %} + */ +{%- endif %} +typedef SDLEnum {{ name }} SDL_SWIFT_ENUM{{ending}}; +{% for param in params %} +{%- if param.description or param.since %} +/** + {%- if param.description %} + {%- for d in param.description %} + * {{d}} + {%- endfor %}{% endif -%} + {%- if param.description and param.since %} + * + {%- endif %} + {%- if param.deprecated %} + * @deprecated + {%- endif %} + {%- if param.since %} + * @since SDL {{param.since}} + {%- endif %} + */ +{%- endif %} +extern {{ name }} const {{ name }}{{param.name}}{{ ' %s%s%s'|format('NS_SWIFT_NAME(', param.name|lower, ')') if NS_SWIFT_NAME is defined}}; +{% endfor -%} +{% endblock -%}
\ No newline at end of file diff --git a/generator/templates/enums/template.m b/generator/templates/enums/template.m new file mode 100644 index 000000000..7e669c293 --- /dev/null +++ b/generator/templates/enums/template.m @@ -0,0 +1,12 @@ +{% include 'copyright.txt' %} +// {{ name }}.m + +#import "{{name}}.h" +{%- block body %} +{% if add_typedef %} +typedef SDLEnum {{name}} SDL_SWIFT_ENUM; +{% endif -%} +{%- for param in params %} +{{ name }} const {{ name }}{{param.name}} = @"{{param.origin}}"; +{%- endfor %} +{% endblock -%}
\ No newline at end of file diff --git a/generator/templates/functions/template.h b/generator/templates/functions/template.h new file mode 100644 index 000000000..51f426c26 --- /dev/null +++ b/generator/templates/functions/template.h @@ -0,0 +1 @@ +{% extends "base_struct_function.h" %}
\ No newline at end of file diff --git a/generator/templates/functions/template.m b/generator/templates/functions/template.m new file mode 100644 index 000000000..3e5e4bd1c --- /dev/null +++ b/generator/templates/functions/template.m @@ -0,0 +1,18 @@ +{% extends "base_struct_function.m" %} +{% block imports %} +{{super()}} +#import "SDLRPCParameterNames.h" +#import "SDLRPCFunctionNames.h" +{%- endblock %} +{% block constructors %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (instancetype)init { + if ((self = [super initWithName:SDLRPCFunctionName{{origin}}])) { + } + return self; +} +#pragma clang diagnostic pop +{{super()}} +{%- endblock -%} +{% set parameters_store = 'parameters' %} diff --git a/generator/templates/structs/template.h b/generator/templates/structs/template.h new file mode 100644 index 000000000..51f426c26 --- /dev/null +++ b/generator/templates/structs/template.h @@ -0,0 +1 @@ +{% extends "base_struct_function.h" %}
\ No newline at end of file diff --git a/generator/templates/structs/template.m b/generator/templates/structs/template.m new file mode 100644 index 000000000..c81f652cf --- /dev/null +++ b/generator/templates/structs/template.m @@ -0,0 +1,6 @@ +{% extends "base_struct_function.m" %} +{% block imports %} +{{super()}} +#import "SDLRPCParameterNames.h" +{%- endblock %} +{% set parameters_store = 'store' %}
\ No newline at end of file diff --git a/generator/test/__init__.py b/generator/test/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/generator/test/__init__.py diff --git a/generator/test/runner.py b/generator/test/runner.py new file mode 100644 index 000000000..62cb20b54 --- /dev/null +++ b/generator/test/runner.py @@ -0,0 +1,54 @@ +""" +All tests +""" +import logging +import sys +from pathlib import Path +from unittest import TestLoader, TestSuite, TextTestRunner + +ROOT = Path(__file__).absolute() + +sys.path.append(ROOT.parents[1].joinpath('rpc_spec/InterfaceParser').as_posix()) +sys.path.append(ROOT.parents[1].as_posix()) + +try: + from test_enums import TestEnumsProducer + from test_functions import TestFunctionsProducer + from test_structs import TestStructsProducer + from test_CodeFormatAndQuality import TestCodeFormatAndQuality +except ImportError as error: + print('{}.\nProbably you did not initialize submodule'.format(error)) + sys.exit(1) + + +def config_logging(): + """ + :return: None + """ + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%m-%d %H:%M')) + root_logger = logging.getLogger() + handler.setLevel(logging.DEBUG) + root_logger.setLevel(logging.DEBUG) + root_logger.addHandler(handler) + + +def main(): + """ + :return: None + """ + config_logging() + suite = TestSuite() + + suite.addTests(TestLoader().loadTestsFromTestCase(TestFunctionsProducer)) + suite.addTests(TestLoader().loadTestsFromTestCase(TestStructsProducer)) + suite.addTests(TestLoader().loadTestsFromTestCase(TestEnumsProducer)) + suite.addTests(TestLoader().loadTestsFromTestCase(TestCodeFormatAndQuality)) + + runner = TextTestRunner(verbosity=2) + runner.run(suite) + + +if __name__ == '__main__': + main() diff --git a/generator/test/test_CodeFormatAndQuality.py b/generator/test/test_CodeFormatAndQuality.py new file mode 100755 index 000000000..070e1c36e --- /dev/null +++ b/generator/test/test_CodeFormatAndQuality.py @@ -0,0 +1,37 @@ +# pylint: disable=C0103, C0301, C0115, C0116 +"""Interface model unit test + +""" +import unittest +from os import walk +from os.path import join +from pathlib import Path + +from pylint.lint import Run + + +class TestCodeFormatAndQuality(unittest.TestCase): + MINIMUM_SCORE = 9 + + def setUp(self): + """Searching for all python files to be checked + + """ + self.list_of_files = ['--max-line-length=130', '--disable=import-error'] + root = Path(__file__).absolute().parents[1] + for (directory, _, filenames) in walk(root.as_posix()): + self.list_of_files += [join(directory, file) for file in filenames + if file.endswith('.py') and not file.startswith('test') + and 'rpc_spec' not in directory] + + def test_pylint_conformance(self): + """Performing checks by PyLint + + """ + results = Run(self.list_of_files, do_exit=False) + score = results.linter.stats['global_note'] + self.assertGreaterEqual(score, self.MINIMUM_SCORE) + + +if __name__ == '__main__': + unittest.main() diff --git a/generator/test/test_enums.py b/generator/test/test_enums.py new file mode 100644 index 000000000..6efa64f15 --- /dev/null +++ b/generator/test/test_enums.py @@ -0,0 +1,56 @@ +from collections import OrderedDict, defaultdict +from unittest import TestCase + +from model.enum import Enum +from model.enum_element import EnumElement +from transformers.enums_producer import EnumsProducer + + +class TestEnumsProducer(TestCase): + def setUp(self): + self.maxDiff = None + + self.producer = EnumsProducer('SDLEnum', defaultdict(dict)) + + def test_FunctionID(self): + elements = OrderedDict() + elements['RESERVED'] = EnumElement(name='RESERVED', value=0) + elements['RegisterAppInterfaceID'] = EnumElement(name='RegisterAppInterfaceID', hex_value=1) + elements['PerformAudioPassThruID'] = EnumElement(name='PerformAudioPassThruID', hex_value=10) + + item = Enum(name='FunctionID', elements=elements) + expected = OrderedDict() + expected['origin'] = 'FunctionID' + expected['name'] = 'SDLFunctionID' + expected['imports'] = {'.h': {'SDLEnum'}, '.m': {'SDLEnum'}} + expected['params'] = ( + self.producer.param_named(description=[], name='Reserved', origin='RESERVED', since=None, value=0), + self.producer.param_named(description=[], name='RegisterAppInterface', origin='RegisterAppInterfaceID', + since=None, value=None), + self.producer.param_named(description=[], name='PerformAudioPassThru', origin='PerformAudioPassThruID', + since=None, value=None),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_TextFieldName(self): + elements = OrderedDict() + elements['SUCCESS'] = EnumElement(name='SUCCESS') + elements['mainField1'] = EnumElement(name='mainField1') + elements['H264'] = EnumElement(name='H264') + elements['UNSUPPORTED_REQUEST'] = EnumElement(name='UNSUPPORTED_REQUEST') + item = Enum(name='TextFieldName', elements=elements) + + expected = OrderedDict() + expected['origin'] = 'TextFieldName' + expected['name'] = 'SDLTextFieldName' + expected['imports'] = {'.h': {'SDLEnum'}, '.m': {'SDLEnum'}} + expected['params'] = ( + self.producer.param_named(description=[], name='Success', origin='SUCCESS', since=None, value=None), + self.producer.param_named(description=[], name='MainField1', origin='mainField1', since=None, value=None), + self.producer.param_named(description=[], name='H264', origin='H264', since=None, value=None), + self.producer.param_named(description=[], name='UnsupportedRequest', origin='UNSUPPORTED_REQUEST', + since=None, value=None)) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) diff --git a/generator/test/test_functions.py b/generator/test/test_functions.py new file mode 100644 index 000000000..fdc6ee9d3 --- /dev/null +++ b/generator/test/test_functions.py @@ -0,0 +1,351 @@ +import re +from collections import namedtuple, OrderedDict, defaultdict +from unittest import TestCase + +from model.array import Array +from model.boolean import Boolean +from model.enum import Enum +from model.enum_element import EnumElement +from model.float import Float +from model.function import Function +from model.integer import Integer +from model.param import Param +from model.string import String +from model.struct import Struct +from transformers.functions_producer import FunctionsProducer + + +class TestFunctionsProducer(TestCase): + def setUp(self): + self.maxDiff = None + + Paths = namedtuple('Paths', 'request_class response_class notification_class function_names parameter_names') + paths = Paths(request_class='SDLRPCRequest', + response_class='SDLRPCResponse', + notification_class='SDLRPCNotification', + function_names='SDLRPCFunctionNames', + parameter_names='SDLRPCParameterNames') + + names = ('FileType', 'Language', 'SyncMsgVersion', 'TemplateColorScheme', 'TTSChunk', 'Choice') + self.producer = FunctionsProducer(paths, names, defaultdict(dict)) + + def test_process_function_name(self): + functions = { + 'RegisterAppInterface': Function(name='RegisterAppInterface', + function_id=EnumElement(name='RegisterAppInterfaceID'), since='3.0.0', + message_type=EnumElement(name='request'), + description=['RegisterAppInterface description'], params={ + 'syncMsgVersion': Param(name='syncMsgVersion', param_type=Float(), since='3.5.0', + description=['syncMsgVersion description'])}), + 'OnHMIStatus': Function(name='OnHMIStatus', function_id=EnumElement(name='OnHMIStatusID'), since='4.0.0', + message_type=EnumElement(name='notification'), + description=['OnHMIStatus description'], params={ + 'acEnable': Param(name='acEnable', param_type=Integer(), since='4.5.0', + description=['acEnable description'])})} + structs = { + 'SoftButton': Struct(name='SoftButton', members={ + 'image': Param(name='image', param_type=String(), since='1.0.0', description=['image description']), + 'ignore': Param(name='ignore', param_type=Struct(name='ignore'))}), + 'PresetBankCapabilities': Struct(name='PresetBankCapabilities', members={ + 'availableHdChannelsAvailable': Param(name='availableHdChannelsAvailable', param_type=Boolean(), + since='2.0.0', + description=['availableHDChannelsAvailable description'])})} + + expected = [ + self.producer.common_names( + description=['OnHMIStatus description'], name='OnHMIStatus', origin='OnHMIStatus', since='4.0.0'), + self.producer.common_names( + description=['RegisterAppInterface description'], name='RegisterAppInterface', + origin='RegisterAppInterface', since='3.0.0')] + actual = self.producer.get_function_names(functions) + self.assertListEqual(expected, actual['params']) + + expected = [ + self.producer.common_names(description=['acEnable description'], name='AcEnable', + origin='acEnable', since='4.5.0'), + self.producer.common_names(description=['availableHDChannelsAvailable description'], + since='2.0.0', name='AvailableHdChannelsAvailable', + origin='availableHdChannelsAvailable'), + self.producer.common_names(description=[], name='Ignore', origin='ignore', since=None), + self.producer.common_names(description=['image description'], name='Image', origin='image', since='1.0.0'), + self.producer.common_names(description=[], name='PresetBankCapabilities', origin='PresetBankCapabilities', since=None), + self.producer.common_names(description=[], name='SoftButton', origin='SoftButton', since=None), + self.producer.common_names(description=['syncMsgVersion description'], name='SyncMsgVersion', + origin='syncMsgVersion', since='3.5.0')] + actual = self.producer.get_simple_params(functions, structs) + self.assertListEqual(expected, actual['params']) + + def test_RegisterAppInterfaceRequest(self): + params = OrderedDict() + params['syncMsgVersion'] = Param( + name='syncMsgVersion', param_type=Struct(name='SyncMsgVersion', description=['Specifies the'], members={ + 'majorVersion': Param(name='majorVersion', param_type=Integer())}), description=['See SyncMsgVersion'], + is_mandatory=True) + params['fullAppID'] = Param(name='fullAppID', description=['ID used'], param_type=String(), is_mandatory=False) + params['dayColorScheme'] = Param( + name='dayColorScheme', param_type=Struct(name='TemplateColorScheme', description=[ + '\n A color scheme for all display layout templates.\n ']), is_mandatory=False) + params['ttsName'] = Param( + name='ttsName', description=['\n TTS string for'], is_mandatory=False, + param_type=Array(element_type=Struct(name='TTSChunk', description=['A TTS chunk']))) + params['isMediaApplication'] = Param( + name='isMediaApplication', param_type=Boolean(), + description=['\n Indicates if the application is a media or a '], is_mandatory=True) + + item = Function(name='RegisterAppInterface', function_id=EnumElement(name='RegisterAppInterfaceID'), + since='1.0.0', + description=['\n Establishes an interface with a mobile application.\n ' + 'Before registerAppInterface no other commands will be accepted/executed.\n '], + message_type=EnumElement(name='request'), params=params) + expected = OrderedDict() + expected['origin'] = 'RegisterAppInterface' + expected['name'] = 'SDLRegisterAppInterface' + expected['extends_class'] = 'SDLRPCRequest' + expected['imports'] = { + '.h': {'enum': {'SDLRPCRequest'}, 'struct': {'SDLTemplateColorScheme', 'SDLTTSChunk', 'SDLSyncMsgVersion'}}, + '.m': {'SDLTemplateColorScheme', 'SDLTTSChunk', 'SDLSyncMsgVersion'}} + expected['description'] = ['Establishes an interface with a mobile application. Before registerAppInterface no ' + 'other commands will be', 'accepted/executed.'] + expected['since'] = '1.0.0' + expected['params'] = ( + self.producer.param_named( + constructor_argument='syncMsgVersion', constructor_argument_override=None, + constructor_prefix='SyncMsgVersion', deprecated=False, description=['See SyncMsgVersion'], + for_name='object', mandatory=True, method_suffix='SyncMsgVersion', modifier='strong', + of_class='SDLSyncMsgVersion.class', origin='syncMsgVersion', since=None, + type_native='SDLSyncMsgVersion *', type_sdl='SDLSyncMsgVersion *'), + self.producer.param_named( + constructor_argument='fullAppID', constructor_argument_override=None, constructor_prefix='FullAppID', + deprecated=False, description=['ID used', + '{"default_value": null, "max_length": null, "min_length": null}'], + for_name='object', mandatory=False, method_suffix='FullAppID', modifier='strong', + of_class='NSString.class', origin='fullAppID', since=None, type_native='NSString *', + type_sdl='NSString *'), + self.producer.param_named( + constructor_argument='dayColorScheme', constructor_argument_override=None, mandatory=False, + constructor_prefix='DayColorScheme', deprecated=False, description=[], for_name='object', + method_suffix='DayColorScheme', modifier='strong', of_class='SDLTemplateColorScheme.class', + origin='dayColorScheme', since=None, type_native='SDLTemplateColorScheme *', + type_sdl='SDLTemplateColorScheme *'), + self.producer.param_named( + constructor_argument='ttsName', constructor_argument_override=None, constructor_prefix='TtsName', + deprecated=False, description=['TTS string for'], for_name='objects', mandatory=False, + method_suffix='TtsName', modifier='strong', of_class='SDLTTSChunk.class', origin='ttsName', since=None, + type_native='NSArray<SDLTTSChunk *> *', type_sdl='NSArray<SDLTTSChunk *> *'), + self.producer.param_named( + constructor_argument='isMediaApplication', constructor_argument_override=None, + constructor_prefix='IsMediaApplication', deprecated=False, + description=['Indicates if the application is a media or a'], for_name='object', mandatory=True, + method_suffix='IsMediaApplication', modifier='strong', of_class='NSNumber.class', + origin='isMediaApplication', since=None, type_native='BOOL', type_sdl='NSNumber<SDLBool> *')) + + mandatory_arguments = [ + self.producer.argument_named(variable='syncMsgVersion', deprecated=False, origin='syncMsgVersion', + constructor_argument='syncMsgVersion'), + self.producer.argument_named(variable='isMediaApplication', deprecated=False, origin='isMediaApplication', + constructor_argument='@(isMediaApplication)')] + not_mandatory_arguments = [ + self.producer.argument_named(variable='fullAppID', deprecated=False, origin='fullAppID', + constructor_argument='fullAppID'), + self.producer.argument_named(variable='dayColorScheme', deprecated=False, origin='dayColorScheme', + constructor_argument='dayColorScheme'), + self.producer.argument_named(variable='ttsName', deprecated=False, origin='ttsName', + constructor_argument='ttsName')] + mandatory_init = 'SyncMsgVersion:(SDLSyncMsgVersion *)syncMsgVersion ' \ + 'isMediaApplication:(BOOL)isMediaApplication' + + expected['constructors'] = ( + self.producer.constructor_named( + all=mandatory_arguments, arguments=mandatory_arguments, deprecated=False, + init=mandatory_init, self=''), + self.producer.constructor_named( + all=mandatory_arguments + not_mandatory_arguments, arguments=not_mandatory_arguments, deprecated=False, + init=mandatory_init + ' fullAppID:(nullable NSString *)fullAppID dayColorScheme:(nullable ' + 'SDLTemplateColorScheme *)dayColorScheme ttsName:(nullable NSArray<SDLTTSChunk ' + '*> *)ttsName', + self=re.sub(r'([\w\d]+:)\([\w\d\s<>*]*\)([\w\d]+\s*)', r'\1\2', mandatory_init))) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_RegisterAppInterfaceResponse(self): + params = OrderedDict() + params['success'] = Param(name='success', param_type=Boolean(), description=[' True if '], is_mandatory=False) + params['language'] = Param(name='language', is_mandatory=False, param_type=Enum(name='Language', elements={ + 'EN-US': EnumElement(name='EN-US', description=['English - US'])}), description=['The currently']) + params['supportedDiagModes'] = Param( + name='supportedDiagModes', is_mandatory=False, description=['\n Specifies the'], + param_type=Array(element_type=Integer(max_value=255, min_value=0), max_size=100, min_size=1)) + params['hmiZoneCapabilities'] = Param(name='hmiZoneCapabilities', is_mandatory=False, + param_type=Array(element_type=Enum(name='HmiZoneCapabilities'), + max_size=100, min_size=1)) + + item = Function(name='RegisterAppInterface', function_id=EnumElement(name='RegisterAppInterfaceID'), + description=['The response '], message_type=EnumElement(name='response'), params=params) + + expected = OrderedDict() + expected['origin'] = 'RegisterAppInterface' + expected['name'] = 'SDLRegisterAppInterfaceResponse' + expected['extends_class'] = 'SDLRPCResponse' + expected['imports'] = {'.h': {'enum': {'SDLRPCResponse', 'SDLLanguage'}, 'struct': set()}, + '.m': {'SDLLanguage'}} + expected['description'] = ['The response'] + expected['params'] = ( + self.producer.param_named( + constructor_argument='language', constructor_argument_override=None, constructor_prefix='Language', + deprecated=False, description=['The currently'], for_name='enum', mandatory=False, + method_suffix='Language', modifier='strong', of_class='', origin='language', + since=None, type_native='SDLLanguage ', type_sdl='SDLLanguage '), + self.producer.param_named( + constructor_argument='supportedDiagModes', constructor_argument_override=None, + constructor_prefix='SupportedDiagModes', deprecated=False, description=['Specifies the'], + for_name='objects', mandatory=False, method_suffix='SupportedDiagModes', modifier='strong', + of_class='NSNumber.class', origin='supportedDiagModes', since=None, + type_native='NSArray<NSNumber<SDLUInt> *> *', type_sdl='NSArray<NSNumber<SDLUInt> *> *'), + self.producer.param_named( + constructor_argument='hmiZoneCapabilities', constructor_argument_override=None, + constructor_prefix='HmiZoneCapabilities', deprecated=False, description=[], for_name='enums', + mandatory=False, method_suffix='HmiZoneCapabilities', modifier='strong', + of_class='', origin='hmiZoneCapabilities', since=None, + type_native='NSArray<SDLHmiZoneCapabilities> *', type_sdl='NSArray<SDLHmiZoneCapabilities> *')) + + arguments = [self.producer.argument_named( + variable='language', deprecated=False, origin='language', constructor_argument='language'), + self.producer.argument_named( + variable='supportedDiagModes', deprecated=False, origin='supportedDiagModes', + constructor_argument='supportedDiagModes'), + self.producer.argument_named( + variable='hmiZoneCapabilities', deprecated=False, origin='hmiZoneCapabilities', + constructor_argument='hmiZoneCapabilities')] + + expected['constructors'] = ( + self.producer.constructor_named( + all=arguments, arguments=arguments, deprecated=False, + init='Language:(nullable SDLLanguage)language supportedDiagModes:(nullable NSArray<NSNumber<SDLUInt> *>' + ' *)supportedDiagModes hmiZoneCapabilities:(nullable NSArray<SDLHmiZoneCapabilities> *)' + 'hmiZoneCapabilities', + self=''),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_OnHMIStatus(self): + item = Function(name='OnHMIStatus', function_id=EnumElement(name='OnHMIStatusID'), + message_type=EnumElement(name='notification'), params={ + 'hmiLevel': Param(name='hmiLevel', param_type=Enum(name='HMILevel')) + }) + expected = OrderedDict() + expected['origin'] = 'OnHMIStatus' + expected['name'] = 'SDLOnHMIStatus' + expected['extends_class'] = 'SDLRPCNotification' + expected['imports'] = { + ".h": {'enum': {'SDLRPCNotification'}, 'struct': set()}, + ".m": set() + } + expected['params'] = ( + self.producer.param_named( + constructor_argument='hmiLevel', constructor_argument_override=None, constructor_prefix='HmiLevel', + deprecated=False, description=[], for_name='enum', mandatory=True, method_suffix='HmiLevel', + modifier='strong', of_class='', origin='hmiLevel', since=None, + type_native='SDLHMILevel ', type_sdl='SDLHMILevel '),) + + arguments = [self.producer.argument_named(variable='hmiLevel', deprecated=False, origin='hmiLevel', + constructor_argument='hmiLevel')] + + expected['constructors'] = (self.producer.constructor_named( + all=arguments, arguments=arguments, deprecated=False, self='', init='HmiLevel:(SDLHMILevel)hmiLevel'),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_CreateWindow(self): + params = OrderedDict() + params['windowID'] = Param(name='windowID', param_type=Integer()) + params['cmdID'] = Param(name='cmdID', param_type=Integer(max_value=2000000000, min_value=0)) + params['position'] = Param(name='position', param_type=Integer(default_value=1000, max_value=1000, min_value=0)) + params['speed'] = Param(name='speed', param_type=Float(max_value=700.0, min_value=0.0)) + params['offset'] = Param(name='offset', param_type=Integer(max_value=100000000000, min_value=0)) + item = Function(name='CreateWindow', function_id=EnumElement(name='CreateWindowID'), + message_type=EnumElement(name='request'), params=params) + + expected = OrderedDict() + expected['origin'] = 'CreateWindow' + expected['name'] = 'SDLCreateWindow' + expected['extends_class'] = 'SDLRPCRequest' + expected['imports'] = {'.m': set(), '.h': {'struct': set(), 'enum': {'SDLRPCRequest'}}} + expected['params'] = ( + self.producer.param_named( + constructor_argument='windowID', constructor_argument_override=None, constructor_prefix='WindowID', + deprecated=False, description=['{"default_value": null, "max_value": null, "min_value": null}'], + for_name='object', mandatory=True, method_suffix='WindowID', modifier='strong', + of_class='NSNumber.class', origin='windowID', since=None, type_native='UInt32', + type_sdl='NSNumber<SDLInt> *'), + self.producer.param_named( + constructor_argument='cmdID', constructor_argument_override=None, constructor_prefix='CmdID', + deprecated=False, description=['{"default_value": null, "max_value": 2000000000, "min_value": 0}'], + for_name='object', mandatory=True, method_suffix='CmdID', modifier='strong', of_class='NSNumber.class', + origin='cmdID', since=None, type_native='UInt32', type_sdl='NSNumber<SDLUInt> *'), + self.producer.param_named( + constructor_argument='position', constructor_argument_override=None, constructor_prefix='Position', + deprecated=False, description=['{"default_value": 1000, "max_value": 1000, "min_value": 0}'], + for_name='object', mandatory=True, method_suffix='Position', modifier='strong', + of_class='NSNumber.class', origin='position', since=None, type_native='UInt16', + type_sdl='NSNumber<SDLUInt> *'), + self.producer.param_named( + constructor_argument='speed', constructor_argument_override=None, constructor_prefix='Speed', + deprecated=False, description=['{"default_value": null, "max_value": 700.0, "min_value": 0.0}'], + for_name='object', mandatory=True, method_suffix='Speed', modifier='strong', of_class='NSNumber.class', + origin='speed', since=None, type_native='float', type_sdl='NSNumber<SDLFloat> *'), + self.producer.param_named( + constructor_argument='offset', constructor_argument_override=None, constructor_prefix='Offset', + deprecated=False, description=['{"default_value": null, "max_value": 100000000000, "min_value": 0}'], + for_name='object', mandatory=True, method_suffix='Offset', modifier='strong', of_class='NSNumber.class', + origin='offset', since=None, type_native='UInt64', type_sdl='NSNumber<SDLUInt> *')) + + expected_arguments = [self.producer.argument_named(variable='windowID', deprecated=False, origin='windowID', + constructor_argument='@(windowID)'), + self.producer.argument_named(variable='cmdID', deprecated=False, origin='cmdID', + constructor_argument='@(cmdID)'), + self.producer.argument_named(variable='position', deprecated=False, origin='position', + constructor_argument='@(position)'), + self.producer.argument_named(variable='speed', deprecated=False, origin='speed', + constructor_argument='@(speed)'), + self.producer.argument_named(variable='offset', deprecated=False, origin='offset', + constructor_argument='@(offset)')] + + expected['constructors'] = (self.producer.constructor_named( + all=expected_arguments, arguments=expected_arguments, deprecated=False, self='', + init='WindowID:(UInt32)windowID cmdID:(UInt32)cmdID position:(UInt16)position speed:(float)speed ' + 'offset:(UInt64)offset'),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_CreateInteractionChoiceSet(self): + params = OrderedDict() + params['choiceSet'] = Param(name='choiceSet', param_type=Array(element_type=Struct(name='Choice'))) + item = Function(name='CreateInteractionChoiceSet', function_id=EnumElement(name='CreateInteractionChoiceSetID'), + message_type=EnumElement(name='request'), params=params) + + expected = OrderedDict() + expected['origin'] = 'CreateInteractionChoiceSet' + expected['name'] = 'SDLCreateInteractionChoiceSet' + expected['extends_class'] = 'SDLRPCRequest' + expected['imports'] = {'.m': {'SDLChoice'}, '.h': {'struct': {'SDLChoice'}, 'enum': {'SDLRPCRequest'}}} + expected['params'] = ( + self.producer.param_named( + constructor_argument='choiceSet', constructor_argument_override=None, + constructor_prefix='ChoiceSet', deprecated=False, description=[], for_name='objects', mandatory=True, + method_suffix='ChoiceSet', modifier='strong', of_class='SDLChoice.class', origin='choiceSet', + since=None, type_native='NSArray<SDLChoice *> *', type_sdl='NSArray<SDLChoice *> *'),) + + argument = [ + self.producer.argument_named(variable='choiceSet', deprecated=False, constructor_argument='choiceSet', + origin='choiceSet')] + + expected['constructors'] = (self.producer.constructor_named( + all=argument, arguments=argument, deprecated=False, self='', + init='ChoiceSet:(NSArray<SDLChoice *> *)choiceSet'),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) diff --git a/generator/test/test_structs.py b/generator/test/test_structs.py new file mode 100644 index 000000000..a1ee10f46 --- /dev/null +++ b/generator/test/test_structs.py @@ -0,0 +1,72 @@ +import re +from collections import OrderedDict, defaultdict +from unittest import TestCase + +from model.param import Param +from model.string import String +from model.struct import Struct +from transformers.structs_producer import StructsProducer + + +class TestStructsProducer(TestCase): + def setUp(self): + self.maxDiff = None + + self.producer = StructsProducer('SDLRPCStruct', ['Image'], defaultdict(dict)) + + def test_Version(self): + version = self.producer.get_version + self.assertIsNotNone(version) + self.assertTrue(re.match(r'^\d*\.\d*\.\d*$', version)) + + def test_CloudAppProperties(self): + item = Struct(name='CloudAppProperties', members={ + 'appID': Param(name='appID', param_type=String()) + }) + expected = OrderedDict() + expected['origin'] = 'CloudAppProperties' + expected['name'] = 'SDLCloudAppProperties' + expected['extends_class'] = 'SDLRPCStruct' + expected['imports'] = {'.m': set(), '.h': {'enum': {'SDLRPCStruct'}, 'struct': set()}} + expected['params'] = ( + self.producer.param_named( + constructor_argument='appID', constructor_argument_override=None, constructor_prefix='AppID', + deprecated=False, description=['{"default_value": null, "max_length": null, "min_length": null}'], + for_name='object', mandatory=True, method_suffix='AppID', modifier='strong', of_class='NSString.class', + origin='appID', since=None, type_native='NSString *', type_sdl='NSString *', ),) + + argument = [ + self.producer.argument_named(variable='appID', deprecated=False, constructor_argument='appID', origin='appID')] + + expected['constructors'] = (self.producer.constructor_named( + all=argument, arguments=argument, deprecated=False, self='', + init='AppID:(NSString *)appID'),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_not_mandatory_NS_DESIGNATED_INITIALIZER(self): + item = Struct(name='CloudAppProperties', members={ + 'appID': Param(name='appID', param_type=String(), is_mandatory=False) + }) + expected = OrderedDict() + expected['origin'] = 'CloudAppProperties' + expected['name'] = 'SDLCloudAppProperties' + expected['extends_class'] = 'SDLRPCStruct' + expected['imports'] = {'.m': set(), '.h': {'enum': {'SDLRPCStruct'}, 'struct': set()}} + expected['params'] = ( + self.producer.param_named( + constructor_argument='appID', constructor_argument_override=None, constructor_prefix='AppID', + deprecated=False, description=['{"default_value": null, "max_length": null, "min_length": null}'], + for_name='object', mandatory=False, method_suffix='AppID', modifier='strong', of_class='NSString.class', + origin='appID', since=None, type_native='NSString *', type_sdl='NSString *', ),) + + argument = [ + self.producer.argument_named(variable='appID', deprecated=False, constructor_argument='appID', origin='appID')] + + expected['constructors'] = (self.producer.constructor_named( + all=argument, arguments=argument, deprecated=False, self='', + init='AppID:(nullable NSString *)appID'),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) diff --git a/generator/transformers/__init__.py b/generator/transformers/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/generator/transformers/__init__.py diff --git a/generator/transformers/common_producer.py b/generator/transformers/common_producer.py new file mode 100644 index 000000000..9ac9f5084 --- /dev/null +++ b/generator/transformers/common_producer.py @@ -0,0 +1,536 @@ +""" +Common transformer +""" +import json +import logging +import re +import textwrap +from abc import ABC +from collections import OrderedDict, namedtuple +from pprint import pformat + +from model.array import Array +from model.boolean import Boolean +from model.enum import Enum +from model.float import Float +from model.function import Function +from model.integer import Integer +from model.param import Param +from model.string import String +from model.struct import Struct + + +class InterfaceProducerCommon(ABC): + """ + Common transformer + """ + + version = '1.0.0' + + def __init__(self, container_name, mapping, names=()): + self.logger = logging.getLogger(self.__class__.__name__) + self.container_name = container_name + self.mapping = mapping + self.names = names + self.param_named = namedtuple('param_named', + 'origin constructor_argument constructor_prefix deprecated mandatory since ' + 'method_suffix of_class type_native type_sdl modifier for_name description ' + 'constructor_argument_override') + self.constructor_named = namedtuple('constructor', 'init self arguments all deprecated') + self.argument_named = namedtuple('argument', 'origin constructor_argument variable deprecated') + + @property + def get_version(self): + """ + + :return: + """ + return self.version + + def transform(self, item: (Enum, Function, Struct), render: dict) -> dict: + """ + + :param item: + :param render: + :return: + """ + if item.description: + render['description'] = self.extract_description(item.description) + if item.since: + render['since'] = item.since + if item.deprecated and item.deprecated.lower() == 'true': + render['deprecated'] = True + + render['params'] = OrderedDict() + + for param in getattr(item, self.container_name).values(): + render['params'][param.name] = self.extract_param(param) + if isinstance(item, (Struct, Function)): + self.extract_imports(param, render['imports']) + + self.custom_mapping(render) + + if 'constructors' not in render and isinstance(item, (Struct, Function)): + designated_initializer = render['designated_initializer'] if 'designated_initializer' in render else False + render['constructors'] = self.extract_constructors(render['params'], designated_initializer) + + render['params'] = tuple(render['params'].values()) + return render + + def extract_imports(self, param: Param, imports): + """ + + :param param: + :param imports: + :return: + """ + + def evaluate(element): + if isinstance(element, (Struct, Enum)): + return element.name, type(element).__name__.lower() + return None, None + + if isinstance(param.param_type, Array): + type_origin, kind = evaluate(param.param_type.element_type) + else: + type_origin, kind = evaluate(param.param_type) + + if type_origin in self.names: + name = 'SDL' + type_origin + imports['.h'][kind].add(name) + imports['.m'].add(name) + + return imports + + @staticmethod + def title(name): + """ + + :param name: + :return: + """ + return name[:1].upper() + name[1:] + + @staticmethod + def minimize_first(name): + """ + + :param name: + :return: + """ + return name[:1].lower() + name[1:] + + @staticmethod + def extract_description(data, length: int = 113) -> list: + """ + Evaluate, align and delete @TODO + :param data: list with description + :param length: + :return: evaluated string + """ + if not data: + return [] + if isinstance(data, list): + data = ' '.join(data) + return textwrap.wrap(re.sub(r'(\s{2,}|\n|\[@TODO.+)', ' ', data).strip(), length) + + @staticmethod + def nullable(type_native, mandatory): + """ + + :param type_native: + :param mandatory: + :return: + """ + if mandatory or re.match(r'\w*Int\d*|BOOL|float|double', type_native): + return '' + return 'nullable ' + + @staticmethod + def wrap(item): + """ + + :param item: + :return: + """ + if getattr(item, 'constructor_argument_override', None) is not None: + return item.constructor_argument_override + if re.match(r'\w*Int\d*|BOOL|float|double', item.type_native): + return '@({})'.format(item.constructor_argument) + return item.constructor_argument + + def extract_constructor(self, data: list, mandatory: bool) -> dict: + """ + + :param data: + :param mandatory: + :return: + """ + data = list(data) + # deprecated = any([m.deprecated for m in data if hasattr(m, 'deprecated')]) + + first = data.pop(0) + init = ['{}:({}{}){}'.format(self.title(first.constructor_prefix), + self.nullable(first.type_native, mandatory), + first.type_native.strip(), first.constructor_argument)] + arguments = [self.argument_named(origin=first.origin, constructor_argument=self.wrap(first), + variable=first.constructor_argument, deprecated=first.deprecated)] + for param in data: + arguments.append(self.argument_named(origin=param.origin, constructor_argument=self.wrap(param), + variable=param.constructor_argument, deprecated=param.deprecated)) + init_str = '{}:({}{}){}'.format(self.minimize_first(param.constructor_prefix), + self.nullable(param.type_native, mandatory), + param.type_native.strip(), param.constructor_argument) + init.append(init_str) + + return {'init': ' '.join(init), 'self': '', 'arguments': arguments, 'all': arguments, 'deprecated': False} + + def extract_constructors(self, data: dict, designated_initializer: bool = None) -> tuple: + """ + + :param data: + :param designated_initializer: + :return: + """ + mandatory = [] + not_mandatory = [] + for param in data.values(): + if param.mandatory: + mandatory.append(param) + else: + not_mandatory.append(param) + + result = [] + if mandatory: + mandatory = self.extract_constructor(mandatory, True) + else: + mandatory = OrderedDict() + + if not_mandatory: + not_mandatory = self.extract_constructor(not_mandatory, False) + if mandatory: + not_mandatory['init'] = '{} {}'.format(mandatory['init'], self.minimize_first(not_mandatory['init'])) + not_mandatory['all'] = mandatory['arguments'] + not_mandatory['arguments'] + not_mandatory['deprecated'] = any([m.deprecated for m in mandatory if hasattr(m, 'deprecated')]) + not_mandatory['self'] = re.sub(r'([\w\d]+:)\([\w\d\s<>*]*\)([\w\d]+\s*)', r'\1\2', mandatory['init']) + if not mandatory and designated_initializer: + not_mandatory['init'] = not_mandatory['init'] + ' NS_DESIGNATED_INITIALIZER' + result.append(self.constructor_named(**not_mandatory)) + + if mandatory: + if designated_initializer: + mandatory['init'] = mandatory['init'] + ' NS_DESIGNATED_INITIALIZER' + result.insert(0, self.constructor_named(**mandatory)) + + return tuple(result) + + @staticmethod + def evaluate_type(instance) -> dict: + """ + + :param instance: + :return: + """ + data = OrderedDict() + if isinstance(instance, Enum): + data['for_name'] = 'enum' + data['of_class'] = '' + else: + data['for_name'] = 'object' + if isinstance(instance, (Struct, Enum)): + data['type_sdl'] = 'SDL' + instance.name + data['type_native'] = data['type_sdl'] = 'SDL{} '.format(instance.name) + if isinstance(instance, Struct): + data['of_class'] = 'SDL{}.class'.format(instance.name) + data['type_native'] = data['type_sdl'] = 'SDL{} *'.format(instance.name) + elif isinstance(instance, (Integer, Float)): + if isinstance(instance, Float): + data['type_sdl'] = 'SDLFloat' + data['type_native'] = 'float' + if isinstance(instance, Integer): + if not instance.max_value: + data['type_native'] = 'UInt32' + elif instance.max_value <= 255: + data['type_native'] = 'UInt8' + elif instance.max_value <= 65535: + data['type_native'] = 'UInt16' + elif instance.max_value <= 4294967295: + data['type_native'] = 'UInt32' + elif instance.max_value > 4294967295: + data['type_native'] = 'UInt64' + if instance.min_value is None or instance.min_value < 0: + data['type_sdl'] = 'SDLInt' + elif instance.min_value >= 0: + data['type_sdl'] = 'SDLUInt' + data['of_class'] = 'NSNumber.class' + data['type_sdl'] = 'NSNumber<{}> *'.format(data['type_sdl']) + elif isinstance(instance, String): + data['of_class'] = 'NSString.class' + data['type_sdl'] = data['type_native'] = 'NSString *' + elif isinstance(instance, Boolean): + data['of_class'] = 'NSNumber.class' + data['type_native'] = 'BOOL' + data['type_sdl'] = 'NSNumber<SDLBool> *' + return data + + def extract_type(self, param: Param) -> dict: + """ + + :param param: + :return: + """ + + if isinstance(param.param_type, Array): + data = self.evaluate_type(param.param_type.element_type) + data['for_name'] = data['for_name'] + 's' + data['type_sdl'] = data['type_native'] = 'NSArray<{}> *'.format(data['type_sdl'].strip()) + return data + return self.evaluate_type(param.param_type) + + @staticmethod + def param_origin_change(name): + """ + + :param name: + :return: + """ + return {'origin': name, + 'constructor_argument': name, + 'constructor_prefix': InterfaceProducerCommon.title(name), + 'method_suffix': InterfaceProducerCommon.title(name)} + + def extract_param(self, param: Param): + """ + + :param param: + :return: + """ + data = {'constructor_argument_override': None, + 'description': self.extract_description(param.description), + 'since': param.since, + 'mandatory': param.is_mandatory, + 'deprecated': json.loads(param.deprecated.lower()) if param.deprecated else False, + 'modifier': 'strong'} + if isinstance(param.param_type, (Integer, Float, String)): + data['description'].append(json.dumps(vars(param.param_type), sort_keys=True)) + + data.update(self.extract_type(param)) + data.update(self.param_origin_change(param.name)) + return self.param_named(**data) + + @staticmethod + def minimize_last_id(name): + """ + + :param name: + :return: + """ + if name.upper().endswith('ID'): + return name[:-1] + name[-1:].lower() + return name + + @staticmethod + def remove_asterisk(data: dict): + """ + + :param data: + :return: + """ + for key, value in data.copy().items(): + if key.startswith('type') and value.endswith('*'): + if value.startswith('NSArray') and value.endswith('*> *'): + data[key] = value[:-5] + '> *' + else: + data[key] = value[:-1] + + @staticmethod + def add_asterisk(data: dict): + """ + + :param data: + :return: + """ + for key, value in data.copy().items(): + if key.startswith('type') and not value.endswith('*'): + data[key] = value.strip() + ' *' + + def evaluate_param(self, data, name): + """ + + :param name: + :param data: + :return: + """ + param_name = data['origin'] if 'origin' in data else '' + redundant = list(set(data.keys()) - set(self.param_named._fields)) + if redundant: + self.logger.error('%s/%s, redundant attributes (%s)', name, param_name, redundant) + return False + missed = list(set(self.param_named._fields) - set(data.keys())) + if missed: + self.logger.error('%s/%s, missed attributes (%s)', name, param_name, missed) + return False + return True + + def custom_mapping(self, render): + """ + + :param render: + :return: + """ + key = render['origin'] + if 'extends_class' in render: + key += render['extends_class'].replace('SDLRPC', '') + if key in self.mapping: + custom = self.mapping[key].copy() + elif render['origin'] in self.mapping: + custom = self.mapping[render['origin']].copy() + else: + return + self.logger.debug('%s fount in mapping', render['origin']) + + if 'params_title' in custom and custom['params_title'] is False: + for name, custom_data in render['params'].items(): + if re.match(r'^[A-Z\d]+$', custom_data.origin): + render['params'][name] = render['params'][name]._replace(name=custom_data.origin) + del custom['params_title'] + + if 'remove_asterisk' in custom: + for name, custom_data in render['params'].items(): + data = custom_data._asdict() + self.remove_asterisk(data) + render['params'][name] = custom_data._replace(**data) + del custom['remove_asterisk'] + + if 'description' in custom: + render['description'] = self.extract_description(custom['description']) + del custom['description'] + + if '-params' in custom: + for name in custom['-params']: + if name in render['params']: + imp = render['params'][name].of_class.replace('.class', '') + if imp in render['imports']['.m']: + render['imports']['.m'].remove(imp) + for kind in ('struct', 'enum'): + if imp in render['imports']['.h'][kind]: + render['imports']['.h'][kind].remove(imp) + del render['params'][name] + del custom['-params'] + + if 'minimize_last_id' in custom: + for name, custom_data in render['params'].items(): + if name.upper().endswith('ID'): + render['params'][name] = custom_data._replace( + constructor_argument=self.minimize_last_id(custom_data.constructor_argument), + constructor_prefix=self.minimize_last_id(custom_data.constructor_prefix), + method_suffix=self.minimize_last_id(custom_data.method_suffix)) + del custom['minimize_last_id'] + + if 'sort_params' in custom: + render['params'] = OrderedDict(sorted(render['params'].items())) + del custom['sort_params'] + + if 'template' in custom: + if isinstance(custom['template'], bool) and custom['template']: + render['template'] = render['name'][3:] + else: + render['template'] = custom['template'] + del custom['template'] + + if 'maximize_method' in custom: + if isinstance(custom['maximize_method'], str): + for name, custom_data in render['params'].items(): + tmp = re.findall(r'^([a-z]+)(\w*)$', self.minimize_first(custom_data.method_suffix)).pop() + render['params'][name] = custom_data._replace(method_suffix=tmp[0].upper() + tmp[1]) + elif isinstance(custom['maximize_method'], list): + for name in custom['maximize_method']: + if name in render['params']: + custom_data = render['params'][name] + tmp = re.findall(r'^([a-z]+)(\w*)$', self.minimize_first(custom_data.method_suffix)).pop() + render['params'][name] = custom_data._replace(method_suffix=tmp[0].upper() + tmp[1]) + del custom['maximize_method'] + + for key in ('modifier', 'mandatory'): + if key in custom: + for name, custom_data in render['params'].items(): + render['params'][name] = custom_data._replace(**{key: custom[key]}) + del custom[key] + + for key in ('name', 'designated_initializer', 'deprecated', 'NS_ENUM', 'NS_SWIFT_NAME', 'add_typedef'): + if key in custom and isinstance(custom[key], str): + render[key] = custom[key] + del custom[key] + + for name, custom_data in custom.copy().items(): + self.logger.info('applying manual mapping for %s/%s\t%s', render['name'], name, pformat(custom_data)) + if name in render['params']: + if isinstance(custom_data, str): + render['params'][name] = render['params'][name]._replace(name=custom_data) + elif isinstance(custom_data, dict): + data = self.custom_param_update(render['params'][name]._asdict(), custom_data, render['imports']) + if self.evaluate_param(data, render['origin']): + render['params'][name] = self.param_named(**data) + del custom[name] + elif name not in ['sort_constructor']: + if 'description' in custom[name]: + custom[name]['description'] = self.extract_description(custom[name]['description']) + custom[name]['origin'] = name + for key, value in custom[name].copy().items(): + if key.startswith('type') and not value.endswith('*'): + custom[name][key] = value.strip() + ' ' + if self.evaluate_param(custom[name], render['origin']): + render['params'][name] = self.param_named(**custom[name]) + render['params'].move_to_end(name, last=False) + else: + self.logger.warning('For %s provided undefined mapping for "%s": %s, which will be skipped', + render['name'], name, pformat(custom_data)) + del custom[name] + + if 'sort_constructor' in custom: + sorted_params = OrderedDict(sorted(render['params'].items())) + render['constructors'] = self.extract_constructors(sorted_params) + del custom['sort_constructor'] + + @staticmethod + def custom_param_update(data, custom_data, imports) -> dict: + """ + + :param data: + :param custom_data: + :param imports: + :return: + """ + if 'description' in custom_data: + custom_data['description'] = InterfaceProducerCommon.extract_description(custom_data['description']) + if 'minimize_last_id' in custom_data: + data['constructor_argument'] = InterfaceProducerCommon.minimize_last_id(data['constructor_argument']) + data['constructor_prefix'] = InterfaceProducerCommon.minimize_last_id(data['constructor_prefix']) + data['method_suffix'] = InterfaceProducerCommon.minimize_last_id(data['method_suffix']) + if 'maximize_method' in custom_data: + tmp = re.findall(r'^([a-z]+)(\w*)$', InterfaceProducerCommon.minimize_first(data['method_suffix'])).pop() + data['method_suffix'] = tmp[0].upper() + tmp[1] + if 'origin' in custom_data: + data.update(InterfaceProducerCommon.param_origin_change(custom_data['origin'])) + if 'type' in custom_data: + new_type = re.sub(r'NSArray|[\s<>*]', '', custom_data['type']) + data['type_native'] = data['type_sdl'] = re.sub(r'[\s*]', '', custom_data['type']) + ' *' + data['method_suffix'] = new_type + if data['of_class']: + data['of_class'] = new_type + '.class' + if new_type.lower() in map(str.lower, imports['.m']): + imports['.m'] = set(i for i in imports['.m'] if i.lower() != new_type.lower()) + imports['.m'].add(new_type) + for kind in ('enum', 'struct'): + if new_type.lower() in map(str.lower, imports['.h'][kind]): + imports['.h'][kind] = set(i for i in imports['.h'][kind] if i.lower() != new_type.lower()) + imports['.h'][kind].add(new_type) + if 'remove_asterisk' in custom_data: + InterfaceProducerCommon.remove_asterisk(data) + if 'add_asterisk' in custom_data: + InterfaceProducerCommon.add_asterisk(data) + for key, value in custom_data.copy().items(): + if key.startswith('type') and not value.endswith('*'): + custom_data[key] = value.strip() + ' ' + data.update(custom_data) + return data diff --git a/generator/transformers/enums_producer.py b/generator/transformers/enums_producer.py new file mode 100644 index 000000000..9dc8b7834 --- /dev/null +++ b/generator/transformers/enums_producer.py @@ -0,0 +1,71 @@ +""" +Enums transformer +""" +import logging +import re +from collections import namedtuple, OrderedDict + +from model.enum import Enum +from model.enum_element import EnumElement +from transformers.common_producer import InterfaceProducerCommon + + +class EnumsProducer(InterfaceProducerCommon): + """ + Enums transformer + """ + + def __init__(self, enum_class, mapping=None): + super(EnumsProducer, self).__init__( + container_name='elements', + mapping=mapping.get('enums', {})) + self.enum_class = enum_class + self.logger = logging.getLogger(self.__class__.__name__) + self.param_named = namedtuple('param_named', 'origin description name since value') + + def transform(self, item: Enum, render=None) -> dict: + """ + + :param item: + :param render: + :return: + """ + name = 'SDL{}{}'.format(item.name[:1].upper(), item.name[1:]) + tmp = {self.enum_class} + imports = {'.h': tmp, '.m': tmp} + if not render: + render = OrderedDict() + render['origin'] = item.name + render['name'] = name + render['imports'] = imports + super(EnumsProducer, self).transform(item, render) + return render + + def extract_param(self, param: EnumElement): + """ + + :param param: + :return: + """ + data = {'origin': param.name, 'description': self.extract_description(param.description, 113), + 'since': param.since} + + if re.match(r'^[A-Z]{1,2}\d|\d[A-Z]{1,2}$', param.name): + data['name'] = param.name + elif re.match(r'(^[a-z\d]+$|^[A-Z\d]+$)', param.name): + data['name'] = param.name.title() # .capitalize() + elif re.match(r'^(?=\w*[a-z])(?=\w*[A-Z])\w+$', param.name): + if param.name.endswith('ID'): + data['name'] = param.name[:-2] + else: + data['name'] = param.name[:1].upper() + param.name[1:] + elif re.match(r'^(?=\w*?[a-zA-Z])(?=\w*?[_-])(?=[0-9])?.*$', param.name): + name = [] + for item in re.split('[_-]', param.name): + if re.match(r'^[A-Z\d]+$', item): + name.append(item.title()) + data['name'] = ''.join(name) + + data['value'] = param.value + + return self.param_named(**data) diff --git a/generator/transformers/functions_producer.py b/generator/transformers/functions_producer.py new file mode 100644 index 000000000..d227b62ea --- /dev/null +++ b/generator/transformers/functions_producer.py @@ -0,0 +1,139 @@ +""" +Functions transformer +""" + +import logging +from collections import namedtuple, OrderedDict +from pprint import pformat + +from model.function import Function +from transformers.common_producer import InterfaceProducerCommon + + +class FunctionsProducer(InterfaceProducerCommon): + """ + Functions transformer + """ + + def __init__(self, paths, names, mapping=None): + super(FunctionsProducer, self).__init__( + container_name='params', + names=names, + mapping=mapping.get('functions', {})) + self.request_class = paths.request_class + self.response_class = paths.response_class + self.notification_class = paths.notification_class + self.function_names = paths.function_names + self.parameter_names = paths.parameter_names + self.logger = logging.getLogger(self.__class__.__name__) + self.common_names = namedtuple('common_names', 'name origin description since') + + def transform(self, item: Function, render=None) -> dict: + """ + + :param item: + :param render: + :return: + """ + list(map(item.params.__delitem__, filter(item.params.__contains__, ['success', 'resultCode', 'info']))) + + name = 'SDL' + item.name + imports = {'.h': {'enum': set(), 'struct': set()}, + '.m': set()} + extends_class = None + if item.message_type.name == 'response': + extends_class = self.response_class + name = name + item.message_type.name.capitalize() + elif item.message_type.name == 'request': + extends_class = self.request_class + elif item.message_type.name == 'notification': + extends_class = self.notification_class + if extends_class: + imports['.h']['enum'].add(extends_class) + + if not render: + render = OrderedDict() + render['origin'] = item.name + render['name'] = name + render['extends_class'] = extends_class + render['imports'] = imports + + super(FunctionsProducer, self).transform(item, render) + + return render + + def get_function_names(self, items: dict) -> dict: + """ + + :param items: + :return: dict + """ + render = OrderedDict() + for item in items.values(): + tmp = {'name': self.title(item.name), + 'origin': item.name, + 'description': self.extract_description(item.description), + 'since': item.since} + render[item.name] = self.common_names(**tmp) + + self.custom_mapping_names(render, self.function_names) + + return {'params': sorted(render.values(), key=lambda a: a.name)} + + def get_simple_params(self, functions: dict, structs: dict) -> dict: + """ + :param functions: + :param structs: + :return: + """ + + def evaluate(element): + # if isinstance(element.param_type, (Integer, Float, Boolean, String)): + return {element.name: self.common_names(**{ + 'name': self.title(element.name), + 'origin': element.name, + 'description': self.extract_description(element.description), + 'since': element.since})} + # return OrderedDict() + + render = OrderedDict() + + for func in functions.values(): + for param in func.params.values(): + render.update(evaluate(param)) + + for struct in structs.values(): + render.update(evaluate(struct)) + for param in struct.members.values(): + render.update(evaluate(param)) + + self.custom_mapping_names(render, self.parameter_names) + + return {'params': sorted(render.values(), key=lambda a: a.name)} + + def custom_mapping_names(self, render, file_name): + """ + + :param render: + :param file_name: + :return: + """ + if file_name in self.mapping: + self.logger.debug('applying manual mapping for %s\n%s', file_name, pformat(self.mapping[file_name])) + for name, item in self.mapping[file_name].items(): + if isinstance(item, dict) and name in render: + render[name] = render[name]._replace(**item) + elif isinstance(item, list): + for value in item: + data = OrderedDict().fromkeys(self.common_names._fields) + data.update(value) + render[value['name']] = self.common_names(**data) + elif name in render: + render[name] = render[name]._replace(name=item) + elif isinstance(item, dict): + data = OrderedDict().fromkeys(self.common_names._fields) + data.update(item) + render[name] = self.common_names(**data) + else: + render[name] = self.common_names(item, name, '', '') + self.logger.warning('Into %s added name="%s", origin="%s"', self.function_names, item, name) diff --git a/generator/transformers/generate_error.py b/generator/transformers/generate_error.py new file mode 100644 index 000000000..3fe1a75ed --- /dev/null +++ b/generator/transformers/generate_error.py @@ -0,0 +1,12 @@ +""" +Generate error. +""" + + +class GenerateError(Exception): + """Generate error. + + This exception is raised when generator is unable to create + output from given model. + + """ diff --git a/generator/transformers/structs_producer.py b/generator/transformers/structs_producer.py new file mode 100644 index 000000000..8f48eb8bd --- /dev/null +++ b/generator/transformers/structs_producer.py @@ -0,0 +1,44 @@ +""" +Structs transformer +""" + +import logging +from collections import OrderedDict + +from model.struct import Struct +from transformers.common_producer import InterfaceProducerCommon + + +class StructsProducer(InterfaceProducerCommon): + """ + Structs transformer + """ + + def __init__(self, struct_class, enum_names, mapping=None): + super(StructsProducer, self).__init__( + container_name='members', + names=enum_names, + mapping=mapping.get('structs', {})) + self.struct_class = struct_class + self.logger = logging.getLogger(self.__class__.__name__) + + def transform(self, item: Struct, render=None) -> dict: + """ + + :param item: + :param render: + :return: + """ + name = 'SDL' + item.name + imports = {'.h': {'enum': set(), 'struct': set()}, '.m': set()} + imports['.h']['enum'].add(self.struct_class) + if not render: + render = OrderedDict() + render['origin'] = item.name + render['name'] = name + render['extends_class'] = self.struct_class + render['imports'] = imports + + super(StructsProducer, self).transform(item, render) + + return render |