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..b6ec2a0b7 --- /dev/null +++ b/generator/README.md @@ -0,0 +1,858 @@ +# Proxy Library RPC Generator + +This script provides the ability to auto-generate Objective-C RPC code (header \*.h and implementation \*.m classes) based on the SDL MOBILE_API XML specification. + +## Requirements and Dependencies + +The script requires **Python 3** pre-installed on the host system. The minimal supported Python 3 version is **3.7.6**. It may work on versions back to 3.5 (the minimal version that has not yet reached [the end-of-life](https://devguide.python.org/devcycle/#end-of-life-branches)), but this is not supported and may break in the future. + +Note: To install the dependencies for this script, you must use the **pip3** command. + +All required libraries are listed in `requirements.txt` and should be pre-installed on the system prior to using the sript. Please use the following command to install the libraries: + +```shell script +$ pip3 install -r generator/requirements.txt +``` + +Please also make sure all git submodules are installed and up to date since the script uses the XML parser provided in a submodule. + +```shell script +$ git submodule update --init --recursive +``` + +## Usage + +**Usage example** + +```shell script +$ cd sdl_ios +$ python3 generator/generator.py -xml generator/rpc_spec/MOBILE_API.xml -xsd generator/rpc_spec/MOBILE_API.xsd -d output_dir +``` + + +As a result the output_dir will have all the new generated files. + +**Detailed usage description (keys, options)** + +``` +usage: generator.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 if present, all enums will be generated + -s, --structs if present, all structs will be generated + -m, -f, --functions if present, all functions will be generated + -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 +``` + +### How to use the generated classes + +All RPC classes used in **SmartDeviceLink iOS** library were created manually due to historical reasons and have public API differences from the RPC_SPEC. Therefore, the generated files will differ from the current ones. The generated files are based on the RPC_SPEC and do not contain changes to match the existing files. Therefore, do not replace existing files with generated files. If you want to update existing files with new parameters using the generator, you must generate the file and then use a diff tool to add only the new information and not to change existing information. + +If you are adding new RPCs entirely, you can generate those RPCs. Use the `--skip` switch to only generate new files. You must add those files to Xcode project, SmartDeviceLink.h, and podspec files manually and place them in proper groups sorting the files by their kind. Note: the groups are just virtual folders; they do not map to the file system, so all files go to the SmartDeviceLink folder on the file system. + + +## Objective-C transformation rules + +### Overview +These are the general transformation rules for SDL RPC classes Objective-C Library. For more information about the base classes for these RPCs, you can look in the app library. + +### Output Directory Structure and Package definitions +The script creates corresponding RPC classes of ``, `` and `` elements following the `MOBILE_API.xml` rules. According to existing structure of sdl_ios library the output directory will contain the following files (plain structure, no subfolders). + +RPC requests, responses, structs, enums, and notifications file names all have the form: + +* SDLxxx.h +* SDLxxx.m + +Responses have the form: + +* SDLxxxResponse.h +* SDLxxxResponse.m + +Where the **xxx** is the correspondent item name. + +### The License Header + +All files should begin with the license information. + +```jinja2 +/* + * 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. + */ +``` + +Where `{{year}}` in the copyright line is the current year. + +### General rules for Objective-C classes +1. Default initializer applies only to Functions(Request / Response / Notification) classes + +```objc +- (instancetype)init { + self = [super initWithName:SDLRPCFunctionNameRegisterAppInterface]; + if (!self) { return nil; } + + return self; +} +``` + +2. Initializer for mandatory params if there is/are any in XML (skipped if no params) +3. Initializer for all params if there is/are any which is not mandatory in XML (skipped if no params) + +#### Scalars +There are 4 type of scalar values declared in the SDL lib. These are: +1. **SDLInt** - A declaration that this NSNumber contains an NSInteger. +0. **SDLUInt** - A declaration that this NSNumber contains an NSUInteger. +0. **SDLBool** - A declaration that this NSNumber contains a BOOL. +0. **SDLFloat** - A declaration that this NSNumber contains a float. + +*Note: These are syntactic sugar to help the developer know what type of value is held in the `NSNumber`.* + +Usage example: +```objc +@property (strong, nonatomic) NSNumber *touchEventId; +``` + +or in an array: +```objc +@property (strong, nonatomic) NSArray *> *timeStamp; +``` + +#### Enums +RPC Enums in SDL are strings. sdl_ios uses `NSString` `typedef`ed with a proper enum type. In Swift projects, however, they become real enums by using the `NS_SWIFT_ENUM` compiler tag. + +Base definition of `SDLEnum`: + +```objc +typedef NSString* SDLEnum SDL_SWIFT_ENUM; + +*Note: This new defined type has already adds a pointer, so anything that inherits from `SDLEnum` needs no asterisk.* + +```objc +typedef SDLEnum SDLTouchType SDL_SWIFT_ENUM; // SDLTouchType will be considered an NSString by the compiler in Obj-C, but will be an enum object of type SDLTouchType in Swift. +``` + +And here is a concrete 'enum' item: + +```objc +extern SDLTouchType const SDLTouchTypeBegin; +``` + +If an item is deprecated then it will be declared as such: + +```objc +__deprecated +extern SDLTouchType const SDLTouchTypeBegin; +``` + +Take, for instance, the enum class `KeypressMode`: + +```xml + + Enumeration listing possible keyboard events. + + Each keypress is individually sent as the user presses the keyboard keys. + + + +``` + +In the following example, we would define in the header: + +```objc +extern SDLKeypressMode const SDLKeypressModeSingleKeypress; +``` + +and `SDLKeypressModeSingleKeypress` itself must be implemented in the correspondent `SDLKeypressMode.m ` file like so: + +```objc +SDLKeypressMode const SDLKeypressModeSingleKeypress = @"SINGLE_KEYPRESS"; +``` + +#### Structs + +Structures in sdl_ios are implemented as classes derived from the parent class SDLRPCStruct with all parameters implemented as `@property`. Let us take for an instance the `DeviceInfo` structure. In the XML it is declared as following: + +```xml + + Various information about connecting device. + + Device model + + + Device firmware revision + + + Device OS + + + Device OS version + + + Device mobile carrier (if applicable) + + + Omitted if connected not via BT. + + + ``` + +*Note: the file begins with the `NS_ASSUME_NONNULL_BEGIN` macro, which makes all properties / parameters mandatory. If a parameter is not mandatory, then the modifier `nullable` must be used* + +```objc +// SDLDeviceInfo.h + +#import "SDLRPCStruct.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Various information about connecting device. + * + * @since SDL 3.0.0 + */ +@interface SDLDeviceInfo : SDLRPCStruct + +/** + * @param hardware - hardware + * @param firmwareRev - firmwareRev + * @param os - os + * @param osVersion - osVersion + * @param carrier - carrier + * @param maxNumberRFCOMMPorts - @(maxNumberRFCOMMPorts) + * @return A SDLDeviceInfo object + */ +- (instancetype)initWithHardware:(nullable NSString *)hardware firmwareRev:(nullable NSString *)firmwareRev os:(nullable NSString *)os osVersion:(nullable NSString *)osVersion carrier:(nullable NSString *)carrier maxNumberRFCOMMPorts:(UInt8)maxNumberRFCOMMPorts; + +/** + * Device model + * {"default_value": null, "max_length": 500, "min_length": 0} + */ +@property (nullable, strong, nonatomic) NSString *hardware; + +/** + * Device firmware revision + * {"default_value": null, "max_length": 500, "min_length": 0} + */ +@property (nullable, strong, nonatomic) NSString *firmwareRev; + +/** + * Device OS + * {"default_value": null, "max_length": 500, "min_length": 0} + */ +@property (nullable, strong, nonatomic) NSString *os; + +/** + * Device OS version + * {"default_value": null, "max_length": 500, "min_length": 0} + */ +@property (nullable, strong, nonatomic) NSString *osVersion; + +/** + * Device mobile carrier (if applicable) + * {"default_value": null, "max_length": 500, "min_length": 0} + */ +@property (nullable, strong, nonatomic) NSString *carrier; + +/** + * Omitted if connected not via BT. + * {"default_value": null, "max_value": 100, "min_value": 0} + */ +@property (nullable, strong, nonatomic) NSNumber *maxNumberRFCOMMPorts; + +@end + +NS_ASSUME_NONNULL_END +``` + +The implementation **SDLDeviceInfo.m** file: + +```objc +// SDLDeviceInfo.m + +#import "SDLDeviceInfo.h" +#import "NSMutableDictionary+Store.h" +#import "SDLRPCParameterNames.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SDLDeviceInfo + +- (instancetype)initWithHardware:(nullable NSString *)hardware firmwareRev:(nullable NSString *)firmwareRev os:(nullable NSString *)os osVersion:(nullable NSString *)osVersion carrier:(nullable NSString *)carrier maxNumberRFCOMMPorts:(UInt8)maxNumberRFCOMMPorts { + self = [super init]; + if (!self) { + return nil; + } + self.hardware = hardware; + self.firmwareRev = firmwareRev; + self.os = os; + self.osVersion = osVersion; + self.carrier = carrier; + self.maxNumberRFCOMMPorts = @(maxNumberRFCOMMPorts); + return self; +} + +- (void)setHardware:(nullable NSString *)hardware { + [self.store sdl_setObject:hardware forName:SDLRPCParameterNameHardware]; +} + +- (nullable NSString *)hardware { + return [self.store sdl_objectForName:SDLRPCParameterNameHardware ofClass:NSString.class error:nil]; +} + +- (void)setFirmwareRev:(nullable NSString *)firmwareRev { + [self.store sdl_setObject:firmwareRev forName:SDLRPCParameterNameFirmwareRev]; +} + +- (nullable NSString *)firmwareRev { + return [self.store sdl_objectForName:SDLRPCParameterNameFirmwareRev ofClass:NSString.class error:nil]; +} + +- (void)setOs:(nullable NSString *)os { + [self.store sdl_setObject:os forName:SDLRPCParameterNameOs]; +} + +- (nullable NSString *)os { + return [self.store sdl_objectForName:SDLRPCParameterNameOs ofClass:NSString.class error:nil]; +} + +- (void)setOsVersion:(nullable NSString *)osVersion { + [self.store sdl_setObject:osVersion forName:SDLRPCParameterNameOsVersion]; +} + +- (nullable NSString *)osVersion { + return [self.store sdl_objectForName:SDLRPCParameterNameOsVersion ofClass:NSString.class error:nil]; +} + +- (void)setCarrier:(nullable NSString *)carrier { + [self.store sdl_setObject:carrier forName:SDLRPCParameterNameCarrier]; +} + +- (nullable NSString *)carrier { + return [self.store sdl_objectForName:SDLRPCParameterNameCarrier ofClass:NSString.class error:nil]; +} + +- (void)setMaxNumberRFCOMMPorts:(nullable NSNumber *)maxNumberRFCOMMPorts { + [self.store sdl_setObject:maxNumberRFCOMMPorts forName:SDLRPCParameterNameMaxNumberRFCOMMPorts]; +} + +- (nullable NSNumber *)maxNumberRFCOMMPorts { + return [self.store sdl_objectForName:SDLRPCParameterNameMaxNumberRFCOMMPorts ofClass:NSNumber.class error:nil]; +} + +@end + +NS_ASSUME_NONNULL_END +``` + +#### Functions + +Functions in iOS are implemented as 3 different classes (`SDLRPCRequest`, `SDLRPCResponse`, and `SDLRPCNotification`) grouped by their respective type. All the 3 extend the common parent class `SDLRPCMessage`. + + +##### Function ID, Function Name, and Parameter Name Special Case Class +There is also the `SDLFunctionID` class generated though it is not declared in the XML. This class maps all function IDs that are integers to function names as strings. + +1. Uses of the `"name"` attribute should be normalized by the removal of the ID suffix, e.g. `RegisterAppInterfaceID -> RegisterAppInterface`. +2. The constant name should be camel case formatted. +3. The constant has 2 fields the first is the `int` value of the `"value"` attribute and the second is the `String` value of normalized `"name"` attribute. + +Internally it uses another file that lists all the function names `SDLRPCFunctionNames`. + +```objc +// SDLFunctionID.h + +#import +#import "NSNumber+NumberType.h" +#import "SDLRPCFunctionNames.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A function ID for an SDL RPC +@interface SDLFunctionID : 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 *)functionIdForName:(SDLRPCFunctionName)functionName; + +@end + +NS_ASSUME_NONNULL_END +``` + +Each from MOBILE_API.XML declares its function name in `SDLRPCFunctionNames.h` and `SDLRPCFunctionNames.m` files. + +```objc +SDLRPCFunctionNames.h +#import "SDLEnum.h" +/** +* All RPC request / response / notification names +*/ +typedef SDLEnum SDLRPCFunctionName SDL_SWIFT_ENUM; + +/// Function name for an AddCommand RPC +extern SDLRPCFunctionName const SDLRPCFunctionNameAddCommand; + +/// Function name for an AddSubMenu RPC +extern SDLRPCFunctionName const SDLRPCFunctionNameAddSubMenu; + +. . . and so on +``` + +And the implementation file SDLRPCFunctionNames.m : + +```objc +// +// SDLRPCFunctionNames.m +// SmartDeviceLink +// + +#import "SDLRPCFunctionNames.h" + +SDLRPCFunctionName const SDLRPCFunctionNameAddCommand = @"AddCommand"; +SDLRPCFunctionName const SDLRPCFunctionNameAddSubMenu = @"AddSubMenu"; + +. . . and so on +``` + +Each from MOBILE_API.XML declares its parameter name in `SDLRPCParameterNames.h` and `SDLRPCParameterNames.m` files. + +```objc +// SDLRPCParameterNames.h + +#import +#import "SDLMacros.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NSString* SDLRPCParameterName SDL_SWIFT_ENUM; + +extern SDLRPCParameterName const SDLRPCParameterNameAcEnable; +extern SDLRPCParameterName const SDLRPCParameterNameAcEnableAvailable; + +. . . and so on +``` + +And the implementation file SDLRPCParameterNames.m : + +```objc +// SDLRPCParameterNames.h + +#import "NSMutableDictionary+Store.h" +#import "SDLRPCParameterNames.h" + +NS_ASSUME_NONNULL_BEGIN + +SDLRPCParameterName const SDLRPCParameterNameAcEnable = @"acEnable"; +SDLRPCParameterName const SDLRPCParameterNameAcEnableAvailable = @"acEnableAvailable"; + +. . . and so on +``` + +##### Request Functions (SDLRPCRequest) + + +```xml + + + RPC used to get the current properties of a cloud application + + + +``` + + +```objc +// SDLGetCloudAppProperties.h +// + +#import "SDLRPCRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * RPC used to get the current properties of a cloud application + * + * @since SDL 5.1.0 + */ +@interface SDLGetCloudAppProperties : SDLRPCRequest + +/** + * @param appID + * @return A SDLGetCloudAppProperties object + */ +- (instancetype)initWithAppID:(NSString *)appID; + +/** + * {"default_value": null, "max_length": 100, "min_length": 1} + * + * Required, NSString * + */ +@property (strong, nonatomic) NSString *appID; + +@end + +NS_ASSUME_NONNULL_END +``` + +```objc +// SDLGetCloudAppProperties.m +// + +#import "SDLGetCloudAppProperties.h" +#import "NSMutableDictionary+Store.h" +#import "SDLRPCFunctionNames.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SDLGetCloudAppProperties + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (instancetype)init { + self = [super initWithName:SDLRPCFunctionNameGetCloudAppProperties]; + if (!self) { return nil; } + + return self; +} +#pragma clang diagnostic pop + +- (void)setAppID:(NSString *)appID { + [self.parameters sdl_setObject:appID forName:SDLRPCParameterNameAppId]; +} + +- (NSString *)appID { + NSError *error = nil; + return [self.parameters sdl_objectForName:SDLRPCParameterNameAppId ofClass:NSString.class error:&error]; +} + +@end + +NS_ASSUME_NONNULL_END +``` + +##### Response Functions (SDLRPCResponse) + + +```xml + + + true if successful; false, if failed + + + + See Result + + + + + + + + + + + + + + + + Provides additional human readable info regarding the result. + + + + + Amount of time (in seconds) that an app must wait before resending an alert. + If provided, another system event or overlay currently has a higher priority than this alert. + An app must not send an alert without waiting at least the amount of time dictated. + + + +``` + + +```objc +// SDLAlertResponse.h +// + +#import "SDLRPCResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * @since SDL 1.0.0 + */ +@interface SDLAlertResponse : SDLRPCResponse + +/** + * @param @(tryAgainTime) + * @return A SDLAlertResponse object + */ +- (instancetype)initWithTryAgainTime:(UInt32)tryAgainTime; + +/** + * Amount of time (in seconds) that an app must wait before resending an alert. If provided, another system event or + * overlay currently has a higher priority than this alert. An app must not send an alert without waiting at least + * the amount of time dictated. + * {"default_value": null, "max_value": 2000000000, "min_value": 0} + * + * @since SDL 2.0.0 + * + * Optional, UInt32 + */ +@property (nullable, strong, nonatomic) NSNumber *tryAgainTime; + +@end + +NS_ASSUME_NONNULL_END +``` + +```objc +// SDLAlertResponse.m +// + + +#import "SDLAlertResponse.h" +#import "NSMutableDictionary+Store.h" +#import "SDLRPCFunctionNames.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SDLAlertResponse + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (instancetype)init { + self = [super initWithName:SDLRPCFunctionNameShowAppMenu]; + if (!self) { return nil; } + + return self; +} +#pragma clang diagnostic pop + +- (void)setTryAgainTime:(nullable NSNumber *)tryAgainTime { + [self.parameters sdl_setObject:tryAgainTime forName:SDLRPCParameterNameTryAgainTime]; +} + +- (nullable NSNumber *)tryAgainTime { + return [self.parameters sdl_objectForName:SDLRPCParameterNameTryAgainTime ofClass:NSNumber.class error:nil]; +} + +@end + +NS_ASSUME_NONNULL_END +``` + +##### Notification Functions (SDLRPCNotification) + + +```xml + + + See AppInterfaceUnregisteredReason + + +``` + +```objc +// SDLOnAppInterfaceUnregistered.h + +#import "SDLRPCNotification.h" + +@class SDLAppInterfaceUnregisteredReason; + +NS_ASSUME_NONNULL_BEGIN + +/** + * @since SDL 1.0.0 + */ +@interface SDLOnAppInterfaceUnregistered : SDLRPCNotification + +/** + * @param reason - @(reason) + * @return A SDLOnAppInterfaceUnregistered object + */ +- (instancetype)initWithReason:(SDLAppInterfaceUnregisteredReason)reason; + +/** + * See AppInterfaceUnregisteredReason + * + * Required, SDLAppInterfaceUnregisteredReason + */ +@property (strong, nonatomic) SDLAppInterfaceUnregisteredReason reason; + +@end + +NS_ASSUME_NONNULL_END +``` + +```objc +// SDLOnAppInterfaceUnregistered.m + +#import "SDLOnAppInterfaceUnregistered.h" +#import "NSMutableDictionary+Store.h" +#import "SDLAppInterfaceUnregisteredReason.h" +#import "SDLRPCParameterNames.h" +#import "SDLRPCFunctionNames.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SDLOnAppInterfaceUnregistered + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (instancetype)init { + self = [super initWithName:SDLRPCFunctionNameOnAppInterfaceUnregistered]; + if (!self) { return nil; } + + return self; +} +#pragma clang diagnostic pop + +- (instancetype)initWithReason:(SDLAppInterfaceUnregisteredReason)reason { + self = [super init]; + if (!self) { + return nil; + } + self.reason = @(reason); + return self; +} + +- (void)setReason:(SDLAppInterfaceUnregisteredReason)reason { + [self.parameters sdl_setObject:reason forName:SDLRPCParameterNameReason]; +} + +- (SDLAppInterfaceUnregisteredReason)reason { + NSError *error = nil; + return [self.parameters sdl_enumForName:SDLRPCParameterNameReason error:&error]; +} + +@end + +NS_ASSUME_NONNULL_END +``` + +## Unit tests of Generator + +After you made any changes in python RPC generator to avoid affecting code logic you should run Unit tests as follow: + +```shell script +$ cd sdl_ios +$ python3 generator/test/runner.py +``` + +In case of successful execution of all Unit tests in output you will see the results as follow: + +```shell script +Ran 12 tests in 0.464s + +OK +``` + +As well you can check coverage by Unit tests of python RPC generator as follow: + +```shell script +coverage run generator/test/runner.py +coverage html +``` + +after the you can check the report in `htmlcov/index.html` + + +## Other Utilities + +### Generator + +Proxy Library RPC Generator inherits the license defined in the root folder of this project. + +#### Third Party Licenses + +Both the source and binary distributions of this software contain +some third party software. All the third party software included +or linked is redistributed under the terms and conditions of their +original licenses. + +The third party software included and used by this project is: + +**xmlschema** + +* Licensed under MIT License +* See [https://pypi.org/project/xmlschema/](https://pypi.org/project/xmlschema/) + +**Jinja2** + +* Licensed under BSD License (BSD-3-Clause) +* See [https://pypi.org/project/Jinja2/](https://pypi.org/project/Jinja2/) + +**coverage** + +* Licensed under Apache Software License (Apache 2.0) +* See [https://pypi.org/project/coverage/](https://pypi.org/project/coverage/) + +**pathlib2** + +* Licensed under MIT License +* See [https://pypi.org/project/pathlib2/](https://pypi.org/project/pathlib2/) + +**flake8** + +* Licensed under MIT License +* See [https://pypi.org/project/flake8/](https://pypi.org/project/flake8/) diff --git a/generator/__init__.py b/generator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/generator/generator.py b/generator/generator.py new file mode 100644 index 000000000..2dda4129f --- /dev/null +++ b/generator/generator.py @@ -0,0 +1,489 @@ +""" +Generator +""" +import asyncio +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 os.path import basename, join +from pathlib import Path +from re import findall + +from jinja2 import UndefinedError, TemplateNotFound, FileSystemLoader, Environment, ChoiceLoader, \ + TemplateAssertionError, TemplateSyntaxError, TemplateRuntimeError + +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) + 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') + + _version = '1.0.0' + + @property + def get_version(self) -> str: + """ + version of the entire generator + :return: current entire generator version + """ + return self._version + + @property + def output_directory(self) -> Path: + """ + Getter for output directory + :return: output directory Path + """ + return self._output_directory + + @output_directory.setter + def output_directory(self, output_directory: str): + """ + Setting and validating output directory + :param output_directory: path to output directory + :return: None + """ + if output_directory.startswith('/'): + path = Path(output_directory).absolute().resolve() + else: + path = Path('.').absolute().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: + """ + Getter for Jinja2 instance + :return: initialized Jinja2 instance + """ + return self._env + + @env.setter + def env(self, paths: list): + """ + Initiating Jinja2 instance + :param paths: list with paths to all Jinja2 templates + :return: None + """ + 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: str): + """ + Capitalizing only first character in string. Using for appropriate filter in Jinja2 + :param name: string to be capitalized first character + :return: initial parameter with capitalized first character + """ + return Common.title(name) + + def config_logging(self, verbose): + """ + Configuring logging for all application + :param verbose: if True setting logging.DEBUG else logging.ERROR + :return: None + """ + 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 and evaluating command-line arguments + :return: object with parsed and validated CLI arguments + """ + 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='if present, all enums will be generated') + parser.add_argument('-s', '--structs', required=False, action='store_true', + help='if present, all structs will be generated') + parser.add_argument('-m', '-f', '--functions', required=False, action='store_true', + help='if present, all functions will be generated') + 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/n]:\t' + .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) + + def get_file_content(self, file_name: Path) -> list: + """ + + :param file_name: + :return: + """ + try: + with file_name.open('r') as file: + content = file.readlines() + return content + except FileNotFoundError as message1: + self.logger.error(message1) + return [] + + def get_key_words(self, file_name=ROOT.joinpath('rpc_spec/RpcParser/RESERVED_KEYWORDS')): + """ + :param file_name: + :return: + """ + content = self.get_file_content(file_name) + content = tuple(map(lambda e: re.sub(r'\n', r'', e).strip().casefold(), content)) + content = tuple(filter(lambda e: not re.search(r'^#+\s+.+|^$', e), content)) + return content + + def get_paths(self, file_name: Path = ROOT.joinpath('paths.ini')): + """ + Getting and validating parent classes names + :param file_name: path to file with container + :return: namedtuple with container to key elements + """ + content = self.get_file_content(file_name) + if not content: + self.logger.critical('%s not found', file_name) + sys.exit(1) + data = OrderedDict() + + for line in content: + 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() + + missed = list(set(self.paths_named._fields) - set(data.keys())) + if missed: + self.logger.critical('in %s missed fields: %s ', content, str(missed)) + sys.exit(1) + + return self.paths_named(**data) + + def write_file(self, file: Path, templates: list, data: dict): + """ + 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 file name + :param templates: list with templates + :param data: Dictionary with prepared output data, ready to be applied to Jinja2 template + """ + 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: bool, overwrite: bool, items: dict, 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(): + if item.name == 'FunctionID': + self.logger.warning('%s will be skipped', item.name) + continue + 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{}.jinja2'.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: bool, overwrite: bool, functions: dict, structs: dict, + transformer: FunctionsProducer): + """ + Processing output for SDLRPCFunctionNames and SDLRPCParameterNames + :param skip: if target file exist it will be skipped + :param overwrite: if target file exist it will be overwritten + :param functions: Dictionary with all functions + :param structs: Dictionary with all structs + :param transformer: FunctionsProducer (transformer) instance + :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 = ['{}{}.jinja2'.format(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: bool, overwrite: bool, file_with_suffix: Path, data: dict, templates: list): + """ + Processing output common + :param skip: if target file exist it will be skipped + :param overwrite: if target file exist it will be overwritten + :param file_with_suffix: output file name + :param data: Dictionary with prepared output data, ready to be applied to Jinja2 template + :param templates: list with paths to Jinja2 templates + :return: None + """ + 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) + file_with_suffix.unlink() + self.write_file(file_with_suffix, templates, data) + else: + while True: + try: + confirm = input('File already exists {}. Overwrite? [Y/n]:\t' + .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): + """ + Filtering Model to match with regex pattern + :param interface: Initial (original) Model, obtained from module 'rpc_spec/InterfaceParser' + :param pattern: regex pattern (string) + :return: Model with items which match with regex pattern + """ + enum_names = tuple(interface.enums.keys()) + struct_names = 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), enum_names, struct_names + return interface, enum_names, struct_names + + async def parser(self, source_xml, source_xsd): + """ + Getting Model from source_xml, parsed and validated by module 'rpc_spec/InterfaceParser' + :param source_xml: path to xml file + :param source_xsd: path to xsd file + :return: Model, obtained from module 'rpc_spec/InterfaceParser' + """ + 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) + + 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 = self.loop.run_until_complete(self.parser(args.source_xml, args.source_xsd)) + paths = self.get_paths() + + self.env = [args.templates_directory] + [join(args.templates_directory, k) for k in vars(interface).keys()] + + filtered, enum_names, struct_names = self.filter_pattern(interface, args.regex_pattern) + + tasks = [] + key_words = self.get_key_words() + + functions_transformer = FunctionsProducer(paths, enum_names, struct_names, key_words) + if args.enums and filtered.enums: + tasks.append(self.process_main(args.skip, args.overwrite, filtered.enums, + EnumsProducer(paths.enum_class, key_words))) + if args.structs and filtered.structs: + tasks.append(self.process_main(args.skip, args.overwrite, filtered.structs, + StructsProducer(paths.struct_class, enum_names, struct_names, key_words))) + 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/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..c59028039 --- /dev/null +++ b/generator/requirements.txt @@ -0,0 +1,5 @@ +xmlschema +Jinja2 +coverage +pathlib2 +flake8 \ No newline at end of file diff --git a/generator/rpc_spec b/generator/rpc_spec new file mode 160000 index 000000000..c5132e001 --- /dev/null +++ b/generator/rpc_spec @@ -0,0 +1 @@ +Subproject commit c5132e0016cf34b30fec2a2687c59cbfd44c543a diff --git a/generator/templates/SDLRPCFunctionNames.h.jinja2 b/generator/templates/SDLRPCFunctionNames.h.jinja2 new file mode 100644 index 000000000..5e13b735b --- /dev/null +++ b/generator/templates/SDLRPCFunctionNames.h.jinja2 @@ -0,0 +1,28 @@ +{% include 'copyright.jinja2' %} +// SDLRPCFunctionNames.h + +#import "SDLEnum.h" + +/** + * All RPC request / response / notification names + */ +typedef SDLEnum SDLRPCFunctionName SDL_SWIFT_ENUM; +{% for param in params %} +{#- description if exist in source xml, will be putted here + since if exist in source xml, will be putted here -#} +{%- 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 -%} diff --git a/generator/templates/SDLRPCFunctionNames.m.jinja2 b/generator/templates/SDLRPCFunctionNames.m.jinja2 new file mode 100644 index 000000000..c7dc5a374 --- /dev/null +++ b/generator/templates/SDLRPCFunctionNames.m.jinja2 @@ -0,0 +1,7 @@ +{% include 'copyright.jinja2' %} +// SDLRPCFunctionNames.m + +#import "SDLRPCFunctionNames.h" +{% for param in params %} +SDLRPCFunctionName const SDLRPCFunctionName{{ param.name }} = @"{{ param.origin }}"; +{%- endfor %} diff --git a/generator/templates/SDLRPCParameterNames.h.jinja2 b/generator/templates/SDLRPCParameterNames.h.jinja2 new file mode 100644 index 000000000..7a5bbb746 --- /dev/null +++ b/generator/templates/SDLRPCParameterNames.h.jinja2 @@ -0,0 +1,14 @@ +{% include 'copyright.jinja2' %} +// SDLRPCParameterNames.h + +#import +#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.jinja2 b/generator/templates/SDLRPCParameterNames.m.jinja2 new file mode 100644 index 000000000..b29a1705c --- /dev/null +++ b/generator/templates/SDLRPCParameterNames.m.jinja2 @@ -0,0 +1,12 @@ +{% include 'copyright.jinja2' %} +// 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.jinja2 b/generator/templates/base_struct_function.h.jinja2 new file mode 100644 index 000000000..cd1c23d6b --- /dev/null +++ b/generator/templates/base_struct_function.h.jinja2 @@ -0,0 +1,57 @@ +{#- To avoid code duplication was crated this parent file, which contain common part used in: + "templates/functions/template.h" and "templates/structs/template.h". -#} +{% include 'copyright.jinja2' %} +{% block imports %} +{%- for import in imports.enum %} +#import "{{import}}.h" +{%- endfor %} +{%- if imports.struct %} +{% endif -%} +{%- for import in imports.struct %} +@class {{import}}; +{%- endfor %} +{%- endblock %} + +NS_ASSUME_NONNULL_BEGIN +{% include 'description.jinja2' %} +@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 deprecated or c.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +- (instancetype)initWith{{c.init}}; +{%- if deprecated or c.deprecated %} +#pragma clang diagnostic pop +{%- endif %} +{% endfor -%} +{%- endblock -%} +{%- block methods %} +{%- for param in params %} +{%- include 'description_param.jinja2' %} +{%- if deprecated or param.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +@property ({{'nullable, ' if not param.mandatory}}{{param.modifier}}, nonatomic) {{param.type_sdl}}{{param.origin}}{{' __deprecated' if param.deprecated and param.deprecated is sameas true }}; +{%- if deprecated or param.deprecated %} +#pragma clang diagnostic pop +{%- endif %} +{% endfor %} +{%- endblock %} +@end + +NS_ASSUME_NONNULL_END diff --git a/generator/templates/base_struct_function.m.jinja2 b/generator/templates/base_struct_function.m.jinja2 new file mode 100644 index 000000000..30bb2e976 --- /dev/null +++ b/generator/templates/base_struct_function.m.jinja2 @@ -0,0 +1,71 @@ +{#- To avoid code duplication was crated this parent file, which contain common part used in: + "templates/functions/template.m" and "templates/structs/template.m". -#} +{% include 'copyright.jinja2' %} +{%- block imports %} +#import "{{name}}.h" +#import "NSMutableDictionary+Store.h" +{%- for import in imports %} +#import "{{import}}.h" +{%- endfor %} +{%- endblock %} + +NS_ASSUME_NONNULL_BEGIN +{% if deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +{%- endif %} +@implementation {{name}} +{%- if deprecated %} +#pragma clang diagnostic pop +{%- endif %} +{% block constructors %} +{%- for c in constructors %} +{%- if deprecated or c.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +- (instancetype)initWith{{c.init}} { + self = [{{ 'self' if c.self else 'super' }} init{{ 'With' + c.self if c.self and c.self is string }}]; + if (!self) { + return nil; + } + {%- for a in c.arguments %} + self.{{a.origin}} = {{a.constructor_argument}}; + {%- endfor %} + return self; +} +{%- if deprecated or c.deprecated %} +#pragma clang diagnostic pop +{%- endif %} +{% endfor -%} +{% endblock -%} +{%- block methods %} +{%- for param in params %} +{%- if deprecated or param.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +- (void)set{{param.origin|title}}:({{'nullable ' if not param.mandatory}}{{param.type_generic}}{{param.type_sdl|trim}}){{param.origin}} { +{%- if deprecated or param.deprecated %} +#pragma clang diagnostic pop +{%- endif %} + [self.{{parameters_store}} sdl_setObject:{{param.origin}} forName:SDLRPCParameterName{{param.method_suffix}}]; +} +{% if deprecated or param.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +- ({{'nullable ' if not param.mandatory}}{{param.type_generic}}{{param.type_sdl|trim}}){{param.origin}} { +{%- if deprecated or param.deprecated %} +#pragma clang diagnostic pop +{%- endif %} + {% if param.mandatory -%} + NSError *error = nil; + {% endif -%} + return [self.{{parameters_store}} sdl_{{param.for_name}}ForName:SDLRPCParameterName{{param.method_suffix}}{{' ofClass:'+param.of_class if param.of_class}} error:{{'&error' if param.mandatory else 'nil'}}]; +} +{% endfor %} +{%- endblock %} +@end + +NS_ASSUME_NONNULL_END diff --git a/generator/templates/copyright.jinja2 b/generator/templates/copyright.jinja2 new file mode 100644 index 000000000..a037d487d --- /dev/null +++ b/generator/templates/copyright.jinja2 @@ -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/description.jinja2 b/generator/templates/description.jinja2 new file mode 100644 index 000000000..fb4932270 --- /dev/null +++ b/generator/templates/description.jinja2 @@ -0,0 +1,24 @@ +{#- Content of this file include into every item (Enum/Struct/Function) between imports and typedef/@interface declaration -#} +{% if description or since or history %} +/** + {%- if description %} + {%- for d in description %} + * {{d}} + {%- endfor %}{% endif -%} + {%- if description and ( since or history ) %} + * + {%- endif %} + {%- if deprecated %} + * @deprecated + {%- endif %} + {%- if history %} + * @history SDL {{ history }} + {%- endif %} + {%- if since %} + * @since SDL {{ since }} + {%- endif %} + */ +{%- endif -%} +{%- if deprecated %} +__deprecated +{%- endif -%} diff --git a/generator/templates/description_param.jinja2 b/generator/templates/description_param.jinja2 new file mode 100644 index 000000000..cb0cc4244 --- /dev/null +++ b/generator/templates/description_param.jinja2 @@ -0,0 +1,21 @@ +{#- Content of this file include into every Param/sub-element of (Enum/Struct/Function) -#} +{% if param.description or param.since or param.history %} +/** + {%- if param.description %} + {%- for d in param.description %} + * {{d}} + {%- endfor %}{% endif -%} + {%- if param.description and ( param.since or param.history ) %} + * + {%- endif %} + {%- if param.deprecated %} + * @deprecated + {%- endif %} + {%- if param.history %} + * @history SDL {{ param.history }} + {%- endif %} + {%- if param.since %} + * @since SDL {{ param.since }} + {%- endif %} + */ +{%- endif %} diff --git a/generator/templates/enums/template.h.jinja2 b/generator/templates/enums/template.h.jinja2 new file mode 100644 index 000000000..edba9f0bf --- /dev/null +++ b/generator/templates/enums/template.h.jinja2 @@ -0,0 +1,28 @@ +{#- String based enum -#} +{% include 'copyright.jinja2' %} +{% block imports -%} +{%- for import in imports %} +#import "{{import}}.h" +{%- endfor %} +{%- endblock -%} +{%- block body %} +{% include 'description.jinja2' %} +typedef SDLEnum {{ name }} SDL_SWIFT_ENUM{{ending}}; +{% if deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{% endif %} +{%- for param in params %} +{%- include 'description_param.jinja2' %}{% if param.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +extern {{ name }} const {{ name }}{{param.name}}{{ " __deprecated" if param.deprecated and param.deprecated }}; +{% if param.deprecated -%} +#pragma clang diagnostic pop +{%- endif -%} +{% endfor -%} +{%- if deprecated %} +#pragma clang diagnostic pop +{%- endif %} +{% endblock -%} diff --git a/generator/templates/enums/template.m.jinja2 b/generator/templates/enums/template.m.jinja2 new file mode 100644 index 000000000..0e8ca0b2b --- /dev/null +++ b/generator/templates/enums/template.m.jinja2 @@ -0,0 +1,25 @@ +{% include 'copyright.jinja2' %} + +#import "{{name}}.h" +{%- block body %} +{% if add_typedef %} +typedef SDLEnum {{name}} SDL_SWIFT_ENUM; +{% endif -%} +{%- if deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +{%- for param in params %} +{%- if param.deprecated %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +{%- endif %} +{{ name }} const {{ name }}{{param.name}} = @"{{param.origin}}"; +{%- if param.deprecated %} +#pragma clang diagnostic pop +{% endif %} +{%- endfor -%} +{%- if deprecated %} +#pragma clang diagnostic pop +{%- endif %} +{% endblock -%} diff --git a/generator/templates/functions/template.h.jinja2 b/generator/templates/functions/template.h.jinja2 new file mode 100644 index 000000000..28e3fd0be --- /dev/null +++ b/generator/templates/functions/template.h.jinja2 @@ -0,0 +1,2 @@ +{#- This template creates RPC requests, responses, and notification .h files -#} +{% extends "base_struct_function.h.jinja2" %} \ No newline at end of file diff --git a/generator/templates/functions/template.m.jinja2 b/generator/templates/functions/template.m.jinja2 new file mode 100644 index 000000000..9809b8df2 --- /dev/null +++ b/generator/templates/functions/template.m.jinja2 @@ -0,0 +1,21 @@ +{#- This template creates RPC requests, responses, and notification .m files -#} +{% extends "base_struct_function.m.jinja2" %} +{% block imports %} +{{super()}} +#import "SDLRPCFunctionNames.h" +#import "SDLRPCParameterNames.h" +{%- endblock %} +{% block constructors %} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (instancetype)init { + self = [super initWithName:SDLRPCFunctionName{{origin}}]; + if (!self) { + return nil; + } + return self; +} +#pragma clang diagnostic pop +{{super()}} +{%- endblock -%} +{% set parameters_store = 'parameters' %} diff --git a/generator/templates/structs/template.h.jinja2 b/generator/templates/structs/template.h.jinja2 new file mode 100644 index 000000000..f57d273ca --- /dev/null +++ b/generator/templates/structs/template.h.jinja2 @@ -0,0 +1,2 @@ +{#- This template creates RPC struct .h files -#} +{% extends "base_struct_function.h.jinja2" %} \ No newline at end of file diff --git a/generator/templates/structs/template.m.jinja2 b/generator/templates/structs/template.m.jinja2 new file mode 100644 index 000000000..488b076af --- /dev/null +++ b/generator/templates/structs/template.m.jinja2 @@ -0,0 +1,7 @@ +{#- This template creates RPC struct .m files -#} +{% extends "base_struct_function.m.jinja2" %} +{% 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 diff --git a/generator/test/runner.py b/generator/test/runner.py new file mode 100644 index 000000000..2cb413227 --- /dev/null +++ b/generator/test/runner.py @@ -0,0 +1,67 @@ +""" +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 CodeFormatAndQuality +except ImportError as error: + print('{}.\nProbably you did not initialize submodule'.format(error)) + sys.exit(1) + + +def config_logging(): + """ + Configuring logging for all application + """ + 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.INFO) + root_logger.setLevel(logging.INFO) + root_logger.addHandler(handler) + + +def main(): + """ + Without performing Tests (simple instances initialization) there are following initial test code coverage: + generator/transformers/common_producer.py 21% + generator/transformers/enums_producer.py 24% + generator/transformers/functions_producer.py 18% + generator/transformers/structs_producer.py 32% + + After performing Tests there are following initial test code coverage: + generator/transformers/common_producer.py 100% + generator/transformers/enums_producer.py 100% + generator/transformers/functions_producer.py 100% + generator/transformers/structs_producer.py 100% + """ + config_logging() + suite = TestSuite() + + suite.addTests(TestLoader().loadTestsFromTestCase(TestFunctionsProducer)) + suite.addTests(TestLoader().loadTestsFromTestCase(TestStructsProducer)) + suite.addTests(TestLoader().loadTestsFromTestCase(TestEnumsProducer)) + suite.addTests(TestLoader().loadTestsFromTestCase(CodeFormatAndQuality)) + + runner = TextTestRunner(verbosity=2) + runner.run(suite) + + +if __name__ == '__main__': + """ + Entry point for parser and generator. + """ + main() diff --git a/generator/test/test_CodeFormatAndQuality.py b/generator/test/test_CodeFormatAndQuality.py new file mode 100755 index 000000000..c9b34e06f --- /dev/null +++ b/generator/test/test_CodeFormatAndQuality.py @@ -0,0 +1,32 @@ +""" +Interface model unit test +""" +import unittest +from os import walk +from os.path import join +from pathlib import Path + +from flake8.api import legacy as flake8 + + +class CodeFormatAndQuality(unittest.TestCase): + def setUp(self): + self.list_of_files = [] + for (directory, _, filenames) in walk(Path(__file__).absolute().parents[1].as_posix()): + self.list_of_files += [join(directory, file) for file in filenames + if file.endswith('.py') and 'test' not in directory + and 'rpc_spec' not in directory] + + def test_checkCodeFormatAndQuality(self): + """ + Performing checks of Code Format And Quality by flake8 + If any inconvenient low quality code will be found, this will be shown in stdout and + each such cases will be reflected with report.total_errors number + """ + style_guide = flake8.get_style_guide(max_line_length=120) + report = style_guide.check_files(self.list_of_files) + self.assertEqual(report.total_errors, 0) + + +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..22650ce67 --- /dev/null +++ b/generator/test/test_enums.py @@ -0,0 +1,83 @@ +from collections import OrderedDict +from unittest import TestCase + +try: + from generator import Generator +except ImportError as error: + from generator.generator import Generator + +from model.enum import Enum +from model.enum_element import EnumElement +from transformers.enums_producer import EnumsProducer + + +class TestEnumsProducer(TestCase): + """ + The structures of tests in this class was prepared to cover all possible combinations of code branching in tested + class EnumsProducer. + All names of Enums and nested elements doesn't reflating with real Enums + and could be replaces with some meaningless names. + + After performing Tests there are following initial test code coverage: + generator/transformers/common_producer.py 34% + generator/transformers/enums_producer.py 100% + """ + + def setUp(self): + self.maxDiff = None + key_words = Generator().get_key_words() + + self.producer = EnumsProducer('SDLEnum', key_words) + + def test_FunctionID(self): + """ + generator/transformers/common_producer.py 34% + generator/transformers/enums_producer.py 80% + """ + 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, deprecated=False), + self.producer.param_named(description=[], name='RegisterAppInterface', origin='RegisterAppInterfaceID', + since=None, deprecated=False), + self.producer.param_named(description=[], name='PerformAudioPassThru', origin='PerformAudioPassThruID', + since=None, deprecated=False),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_TextFieldName(self): + """ + generator/transformers/common_producer.py 34% + generator/transformers/enums_producer.py 98% + """ + 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, + deprecated=False), + self.producer.param_named(description=[], name='MainField1', origin='mainField1', since=None, + deprecated=False), + self.producer.param_named(description=[], name='H264', origin='H264', since=None, deprecated=False), + self.producer.param_named(description=[], name='UnsupportedRequest', origin='UNSUPPORTED_REQUEST', + since=None, deprecated=False)) + + 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..a271fa23a --- /dev/null +++ b/generator/test/test_functions.py @@ -0,0 +1,456 @@ +import re +from collections import namedtuple, OrderedDict +from unittest import TestCase + +try: + from generator import Generator +except ImportError as error: + from generator.generator import Generator + +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): + """ + The structures of tests in this class was prepared to cover all possible combinations of code branching in tested + class FunctionsProducer. + All names of Functions and nested elements doesn't reflating with real Functions + and could be replaces with some meaningless names. + + After performing Tests there are following initial test code coverage: + generator/transformers/common_producer.py 99% + generator/transformers/functions_producer.py 100% + """ + + def setUp(self): + self.maxDiff = None + key_words = Generator().get_key_words() + + 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') + + enum_names = ('FileType', 'Language',) + struct_names = ('SyncMsgVersion', 'TemplateColorScheme', 'TTSChunk', 'Choice') + + self.producer = FunctionsProducer(paths, enum_names, struct_names, key_words) + + def test_process_function_name(self): + """ + generator/transformers/common_producer.py 29% + generator/transformers/functions_producer.py 61% + """ + 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='SdlMsgVersion', + origin='syncMsgVersion', since='3.5.0')] + actual = self.producer.get_simple_params(functions, structs) + self.assertCountEqual(expected, actual['params']) + + def test_RegisterAppInterfaceRequest(self): + """ + generator/transformers/common_producer.py 85% + generator/transformers/functions_producer.py 63% + """ + 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', 'SDLSdlMsgVersion'}}, + '.m': {'SDLTemplateColorScheme', 'SDLTTSChunk', 'SDLSdlMsgVersion'}} + 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='sdlMsgVersion', constructor_argument_override=None, + constructor_prefix='SdlMsgVersion', deprecated=False, description=['See SyncMsgVersion'], + for_name='object', mandatory=True, method_suffix='SdlMsgVersion', modifier='strong', + of_class='SDLSdlMsgVersion.class', origin='sdlMsgVersion', since=None, + type_native='SDLSdlMsgVersion *', type_sdl='SDLSdlMsgVersion *'), + 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 *', type_sdl='NSArray *'), + 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 *')) + + mandatory_arguments = [ + self.producer.argument_named(variable='sdlMsgVersion', deprecated=False, origin='sdlMsgVersion', + constructor_argument='sdlMsgVersion'), + 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 = 'SdlMsgVersion:(SDLSdlMsgVersion *)sdlMsgVersion ' \ + 'isMediaApplication:(BOOL)isMediaApplication' + + expected['constructors'] = ( + self.producer.constructor_named( + all=mandatory_arguments, arguments=mandatory_arguments, deprecated=False, + init=mandatory_init, self=True), + 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 *)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): + """ + generator/transformers/common_producer.py 82% + generator/transformers/functions_producer.py 63% + """ + 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 *> *', type_sdl='NSArray *> *'), + 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 *', type_sdl='NSArray *')) + + 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 *>' + ' *)supportedDiagModes hmiZoneCapabilities:(nullable NSArray *)' + 'hmiZoneCapabilities', + self=''),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_OnHMIStatus(self): + """ + generator/transformers/common_producer.py 66% + generator/transformers/functions_producer.py 65% + """ + 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=True, init='HmiLevel:(SDLHMILevel)hmiLevel'),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_CreateWindow(self): + """ + generator/transformers/common_producer.py 82% + generator/transformers/functions_producer.py 63% + """ + 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)) + params['duplicateUpdatesFromWindowID'] = Param(name='duplicateUpdatesFromWindowID', param_type=Integer(), + is_mandatory=False) + 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 *'), + 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 *'), + 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 *'), + 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 *'), + 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 *'), + self.producer.param_named( + constructor_argument='duplicateUpdatesFromWindowID', constructor_argument_override=None, + constructor_prefix='DuplicateUpdatesFromWindowID', deprecated=False, + description=['{"default_value": null, "max_value": null, "min_value": null}'], for_name='object', + mandatory=False, method_suffix='DuplicateUpdatesFromWindowID', modifier='strong', + of_class='NSNumber.class', origin='duplicateUpdatesFromWindowID', since=None, + type_native='NSNumber *', type_sdl='NSNumber *')) + + not_mandatory_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)')] + mandatory_arguments = [self.producer.argument_named( + variable='duplicateUpdatesFromWindowID', deprecated=False, origin='duplicateUpdatesFromWindowID', + constructor_argument='duplicateUpdatesFromWindowID')] + + expected['constructors'] = ( + self.producer.constructor_named( + all=not_mandatory_arguments, arguments=not_mandatory_arguments, deprecated=False, self=True, + init='WindowID:(UInt32)windowID cmdID:(UInt32)cmdID position:(UInt16)position speed:(float)speed ' + 'offset:(UInt64)offset'), + self.producer.constructor_named( + all=not_mandatory_arguments + mandatory_arguments, arguments=mandatory_arguments, + deprecated=False, self='WindowID:windowID cmdID:cmdID position:position speed:speed offset:offset', + init='WindowID:(UInt32)windowID cmdID:(UInt32)cmdID position:(UInt16)position speed:(float)speed ' + 'offset:(UInt64)offset duplicateUpdatesFromWindowID:(nullable NSNumber *)' + 'duplicateUpdatesFromWindowID')) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_CreateInteractionChoiceSet(self): + """ + generator/transformers/common_producer.py 67% + generator/transformers/functions_producer.py 63% + """ + 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 *', type_sdl='NSArray *'),) + + 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=True, + init='ChoiceSet:(NSArray *)choiceSet'),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_SetDisplayLayout(self): + """ + generator/transformers/common_producer.py 66% + generator/transformers/functions_producer.py 63% + """ + params = OrderedDict() + params['displayLayout'] = Param(name='displayLayout', param_type=String(max_length=500, min_length=1)) + item = Function(name='SetDisplayLayout', function_id=EnumElement(name='SetDisplayLayoutID'), + message_type=EnumElement(name='request'), params=params, history=[ + Function(name='SetDisplayLayout', function_id=EnumElement(name='SetDisplayLayoutID'), + message_type=EnumElement(name='request'), params={}, history=None, since='3.0.0', + until='6.0.0') + ], since='6.0.0', until=None, deprecated='true') + + expected = OrderedDict() + expected['origin'] = 'SetDisplayLayout' + expected['name'] = 'SDLSetDisplayLayout' + expected['extends_class'] = 'SDLRPCRequest' + expected['imports'] = {'.h': {'enum': {'SDLRPCRequest'}, 'struct': set()}, '.m': set()} + expected['since'] = '6.0.0' + expected['history'] = '3.0.0' + expected['deprecated'] = True + expected['params'] = ( + self.producer.param_named( + constructor_argument='displayLayout', constructor_argument_override=None, + constructor_prefix='DisplayLayout', deprecated=False, + description=['{"default_value": null, "max_length": 500, "min_length": 1}'], for_name='object', + mandatory=True, method_suffix='DisplayLayout', modifier='strong', of_class='NSString.class', + origin='displayLayout', since=None, type_native='NSString *', type_sdl='NSString *'),) + + argument = [ + self.producer.argument_named(variable='displayLayout', deprecated=False, + constructor_argument='displayLayout', origin='displayLayout')] + + expected['constructors'] = (self.producer.constructor_named( + all=argument, arguments=argument, deprecated=False, self=True, + init='DisplayLayout:(NSString *)displayLayout'),) + + 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..61ea23702 --- /dev/null +++ b/generator/test/test_structs.py @@ -0,0 +1,106 @@ +from collections import OrderedDict +from unittest import TestCase + +try: + from generator import Generator +except ImportError as error: + from generator.generator import Generator + +from model.integer import Integer +from model.param import Param +from model.string import String +from model.struct import Struct +from transformers.structs_producer import StructsProducer + + +class TestStructsProducer(TestCase): + """ + The structures of tests in this class was prepared to cover all possible combinations of code branching in tested + class StructsProducer. + All names of Structs and nested elements doesn't reflating with real Structs + and could be replaces with some meaningless names. + + After performing Tests there are following initial test code coverage: + generator/transformers/common_producer.py 72% + generator/transformers/structs_producer.py 100% + """ + + def setUp(self): + self.maxDiff = None + key_words = ('value', 'id') + + self.producer = StructsProducer('SDLRPCStruct', enum_names=(), struct_names=['Image'], key_words=key_words) + + def test_CloudAppProperties(self): + """ + generator/transformers/common_producer.py 64% + generator/transformers/structs_producer.py 100% + """ + members = OrderedDict() + members['appID'] = Param(name='appID', param_type=String()) + members['value'] = Param(name='value', param_type=String()) + item = Struct(name='CloudAppProperties', members=members) + 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 *'), + self.producer.param_named( + constructor_argument='valueParam', constructor_argument_override=None, constructor_prefix='ValueParam', + deprecated=False, description=['{"default_value": null, "max_length": null, "min_length": null}'], + for_name='object', mandatory=True, method_suffix='ValueParam', modifier='strong', + of_class='NSString.class', origin='valueParam', since=None, type_native='NSString *', + type_sdl='NSString *') + ) + + argument = [ + self.producer.argument_named( + variable='appID', deprecated=False, constructor_argument='appID', origin='appID'), + self.producer.argument_named( + variable='valueParam', deprecated=False, constructor_argument='valueParam', origin='valueParam') + ] + + expected['constructors'] = (self.producer.constructor_named( + all=argument, arguments=argument, deprecated=False, self='', + init='AppID:(NSString *)appID valueParam:(NSString *)valueParam'),) + + actual = self.producer.transform(item) + self.assertDictEqual(expected, actual) + + def test_TouchEvent(self): + """ + generator/transformers/common_producer.py 69% + generator/transformers/structs_producer.py 100% + """ + item = Struct(name='TouchEvent', members={ + 'id': Param(name='id', param_type=Integer(max_value=9, min_value=0)) + }) + expected = OrderedDict() + expected['origin'] = 'TouchEvent' + expected['name'] = 'SDLTouchEvent' + expected['extends_class'] = 'SDLRPCStruct' + expected['imports'] = {'.h': {'enum': {'SDLRPCStruct'}, 'struct': set()}, '.m': set()} + expected['params'] = ( + self.producer.param_named( + constructor_argument='idParam', constructor_argument_override=None, + constructor_prefix='IdParam', deprecated=False, + description=['{"default_value": null, "max_value": 9, "min_value": 0}'], for_name='object', + mandatory=True, method_suffix='IdParam', modifier='strong', of_class='NSNumber.class', + origin='idParam', since=None, type_native='UInt8', type_sdl='NSNumber *'),) + + argument = [ + self.producer.argument_named(variable='idParam', deprecated=False, + constructor_argument='@(idParam)', origin='idParam')] + + expected['constructors'] = (self.producer.constructor_named( + all=argument, arguments=argument, deprecated=False, self='', + init='IdParam:(UInt8)idParam'),) + + 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 diff --git a/generator/transformers/common_producer.py b/generator/transformers/common_producer.py new file mode 100644 index 000000000..ac7bc6167 --- /dev/null +++ b/generator/transformers/common_producer.py @@ -0,0 +1,345 @@ +""" +All Enums/Structs/Functions Producer are inherited from this class and using features of it +""" +import json +import logging +import re +import textwrap +from abc import ABC, abstractmethod +from collections import OrderedDict, namedtuple + +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): + """ + All Enums/Structs/Functions Producer are inherited from this class and using features of it + """ + + def __init__(self, enum_names=(), struct_names=(), key_words=()): + self.logger = logging.getLogger(self.__class__.__name__) + self.struct_names = tuple(map(lambda e: self._replace_sync(e), struct_names)) + self.key_words = key_words + 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') + self.names = self.struct_names + tuple(map(lambda e: self._replace_sync(e), enum_names)) + + @property + @abstractmethod + def container_name(self): + pass + + def transform(self, item: (Enum, Function, Struct), render: dict) -> dict: + """ + Main entry point for transforming each Enum/Function/Struct into output dictionary, + which going to be applied to Jinja2 template + :param item: instance of Enum/Function/Struct + :param render: dictionary with pre filled entries, which going to be filled/changed by reference + :return: dictionary which going to be applied to Jinja2 template + """ + if item.description: + render['description'] = self.extract_description(item.description) + if item.since: + render['since'] = item.since + if item.history: + render['history'] = item.history.pop().since + if item.deprecated and str(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, item.name) + if isinstance(item, (Struct, Function)): + self.extract_imports(param, render['imports']) + + if 'constructors' not in render and isinstance(item, (Struct, Function)): + render['constructors'] = self.extract_constructors(render['params']) + + render['params'] = tuple(render['params'].values()) + return render + + def _replace_keywords(self, name: str) -> str: + origin = name + if name.isupper(): + name += '_PARAM' + else: + name += 'Param' + self.logger.debug('Replacing %s with %s', origin, name) + return name + + def replace_keywords(self, name: str) -> str: + """ + if :param name in self.key_words, :return: name += 'Param' + :param name: string with item name + """ + if name.casefold() in self.key_words: + name = self._replace_keywords(name) + return self._replace_sync(name) + + @staticmethod + def _replace_sync(name): + """ + :param name: string with item name + :return: string with replaced 'sync' to 'Sdl' + """ + if name: + name = re.sub(r'^([sS])ync(.+)$', r'\1dl\2', name) + return name + + def extract_imports(self, param: Param, imports: dict): + """ + Extracting appropriate imports and updating in render['imports'] by reference + :param param: instance of Param, which is sub element of Enum/Function/Struct + :param imports: dictionary from render['imports'] + :return: dictionary with extracted imports + """ + + if isinstance(param.param_type, Array): + type_origin, kind = self.evaluate_import(param.param_type.element_type) + else: + type_origin, kind = self.evaluate_import(param.param_type) + + if type_origin and any(map(lambda n: type_origin.lower() in n.lower(), self.names)): + name = 'SDL' + type_origin + imports['.h'][kind].add(name) + imports['.m'].add(name) + + return imports + + def evaluate_import(self, element): + """ + :param element: instance of param.param_type + :return: tuple with element.name, type(element).__name__.lower() + """ + if isinstance(element, (Struct, Enum)): + return self._replace_sync(element.name), type(element).__name__.lower() + return None, None + + @staticmethod + def title(name: str = '') -> str: + """ + Capitalizing only first character in string. + :param name: string to be capitalized first character + :return: initial parameter with capitalized first character + """ + return name[:1].upper() + name[1:] + + @staticmethod + def minimize_first(name: str = '') -> str: + """ + Minimizing only first character in string. + :param name: string to be minimized first character + :return: initial parameter with minimized first character + """ + 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: length of the string to be split + :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: str, mandatory: bool) -> str: + """ + Used for adding nullable modificator into initiator (constructor) parameter + :param type_native: native type + :param mandatory: is parameter mandatory + :return: string with modificator + """ + if mandatory or re.match(r'BOOL|float|double', type_native): + return '' + return 'nullable ' + + def parentheses(self, item): + """ + Used for wrapping appropriate initiator (constructor) parameter with '@({})' + :param item: named tup[le with initiator (constructor) parameter + :return: wrapped parameter + """ + if re.match(r'\w*Int\d+|BOOL|float|double', item.type_native) or \ + any(map(lambda n: item.type_native.lower() in n.lower(), self.struct_names)): + return '@({})'.format(item.constructor_argument) + return item.constructor_argument + + def extract_constructor(self, data: list, mandatory: bool) -> dict: + """ + Preparing dictionary with initial initiator (constructor) + :param data: list with prepared parameters + :param mandatory: is parameter mandatory + :return: dictionary with initial initiator (constructor) + """ + data = list(data) + + 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.parentheses(first), + variable=first.constructor_argument, deprecated=first.deprecated)] + for param in data: + arguments.append(self.argument_named(origin=param.origin, constructor_argument=self.parentheses(param), + variable=param.constructor_argument, deprecated=param.deprecated)) + init.append('{}:({}{}){}'.format(self.minimize_first(param.constructor_prefix), + self.nullable(param.type_native, mandatory), + param.type_native.strip(), param.constructor_argument)) + _self = True if 'functions' in self.__class__.__name__.lower() and mandatory else '' + return {'init': ' '.join(init), 'self': _self, 'arguments': arguments, 'all': arguments} + + def extract_constructors(self, data: dict) -> tuple: + """ + Preparing tuple with all initiators (constructors) + :param data: list with prepared parameters + :return: tuple with all initiators (constructors) + """ + mandatory = [] + not_mandatory = [] + deprecated = any([m.deprecated for m in data.values() if getattr(m, 'deprecated', False)]) + 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) + mandatory['deprecated'] = deprecated + else: + mandatory = OrderedDict() + + if not_mandatory: + not_mandatory = self.extract_constructor(not_mandatory, False) + not_mandatory['deprecated'] = deprecated + if mandatory: + not_mandatory['init'] = '{} {}'.format(mandatory['init'], self.minimize_first(not_mandatory['init'])) + not_mandatory['all'] = mandatory['arguments'] + not_mandatory['arguments'] + not_mandatory['self'] = re.sub(r'([\w\d]+:)\([\w\d\s<>*]*\)([\w\d]+\s*)', r'\1\2', mandatory['init']) + result.append(self.constructor_named(**not_mandatory)) + + if mandatory: + result.insert(0, self.constructor_named(**mandatory)) + + return tuple(result) + + def evaluate_type(self, instance) -> dict: + """ + Extracting dictionary with evaluated output types + :param instance: param_type of Param + :return: dictionary with evaluated output types + """ + if hasattr(instance, 'name'): + instance.name = self.replace_keywords(instance.name) + 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 *' + return data + + def extract_type(self, param: Param) -> dict: + """ + Preparing dictionary with output types information + :param param: sub element of Enum/Function/Struct + :return: dictionary with output types information + """ + + 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()) + else: + data = self.evaluate_type(param.param_type) + + if not param.is_mandatory and re.match(r'\w*Int\d*|BOOL', data['type_native']): + data['type_native'] = data['type_sdl'] + + return data + + @staticmethod + def param_origin_change(name) -> dict: + """ + Based on name preparing common part of output types information + :param name: Param name + :return: dictionary with part of output types information + """ + return {'origin': name, + 'constructor_argument': name, + 'constructor_prefix': InterfaceProducerCommon.title(name), + 'method_suffix': InterfaceProducerCommon.title(name)} + + def extract_param(self, param: Param, item_name: str): + """ + Preparing self.param_named with prepared params + :param param: Param from initial Model + :return: self.param_named with prepared params + """ + param.name = self.replace_keywords(param.name) + 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) diff --git a/generator/transformers/enums_producer.py b/generator/transformers/enums_producer.py new file mode 100644 index 000000000..d41cd739d --- /dev/null +++ b/generator/transformers/enums_producer.py @@ -0,0 +1,81 @@ +""" +Enums transformer +""" +import json +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, key_words): + super(EnumsProducer, self).__init__(key_words=key_words) + self._container_name = 'elements' + self.enum_class = enum_class + self.logger = logging.getLogger(self.__class__.__name__) + self.param_named = namedtuple('param_named', 'origin description name since deprecated') + self._item_name = None + + @property + def container_name(self): + return self._container_name + + def transform(self, item: Enum, render: dict = None) -> dict: + """ + Main entry point for transforming each Enum into output dictionary, + which going to be applied to Jinja2 template + :param item: instance of Enum + :param render: empty dictionary, present in parameter for code consistency + :return: dictionary which going to be applied to Jinja2 template + """ + item.name = self._replace_sync(item.name) + 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, item_name: str): + """ + Preparing self.param_named with prepared params + :param param: EnumElement from initial Model + :param item_name: + :return: self.param_named with prepared params + """ + data = {'origin': param.name, + 'description': self.extract_description(param.description), + 'since': param.since, + 'deprecated': json.loads(param.deprecated.lower()) if param.deprecated else False} + name = None + if re.match(r'^[A-Z]{1,2}\d|\d[A-Z]{1,2}$', param.name): + name = param.name + elif re.match(r'(^[a-z\d]+$|^[A-Z\d]+$)', param.name): + name = param.name.title() + elif re.match(r'^(?=\w*[a-z])(?=\w*[A-Z])\w+$', param.name): + if param.name.endswith('ID'): + name = param.name[:-2] + else: + 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()) + name = ''.join(name) + if any(re.search(r'^(sdl)?({}){}$'.format(item_name.casefold(), name.casefold()), k) for k in self.key_words): + name = self._replace_keywords(name) + data['name'] = name + 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..fcab84a14 --- /dev/null +++ b/generator/transformers/functions_producer.py @@ -0,0 +1,116 @@ +""" +Functions transformer +""" + +import logging +from collections import namedtuple, OrderedDict + +from model.function import Function +from transformers.common_producer import InterfaceProducerCommon + + +class FunctionsProducer(InterfaceProducerCommon): + """ + Functions transformer + """ + + def __init__(self, paths, enum_names, struct_names, key_words): + super(FunctionsProducer, self).__init__(enum_names=enum_names, struct_names=struct_names, key_words=key_words) + self._container_name = 'params' + 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') + + @property + def container_name(self): + return self._container_name + + def transform(self, item: Function, render: dict = None) -> dict: + """ + Main entry point for transforming each Enum/Function/Struct into output dictionary, + which going to be applied to Jinja2 template + :param item: instance of Enum/Function/Struct + :param render: dictionary with pre filled entries, which going to be filled/changed by reference + :return: dictionary which going to be applied to Jinja2 template + """ + list(map(item.params.__delitem__, filter(item.params.__contains__, ['success', 'resultCode', 'info']))) + item.name = self._replace_sync(item.name) + 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: + """ + Standalone method used for preparing SDLRPCFunctionNames collection ready to be applied to Jinja2 template + :param items: collection with all functions from initial Model + :return: collection with transformed element ready to be applied to Jinja2 template + """ + render = OrderedDict() + for item in items.values(): + tmp = {'name': self.title(self.replace_keywords(item.name)), + 'origin': item.name, + 'description': self.extract_description(item.description), + 'since': item.since} + render[item.name] = self.common_names(**tmp) + + return {'params': sorted(render.values(), key=lambda a: a.name)} + + def evaluate(self, element) -> dict: + """ + Internal evaluator used for preparing SDLRPCParameterNames collection + :param element: Param from initial Model + :return: dictionary with evaluated part of output collection + """ + origin = element.name + name = self._replace_sync(element.name) + # if isinstance(element.param_type, (Integer, Float, Boolean, String)): + return {name: self.common_names(**{ + 'name': self.title(name), + 'origin': origin, + 'description': self.extract_description(element.description), + 'since': element.since})} + # return OrderedDict() + + def get_simple_params(self, functions: dict, structs: dict) -> dict: + """ + Standalone method used for preparing SDLRPCParameterNames collection ready to be applied to Jinja2 template + :param functions: collection with all functions from initial Model + :param structs: collection with all structs from initial Model + :return: collection with transformed element ready to be applied to Jinja2 template + """ + render = OrderedDict() + + for func in functions.values(): + for param in func.params.values(): + render.update(self.evaluate(param)) + + for struct in structs.values(): + render.update(self.evaluate(struct)) + for param in struct.members.values(): + render.update(self.evaluate(param)) + unique = dict(zip(list(map(lambda l: l.name, render.values())), render.values())) + return {'params': sorted(unique.values(), key=lambda a: a.name)} diff --git a/generator/transformers/structs_producer.py b/generator/transformers/structs_producer.py new file mode 100644 index 000000000..b25d86230 --- /dev/null +++ b/generator/transformers/structs_producer.py @@ -0,0 +1,48 @@ +""" +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, struct_names, key_words): + super(StructsProducer, self).__init__(enum_names=enum_names, struct_names=struct_names, key_words=key_words) + self._container_name = 'members' + self.struct_class = struct_class + self.logger = logging.getLogger(self.__class__.__name__) + + @property + def container_name(self): + return self._container_name + + def transform(self, item: Struct, render: dict = None) -> dict: + """ + Main entry point for transforming each Enum/Function/Struct into output dictionary, + which going to be applied to Jinja2 template + :param item: instance of Enum/Function/Struct + :param render: dictionary with pre filled entries, which going to be filled/changed by reference + :return: dictionary which going to be applied to Jinja2 template + """ + item.name = self._replace_sync(item.name) + 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