diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 211926c5..e31b1b82 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [macos-latest, ubuntu-20.04] - python-version: [3.6, 3.8, 3.11, pypy-3.7] + python-version: [3.8, 3.11, pypy-3.8, pypy-3.10] steps: - uses: actions/checkout@v2 - name: Setup Python environment @@ -35,5 +35,5 @@ jobs: - name: Run MyPy if: matrix.python-version != 'pypy-3.7' run: | - pip install enum34 mypy typed-ast types-six + pip install enum34 mypy types-six ./mypy-run.sh diff --git a/requirements.txt b/requirements.txt index 07605c18..d2431b5c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ ply>= 3.4 six>= 1.12.0 packaging>=21.0 +Jinja2>= 3.0.3 diff --git a/stone/backends/python_types.py b/stone/backends/python_types.py index b6d40763..f8625ea7 100644 --- a/stone/backends/python_types.py +++ b/stone/backends/python_types.py @@ -640,10 +640,12 @@ def _generate_custom_annotation_processors(self, ns, data_type, extra_annotation dt, _, _ = unwrap(data_type) if is_struct_type(dt) or is_union_type(dt): annotation_types_seen = set() - # If data type enumerates subtypes, recurse to subtypes instead which in turn collect parents' custom annotations + # If data type enumerates subtypes, recurse to subtypes instead which in turn collect + # parents' custom annotations if is_struct_type(dt) and dt.has_enumerated_subtypes(): for subtype in dt.get_enumerated_subtypes(): - for annotation_type, recursive_processor in self._generate_custom_annotation_processors(ns, subtype.data_type): + processors = self._generate_custom_annotation_processors(ns, subtype.data_type) + for annotation_type, recursive_processor in processors: if annotation_type not in annotation_types_seen: yield (annotation_type, recursive_processor) annotation_types_seen.add(annotation_type) @@ -653,7 +655,8 @@ def _generate_custom_annotation_processors(self, ns, data_type, extra_annotation yield (annotation.annotation_type, generate_func_call( 'bb.make_struct_annotation_processor', - args=[class_name_for_annotation_type(annotation.annotation_type, ns), + args=[class_name_for_annotation_type(annotation.annotation_type, + ns), 'processor'] )) annotation_types_seen.add(annotation.annotation_type) diff --git a/stone/backends/swift.py b/stone/backends/swift.py index f20aeccb..d97a621d 100644 --- a/stone/backends/swift.py +++ b/stone/backends/swift.py @@ -1,5 +1,5 @@ from contextlib import contextmanager - +import os from stone.backend import CodeBackend from stone.backends.swift_helpers import ( fmt_class, @@ -7,26 +7,31 @@ fmt_obj, fmt_type, fmt_var, + fmt_objc_type, + mapped_list_info, ) + from stone.ir import ( Boolean, Bytes, - DataType, Float32, Float64, Int32, Int64, List, + Map, String, Timestamp, UInt32, UInt64, Void, is_list_type, + is_map_type, is_timestamp_type, is_union_type, is_user_defined_type, unwrap_nullable, + is_nullable_type, ) _serial_type_table = { @@ -37,6 +42,7 @@ Int32: 'Int32Serializer', Int64: 'Int64Serializer', List: 'ArraySerializer', + Map: 'DictionarySerializer', String: 'StringSerializer', Timestamp: 'NSDateSerializer', UInt32: 'UInt32Serializer', @@ -44,6 +50,21 @@ Void: 'VoidSerializer', } +_nsnumber_type_table = { + Boolean: '.boolValue', + Bytes: '', + Float32: '.floatValue', + Float64: '.doubleValue', + Int32: '.int32Value', + Int64: '.int64Value', + List: '', + String: '', + Timestamp: '', + UInt32: '.uint32Value', + UInt64: '.uint64Value', + Void: '', + Map: '', +} stone_warning = """\ /// @@ -98,24 +119,6 @@ def _func_args(self, args_list, newlines=False, force_first=False, not_init=Fals sep += '\n' + self.make_indent() return sep.join(out) - @contextmanager - def class_block(self, thing, protocols=None): - protocols = protocols or [] - extensions = [] - - if isinstance(thing, DataType): - name = fmt_class(thing.name) - if thing.parent_type: - extensions.append(fmt_type(thing.parent_type)) - else: - name = thing - extensions.extend(protocols) - - extend_suffix = ': {}'.format(', '.join(extensions)) if extensions else '' - - with self.block('open class {}{}'.format(name, extend_suffix)): - yield - def _struct_init_args(self, data_type, namespace=None): # pylint: disable=unused-argument args = [] for field in data_type.all_fields: @@ -135,6 +138,113 @@ def _struct_init_args(self, data_type, namespace=None): # pylint: disable=unuse args.append(arg) return args + def _objc_init_args(self, data_type, include_defaults=True): + args = [] + for field in data_type.all_fields: + name = fmt_var(field.name) + value = fmt_objc_type(field.data_type) + data_type, nullable = unwrap_nullable(field.data_type) + + if not include_defaults and (field.has_default or nullable): + continue + + arg = (name, value) + args.append(arg) + return args + + def _objc_no_defualts_func_args(self, data_type, args_data=None): + args = [] + for field in data_type.all_fields: + name = fmt_var(field.name) + _, nullable = unwrap_nullable(field.data_type) + if field.has_default or nullable: + continue + arg = (name, name) + args.append(arg) + + if args_data is not None: + _, type_data_list = tuple(args_data) + extra_args = [tuple(type_data[:-1]) for type_data in type_data_list] + for name, _, extra_type in extra_args: + if not is_nullable_type(extra_type): + arg = (name, name) + args.append(arg) + + return self._func_args(args) + + def _objc_init_args_to_swift(self, data_type, args_data=None, include_defaults=True): + args = [] + for field in data_type.all_fields: + name = fmt_var(field.name) + field_data_type, nullable = unwrap_nullable(field.data_type) + if not include_defaults and (field.has_default or nullable): + continue + nsnumber_type = _nsnumber_type_table.get(field_data_type.__class__) + value = '{}{}{}'.format(name, + '?' if nullable and nsnumber_type else '', + nsnumber_type) + if is_list_type(field_data_type): + _, prefix, suffix, list_data_type, _ = mapped_list_info(field_data_type) + + value = '{}{}'.format(name, + '?' if nullable else '') + list_nsnumber_type = _nsnumber_type_table.get(list_data_type.__class__) + + if not is_user_defined_type(list_data_type) and not list_nsnumber_type: + value = name + else: + value = '{}.map {}'.format(value, + prefix) + + if is_user_defined_type(list_data_type): + value = '{}{{ $0.{} }}'.format(value, + self._objc_swift_var_name(list_data_type)) + else: + value = '{}{{ $0{} }}'.format(value, + list_nsnumber_type) + + value = '{}{}'.format(value, + suffix) + elif is_map_type(field_data_type): + if is_user_defined_type(field_data_type.value_data_type): + value = '{}{}.mapValues {{ $0.swift }}'.format(name, + '?' if nullable else '') + elif is_user_defined_type(field_data_type): + value = '{}{}.{}'.format(name, + '?' if nullable else '', + self._objc_swift_var_name(field_data_type)) + + arg = (name, value) + args.append(arg) + + if args_data is not None: + _, type_data_list = tuple(args_data) + extra_args = [tuple(type_data[:-1]) for type_data in type_data_list] + for name, _, _ in extra_args: + args.append((name, name)) + + return self._func_args(args) + + def _objc_swift_var_name(self, data_type): + parent_type = data_type.parent_type + uw_parent_type, _ = unwrap_nullable(parent_type) + sub_count = 1 if parent_type else 0 + while is_user_defined_type(uw_parent_type) and parent_type.parent_type: + sub_count += 1 + parent_type = parent_type.parent_type + uw_parent_type, _ = unwrap_nullable(parent_type) + + if sub_count == 0 or is_union_type(data_type): + return 'swift' + else: + name = 'Swift' + i = 1 + while i <= sub_count: + name = '{}{}'.format('sub' if i == sub_count else 'Sub', + name) + i += 1 + return name + def _docf(self, tag, val): if tag == 'route': if ':' in val: @@ -155,6 +265,14 @@ def _docf(self, tag, val): else: return val + def _write_output_in_target_folder(self, output, file_name): + full_path = self.target_folder_path + if not os.path.exists(full_path): + os.mkdir(full_path) + full_path = os.path.join(full_path, file_name) + with open(full_path, "w", encoding='utf-8') as fh: + fh.write(output) + def fmt_serial_type(data_type): data_type, nullable = unwrap_nullable(data_type) @@ -167,6 +285,9 @@ def fmt_serial_type(data_type): if is_list_type(data_type): result = result + '<{}>'.format(fmt_serial_type(data_type.data_type)) + if is_map_type(data_type): + result = result + '<{}, {}>'.format(fmt_serial_type(data_type.key_data_type), + fmt_serial_type(data_type.value_data_type)) return result if not nullable else 'NullableSerializer' @@ -183,6 +304,8 @@ def fmt_serial_obj(data_type): if is_list_type(data_type): result = result + '({})'.format(fmt_serial_obj(data_type.data_type)) + elif is_map_type(data_type): + result = result + '({})'.format(fmt_serial_obj(data_type.value_data_type)) elif is_timestamp_type(data_type): result = result + '("{}")'.format(data_type.format) else: diff --git a/stone/backends/swift_client.py b/stone/backends/swift_client.py index 8225286f..00362d69 100644 --- a/stone/backends/swift_client.py +++ b/stone/backends/swift_client.py @@ -1,7 +1,18 @@ import json +import os +import jinja2 +import textwrap +from stone.ir import ( + is_struct_type, + is_union_type, + is_void_type, + unwrap_nullable, + is_list_type, + is_user_defined_type, + is_numeric_type, +) from stone.backends.swift import ( - base, fmt_serial_type, SwiftBaseBackend, undocumented, @@ -12,11 +23,12 @@ fmt_func, fmt_var, fmt_type, -) -from stone.ir import ( - is_struct_type, - is_union_type, - is_void_type, + fmt_route_name, + fmt_route_name_namespace, + fmt_func_namespace, + fmt_objc_type, + mapped_list_info, + datatype_has_subtypes, ) _MYPY = False @@ -56,6 +68,12 @@ type=str, help='The name of the Swift class that manages network API calls.', ) +_cmdline_parser.add_argument( + '-w', + '--auth-type', + type=str, + help='The auth type of the client to generate.', +) _cmdline_parser.add_argument( '-y', '--client-args', @@ -70,6 +88,11 @@ type=str, help='The dict that maps a style type to a Swift request object name.', ) +_cmdline_parser.add_argument( + '--objc', + action='store_true', + help='Generate the Objective-C compatibile files.', +) class SwiftBackend(SwiftBaseBackend): @@ -79,11 +102,11 @@ class SwiftBackend(SwiftBaseBackend): Examples: ``` - open class ExampleClientBase { + public class ExampleClientBase { /// Routes within the namespace1 namespace. See Namespace1 for details. - open var namespace1: Namespace1! + public var namespace1: Namespace1! /// Routes within the namespace2 namespace. See Namespace2 for details. - open var namespace2: Namespace2! + public var namespace2: Namespace2! public init(client: ExampleTransportClient) { self.namespace1 = Namespace1(client: client) @@ -98,7 +121,7 @@ class SwiftBackend(SwiftBaseBackend): enpoding might be implemented like: ``` - open func copy(fromPath fromPath: String, toPath: String) -> + public func copy(fromPath fromPath: String, toPath: String) -> ExampleRequestType { let route = Namespace1.copy let serverArgs = Namespace1.CopyArg(fromPath: fromPath, toPath: toPath) @@ -114,66 +137,262 @@ class SwiftBackend(SwiftBaseBackend): def generate(self, api): for namespace in api.namespaces.values(): - ns_class = fmt_class(namespace.name) if namespace.routes: - with self.output_to_relative_path('{}Routes.swift'.format(ns_class)): - self._generate_routes(namespace) + self._generate_routes(namespace) - with self.output_to_relative_path('{}.swift'.format(self.args.module_name)): - self._generate_client(api) + self._generate_client(api) + self._generate_request_boxes(api) + if not self.args.objc: + self._generate_reconnection_helpers(api) def _generate_client(self, api): - self.emit_raw(base) - self.emit('import Alamofire') - self.emit() - - with self.block('open class {}'.format(self.args.class_name)): - namespace_fields = [] - for namespace in api.namespaces.values(): - if namespace.routes: - namespace_fields.append((namespace.name, - fmt_class(namespace.name))) - for var, typ in namespace_fields: - self.emit('/// Routes within the {} namespace. ' - 'See {}Routes for details.'.format(var, typ)) - self.emit('open var {}: {}Routes!'.format(var, typ)) - self.emit() - - with self.function_block('public init', args=self._func_args( - [('client', '{}'.format(self.args.transport_client_name))])): - for var, typ in namespace_fields: - self.emit('self.{} = {}Routes(client: client)'.format(var, typ)) + template_globals = {} + template_globals['class_name'] = self.args.class_name + template_globals['namespaces'] = api.namespaces.values() + template_globals['namespace_fields'] = self._namespace_fields(api) + template_globals['transport_client_name'] = self.args.transport_client_name + + if self.args.objc: + template = self._jinja_template("ObjCClient.jinja") + template.globals = template_globals + + self._write_output_in_target_folder(template.render(), + 'DBX{}.swift'.format(self.args.module_name)) + else: + template = self._jinja_template("SwiftClient.jinja") + template.globals = template_globals + + self._write_output_in_target_folder(template.render(), + '{}.swift'.format(self.args.module_name)) + + def _namespace_fields(self, api): + namespace_fields = [] + for namespace in api.namespaces.values(): + if self._namespace_contains_valid_routes_for_auth_type(namespace): + namespace_fields.append((fmt_var(namespace.name), + fmt_class(self._class_name(namespace.name)))) + + return namespace_fields def _generate_routes(self, namespace): check_route_name_conflict(namespace) + if not self._namespace_contains_valid_routes_for_auth_type(namespace): + return + + template_globals = {} + template_globals['fmt_class'] = fmt_class + template_globals['class_name'] = self._class_name + template_globals['route_doc'] = self._route_doc + template_globals['route_param_docs'] = self._route_param_docs + template_globals['route_returns_doc'] = self._route_returns_doc + template_globals['route_client_args'] = self._route_client_args + template_globals['fmt_func'] = fmt_func + template_globals['deprecation_warning'] = self._deprecation_warning + template_globals['route_args'] = self._route_args + template_globals['request_object_name'] = self._request_object_name + template_globals['fmt_serial_type'] = fmt_serial_type + template_globals['is_struct_type'] = is_struct_type + template_globals['is_union_type'] = is_union_type + template_globals['fmt_var'] = fmt_var + template_globals['server_args'] = self._server_args + template_globals['fmt_type'] = fmt_type + template_globals['return_args'] = self._return_args + template_globals['transport_client_name'] = self.args.transport_client_name + template_globals['fmt_route_objc_class'] = self._fmt_route_objc_class + template_globals['route_objc_result_type'] = self._route_objc_result_type + template_globals['routes_for_objc_requests'] = self._routes_for_objc_requests + template_globals['valid_route_for_auth_type'] = self._valid_route_for_auth_type + template_globals['route_objc_func_suffix'] = self._route_objc_func_suffix + template_globals['fmt_objc_type'] = fmt_objc_type + template_globals['objc_init_args_to_swift'] = self._objc_init_args_to_swift + template_globals['objc_result_from_swift'] = self._objc_result_from_swift + template_globals['objc_no_defualts_func_args'] = self._objc_no_defualts_func_args + template_globals['objc_app_auth_route_wrapper_already_defined'] = \ + self._objc_app_auth_route_wrapper_already_defined + + ns_class = self._class_name(fmt_class(namespace.name)) + + if self.args.objc: + template = self._jinja_template("ObjCRoutes.jinja") + template.globals = template_globals + + output_from_parsed_template = template.render(namespace=namespace) + + self._write_output_in_target_folder(output_from_parsed_template, + 'DBX{}Routes.swift'.format(ns_class)) + else: + template = self._jinja_template("SwiftRoutes.jinja") + template.globals = template_globals + + output_from_parsed_template = template.render(namespace=namespace) + + self._write_output_in_target_folder(output_from_parsed_template, + '{}Routes.swift'.format(ns_class)) + + def _generate_request_boxes(self, api): + background_compatible_routes = self._background_compatible_namespace_route_pairs(api) - ns_class = fmt_class(namespace.name) - self.emit_raw(base) - self.emit('/// Routes for the {} namespace'.format(namespace.name)) + if len(background_compatible_routes) == 0: + return - with self.block('open class {}Routes'.format(ns_class)): - self.emit('public let client: {}'.format(self.args.transport_client_name)) - args = [('client', '{}'.format(self.args.transport_client_name))] + background_objc_routes = self._background_compatible_routes_for_objc_requests(api) - with self.function_block('init', self._func_args(args)): - self.emit('self.client = client') + template_globals = {} + template_globals['request_type_signature'] = self._request_type_signature + template_globals['fmt_func'] = fmt_func + template_globals['fmt_route_objc_class'] = self._fmt_route_objc_class + template_globals['fmt_func_namespace'] = fmt_func_namespace + template_globals['fmt_route_name_namespace'] = fmt_route_name_namespace + swift_class_name = '{}RequestBox'.format(self.args.class_name) - self.emit() + if self.args.objc: + template = self._jinja_template("ObjCRequestBox.jinja") + template.globals = template_globals + output = template.render( + background_compatible_routes=background_compatible_routes, + background_objc_routes=background_objc_routes, + class_name=swift_class_name + ) + + file_name = 'DBX{}RequestBox.swift'.format(self.args.class_name) + self._write_output_in_target_folder(output, + file_name) + else: + template = self._jinja_template("SwiftRequestBox.jinja") + template.globals = template_globals + + output = template.render(background_compatible_routes=background_compatible_routes, + background_objc_routes=background_objc_routes, + class_name=swift_class_name) + self._write_output_in_target_folder(output, + '{}RequestBox.swift'.format(self.args.class_name)) + + def _generate_reconnection_helpers(self, api): + background_compatible_pairs = self._background_compatible_namespace_route_pairs(api) + + if len(background_compatible_pairs) == 0: + return + + is_app_auth_client = self.args.auth_type == 'app' + class_name_prefix = 'AppAuth' if is_app_auth_client else '' + class_name = '{}ReconnectionHelpers'.format(class_name_prefix) + return_type = '{}RequestBox'.format(self.args.class_name) + + template = self._jinja_template("SwiftReconnectionHelpers.jinja") + template.globals['fmt_route_name'] = fmt_route_name + template.globals['fmt_route_name_namespace'] = fmt_route_name_namespace + template.globals['fmt_func_namespace'] = fmt_func_namespace + template.globals['fmt_func'] = fmt_func + template.globals['fmt_class'] = fmt_class + template.globals['class_name'] = class_name + template.globals['return_type'] = return_type + + output_from_parsed_template = template.render( + background_compatible_namespace_route_pairs=background_compatible_pairs + ) + + self._write_output_in_target_folder( + output_from_parsed_template, '{}.swift'.format(class_name) + ) + + def _background_compatible_namespace_route_pairs(self, api): + namespaces = api.namespaces.values() + background_compatible_routes = [] + for namespace in namespaces: for route in namespace.routes: - self._generate_route(namespace, route) + is_background_compatible = self._background_session_route_style(route) is not None + if is_background_compatible and self._valid_route_for_auth_type(route): + namespace_route_pair = (namespace, route) + background_compatible_routes.append(namespace_route_pair) + return background_compatible_routes + + def _request_type_signature(self, route): + route_style = self._background_session_route_style(route) + request_name = self._request_object_name_for_key(route_style) + rserializer_type = fmt_serial_type(route.result_data_type) + eserializer_type = fmt_serial_type(route.error_data_type) + + return '{}<{}, {}>'.format(request_name, rserializer_type, eserializer_type) + + def _jinja_template(self, template_file): + rsrc_folder = os.path.join(os.path.dirname(__file__), 'swift_rsrc') + template_loader = jinja2.FileSystemLoader(searchpath=rsrc_folder) + template_env = jinja2.Environment(loader=template_loader, + trim_blocks=True, + lstrip_blocks=True, + autoescape=False) + template = template_env.get_template(template_file) + return template + + def _valid_route_for_auth_type(self, route): + # jlocke: this is a bit of a hack to match the route grouping style of the Objective-C SDK + # in app auth situations without blowing up the current user and team auth names + + # route_auth_type can be either a string or a list of strings + route_auth_type = route.attrs.get('auth') + client_auth_type = self.args.auth_type + + # if building the app client, only include app auth routes + # if building the user or team client, include routes of all auth types except + # app auth exclusive routes + + is_app_auth_only_route = route_auth_type == 'app' + route_auth_types_include_app = 'app' in route_auth_type + + if client_auth_type == 'app': + return is_app_auth_only_route or route_auth_types_include_app + else: + return not is_app_auth_only_route - def _get_route_args(self, namespace, route): + # The objc compatibility wrapper generates a class to wrap each route providing properly + # typed completion handlers without generics. User and App clients are generated in separate + # passes, and if the wrapper is already defined for the user client, we must skip generating + # a second definition of it for the app client. + def _objc_app_auth_route_wrapper_already_defined(self, route): + client_auth_type = self.args.auth_type + is_app_auth_client = client_auth_type == 'app' + + return is_app_auth_client and route.attrs.get('auth') != 'app' + + def _namespace_contains_valid_routes_for_auth_type(self, namespace): + valid_count = 0 + for route in namespace.routes: + if self._valid_route_for_auth_type(route): + valid_count = valid_count + 1 + + return valid_count > 0 + + def _class_name(self, name): + class_name = name + + if self.args.auth_type == 'app': + class_name = class_name + "AppAuth" + + return class_name + + def _get_route_args(self, namespace, route, objc=False, include_defaults=True): data_type = route.arg_data_type - arg_type = fmt_type(data_type) + if objc is False: + arg_type = fmt_type(data_type) + else: + arg_type = fmt_objc_type(data_type) + if is_struct_type(data_type): - arg_list = self._struct_init_args(data_type, namespace=namespace) + if objc is False: + arg_list = self._struct_init_args(data_type, namespace=namespace) + else: + arg_list = self._objc_init_args(data_type, include_defaults) doc_list = [(fmt_var(f.name), self.process_doc(f.doc, self._docf) if f.doc else undocumented) for f in data_type.fields if f.doc] elif is_union_type(data_type): - arg_list = [(fmt_var(data_type.name), '{}.{}'.format( - fmt_class(namespace.name), fmt_class(data_type.name)))] + if objc is False: + arg_list = [(fmt_var(data_type.name), '{}.{}'.format( + fmt_class(namespace.name), fmt_class(data_type.name)))] + else: + arg_list = [(fmt_var(data_type.name), '{}'.format(fmt_objc_type(data_type)))] + doc_list = [(fmt_var(data_type.name), self.process_doc(data_type.doc, self._docf) if data_type.doc else 'The {} union'.format(fmt_class(data_type.name)))] @@ -182,91 +401,228 @@ def _get_route_args(self, namespace, route): doc_list = [] return arg_list, doc_list - def _emit_route(self, namespace, route, req_obj_name, extra_args=None, extra_docs=None): - arg_list, doc_list = self._get_route_args(namespace, route) - extra_args = extra_args or [] - extra_docs = extra_docs or [] + def _route_client_args(self, route): + route_type = route.attrs.get('style') + client_args = json.loads(self.args.client_args) + + if route_type not in client_args.keys(): + return [None] + else: + return client_args[route_type] - arg_type = fmt_type(route.arg_data_type) - func_name = fmt_func(route.name, route.version) + def _background_session_route_style(self, route): + route_type = route.attrs.get('style') + client_args = json.loads(self.args.client_args) + if route_type not in client_args.keys(): + return None + else: + client_arg_styles_for_route_type = map(lambda arg_data: arg_data[0], + client_args[route_type]) + route_style_if_background_session_compatible = next( + filter(lambda arg_data: arg_data in ["upload", "download_file"], + client_arg_styles_for_route_type)) + return route_style_if_background_session_compatible + + def _route_doc(self, route): if route.doc: - route_doc = self.process_doc(route.doc, self._docf) + rdoc = self.process_doc(route.doc, self._docf) else: - route_doc = 'The {} route'.format(func_name) - self.emit_wrapped_text(route_doc, prefix='/// ', width=120) - self.emit('///') + rdoc = 'The {} route'.format(fmt_func(route.name, route.version)) + return textwrap.fill(rdoc, + initial_indent='/// ', + subsequent_indent=' /// ', + break_long_words=False, + break_on_hyphens=False, + width=116) + + def _route_param_docs(self, namespace, route, args_data): + _, doc_list = self._get_route_args(namespace, route) + if args_data is not None: + _, type_data_list = tuple(args_data) + extra_docs = [(type_data[0], type_data[-1]) for type_data in type_data_list] + else: + extra_docs = [] + param_docs = [] for name, doc in doc_list + extra_docs: param_doc = '- parameter {}: {}'.format(name, doc if doc is not None else undocumented) - self.emit_wrapped_text(param_doc, prefix='/// ', width=120) - self.emit('///') - output = (' - returns: Through the response callback, the caller will ' + - 'receive a `{}` object on success or a `{}` object on failure.') - output = output.format(fmt_type(route.result_data_type), - fmt_type(route.error_data_type)) - self.emit_wrapped_text(output, prefix='/// ', width=120) - - func_args = [ - ('route', '{}.{}'.format(fmt_class(namespace.name), func_name)), - ] - client_args = [] - return_args = [('route', 'route')] + param_docs.append(textwrap.fill(param_doc, + initial_indent='/// ', + subsequent_indent=' /// ', + break_long_words=False, + break_on_hyphens=False, + width=116)) + + return param_docs + + def _route_returns_doc(self, route): + rdoc = '- returns: Through the response callback, the caller will receive a ' + rdoc = rdoc + '`{}` object on success'.format(fmt_type(route.result_data_type)) + rdoc = rdoc + ' or a `{}` object on failure.'.format(fmt_type(route.error_data_type)) + return textwrap.fill(rdoc, + initial_indent='/// ', + subsequent_indent=' /// ', + break_long_words=False, + break_on_hyphens=False, + width=116) + + def _deprecation_warning(self, route): + if route.deprecated: + msg = '{} is deprecated.'.format(fmt_func(route.name, route.version)) + if route.deprecated.by: + msg += ' Use {}.'.format( + fmt_func(route.deprecated.by.name, route.deprecated.by.version)) + return '@available(*, unavailable, message:"{}")'.format(msg) + + def _route_args(self, namespace, route, args_data, objc=False, include_defaults=True): + arg_list, _ = self._get_route_args(namespace, route, objc, include_defaults) + if args_data is not None: + _, type_data_list = tuple(args_data) + extra_args = [tuple(type_data[:-1]) for type_data in type_data_list] + if objc and not include_defaults: + extra_args = [arg for arg in extra_args] + # extra_args = [tuple(type_data[:-1]) for type_data in type_data_list] + # for name, value, type in extra_args: + # if not is_nullable_type(type): + else: + extra_args = [] - for name, value, typ in extra_args: - arg_list.append((name, typ)) - func_args.append((name, value)) - client_args.append((name, value)) + for name, _, extra_type in extra_args: + if objc and '=' in extra_type: + extra_type = extra_type.split(' = ', 1)[0] + arg_list.append((name, extra_type)) - rtype = fmt_serial_type(route.result_data_type) - etype = fmt_serial_type(route.error_data_type) + return self._func_args(arg_list, force_first=False) - self._maybe_generate_deprecation_warning(route) + def _request_object_name_for_key(self, key): + style_to_request = json.loads(self.args.style_to_request) + return style_to_request[key] - with self.function_block('@discardableResult open func {}'.format(func_name), - args=self._func_args(arg_list, force_first=False), - return_type='{}<{}, {}>'.format(req_obj_name, rtype, etype)): - self.emit('let route = {}.{}'.format(fmt_class(namespace.name), func_name)) - if is_struct_type(route.arg_data_type): - args = [(name, name) for name, _ in self._struct_init_args(route.arg_data_type)] - func_args += [('serverArgs', '{}({})'.format(arg_type, self._func_args(args)))] - self.emit('let serverArgs = {}({})'.format(arg_type, self._func_args(args))) - elif is_union_type(route.arg_data_type): - self.emit('let serverArgs = {}'.format(fmt_var(route.arg_data_type.name))) + def _request_object_name(self, route, args_data): + route_type = route.attrs.get('style') + if args_data is not None: + req_obj_key, _ = tuple(args_data) + return self._request_object_name_for_key(req_obj_key) + else: + return self._request_object_name_for_key(route_type) - if not is_void_type(route.arg_data_type): - return_args += [('serverArgs', 'serverArgs')] + def _server_args(self, route): + args = [(name, name) for name, _ in self._struct_init_args(route.arg_data_type)] + return self._func_args(args) - return_args += client_args + def _return_args(self, route, args_data): + return_args = [('route', 'route')] - txt = 'return client.request({})'.format( - self._func_args(return_args, not_init=True) - ) + if not is_void_type(route.arg_data_type): + return_args += [('serverArgs', 'serverArgs')] + + if args_data is not None: + _, type_data_list = tuple(args_data) + extra_args = [tuple(type_data[:-1]) for type_data in type_data_list] + for name, value, _ in extra_args: + return_args.append((name, value)) + + return self._func_args(return_args, not_init=True) + + def _fmt_route_objc_class(self, namespace, route, args_data): + name = 'DBX{}{}{}'.format(fmt_class(namespace.name), + fmt_class(route.name), + self._request_object_name(route, args_data)) + if route.version > 1: + name = '{}V{}'.format(name, route.version) + return name + + def _route_objc_result_type(self, route, args_data): + data_type = route.result_data_type + error_data_type = route.error_data_type + error_type = 'DBXCallError?' + if error_data_type.name != 'Void': + error_type = 'DBX{}{}?, {}'.format(fmt_class(error_data_type.namespace.name), + fmt_class(error_data_type.name), + error_type) + + if data_type.name == 'Void': + return error_type + else: + result_type = '{}?'.format(fmt_objc_type(data_type)) - self.emit(txt) - self.emit() + request_object_name = self._request_object_name(route, args_data) - def _maybe_generate_deprecation_warning(self, route): - if route.deprecated: - msg = '{} is deprecated.'.format(fmt_func(route.name, route.version)) - if route.deprecated.by: - msg += ' Use {}.'.format( - fmt_func(route.deprecated.by.name, route.deprecated.by.version)) - self.emit('@available(*, unavailable, message:"{}")'.format(msg)) + if request_object_name == 'DownloadRequestFile': + result_type = '{}, URL?'.format(result_type) + elif request_object_name == 'DownloadRequestMemory': + result_type = '{}, Data?'.format(result_type) - def _generate_route(self, namespace, route): - route_type = route.attrs.get('style') - client_args = json.loads(self.args.client_args) - style_to_request = json.loads(self.args.style_to_request) + result_type = '{}, {}'.format(result_type, error_type) + return result_type - if route_type not in client_args.keys(): - self._emit_route(namespace, route, style_to_request[route_type]) + def _background_compatible_routes_for_objc_requests(self, api): + namespaces = api.namespaces.values() + objc_class_to_route = {} + for namespace in namespaces: + for route in namespace.routes: + bg_route_style = self._background_session_route_style(route) + if bg_route_style is not None and self._valid_route_for_auth_type(route): + args_data = self._route_client_args(route)[0] + objc_class = self._fmt_route_objc_class(namespace, route, args_data) + objc_class_to_route[objc_class] = [namespace, route, args_data] + return list(objc_class_to_route.values()) + + def _routes_for_objc_requests(self, namespace): + objc_class_to_route = {} + for route in namespace.routes: + for args_data in self._route_client_args(route): + objc_class = self._fmt_route_objc_class(namespace, route, args_data) + objc_class_to_route[objc_class] = [route, args_data] + + return list(objc_class_to_route.values()) + + def _route_objc_func_suffix(self, args_data): + if args_data is not None: + _, type_data_list = tuple(args_data) + if type_data_list: + extra_args = tuple(type_data_list[-1]) + return extra_args[-2] + else: + return '' else: - for args_data in client_args[route_type]: - req_obj_key, type_data_list = tuple(args_data) - req_obj_name = style_to_request[req_obj_key] - - extra_args = [tuple(type_data[:-1]) for type_data in type_data_list] - extra_docs = [(type_data[0], type_data[-1]) for type_data in type_data_list] - - self._emit_route(namespace, route, req_obj_name, extra_args, extra_docs) + return '' + + def _objc_result_from_swift(self, result_data_type, swift_var_name='swift'): + data_type, _ = unwrap_nullable(result_data_type) + + if is_list_type(data_type): + _, prefix, suffix, list_data_type, list_nullable = mapped_list_info(data_type) + + value = swift_var_name + + if is_user_defined_type(list_data_type): + objc_type = fmt_objc_type(list_data_type, False) + factory_func = '.factory' if is_union_type(list_data_type) else '' + factory_func = '.wrapPreservingSubtypes' if datatype_has_subtypes(data_type) \ + else factory_func + value = '{}.map {}{{ {}{}(swift: $0) }}'.format(value, + prefix, + objc_type, + factory_func) + elif is_numeric_type(list_data_type): + map_func = 'compactMap' if list_nullable else 'map' + value = '{}.{} {}{{ $0 as NSNumber{} }}'.format(value, + map_func, + prefix, + '?' if list_nullable else '') + else: + return value + + value = '{}{}'.format(value, + suffix) + return value + else: + objc_data_type = fmt_objc_type(data_type) + factory_func = '.factory' if is_union_type(data_type) else '' + factory_func = '.wrapPreservingSubtypes' if datatype_has_subtypes(data_type) \ + else factory_func + return '{}{}(swift: {})'.format(objc_data_type, + factory_func, + swift_var_name) diff --git a/stone/backends/swift_helpers.py b/stone/backends/swift_helpers.py index 16d839de..69cd462b 100644 --- a/stone/backends/swift_helpers.py +++ b/stone/backends/swift_helpers.py @@ -8,13 +8,16 @@ Int32, Int64, List, + Map, String, Timestamp, UInt32, UInt64, Void, + is_struct_type, is_boolean_type, is_list_type, + is_map_type, is_numeric_type, is_string_type, is_tag_ref, @@ -35,6 +38,7 @@ Int32: 'Int32', Int64: 'Int64', List: 'Array', + Map: 'Dictionary', String: 'String', Timestamp: 'Date', UInt32: 'UInt32', @@ -42,6 +46,22 @@ Void: 'Void', } +_objc_type_table = { + Boolean: 'NSNumber', + Bytes: 'Data', + Float32: 'NSNumber', + Float64: 'NSNumber', + Int32: 'NSNumber', + Int64: 'NSNumber', + List: 'Array', + Map: 'Dictionary', + String: 'String', + Timestamp: 'Date', + UInt32: 'NSNumber', + UInt64: 'NSNumber', + Void: '', +} + _reserved_words = { 'description', 'bool', @@ -77,6 +97,8 @@ 'typealias', 'var', 'default', + 'hash', + 'client', } @@ -90,6 +112,9 @@ def fmt_obj(o): return 'nil' if o == '': return '""' + elif isinstance(o, str): + return '"{}"'.format(o) + return pprint.pformat(o, width=1) @@ -125,18 +150,36 @@ def fmt_type(data_type): if is_list_type(data_type): result = result + '<{}>'.format(fmt_type(data_type.data_type)) + if is_map_type(data_type): + result = result + '<{}, {}>'.format(fmt_type(data_type.key_data_type), + fmt_type(data_type.value_data_type)) return result if not nullable else result + '?' +def fmt_objc_type(data_type, allow_nullable=True): + data_type, nullable = unwrap_nullable(data_type) + + if is_user_defined_type(data_type): + result = 'DBX{}{}'.format(fmt_class(data_type.namespace.name), + fmt_class(data_type.name)) + else: + result = _objc_type_table.get(data_type.__class__, fmt_class(data_type.name)) + + if is_list_type(data_type): + result = result + '<{}>'.format(fmt_objc_type(data_type.data_type, False)) + elif is_map_type(data_type): + result = result + ''.format(fmt_objc_type(data_type.value_data_type)) + + return result if not nullable or not allow_nullable else result + '?' def fmt_var(name): return _format_camelcase(name) -def fmt_default_value(namespace, field): +def fmt_default_value(field): if is_tag_ref(field.default): return '{}.{}Serializer().serialize(.{})'.format( - fmt_class(namespace.name), + fmt_class(field.default.union_data_type.namespace.name), fmt_class(field.default.union_data_type.name), fmt_var(field.default.tag_name)) elif is_list_type(field.data_type): @@ -155,6 +198,18 @@ def fmt_default_value(namespace, field): raise TypeError('Can\'t handle default value type %r' % type(field.data_type)) +def fmt_route_name(route): + if route.version == 1: + return route.name + else: + return '{}_v{}'.format(route.name, route.version) + +def fmt_route_name_namespace(route, namespace_name): + return '{}/{}'.format(namespace_name, fmt_route_name(route)) + +def fmt_func_namespace(name, version, namespace_name): + return '{}_{}'.format(namespace_name, fmt_func(name, version)) + def check_route_name_conflict(namespace): """ Check name conflicts among generated route definitions. Raise a runtime exception when a @@ -169,3 +224,73 @@ def check_route_name_conflict(namespace): raise RuntimeError( 'There is a name conflict between {!r} and {!r}'.format(other_route, route)) route_by_name[route_name] = route + +def mapped_list_info(data_type): + list_data_type, list_nullable = unwrap_nullable(data_type.data_type) + list_depth = 0 + + while is_list_type(list_data_type): + list_depth += 1 + list_data_type, list_nullable = unwrap_nullable(list_data_type.data_type) + + prefix = '' + suffix = '' + if list_depth > 0: + i = 0 + while i < list_depth: + i += 1 + prefix = '{}{{ $0.map '.format(prefix) + suffix = '{} }}'.format(suffix) + + return (list_depth, prefix, suffix, list_data_type, list_nullable) + +def field_is_user_defined(field): + data_type, nullable = unwrap_nullable(field.data_type) + return is_user_defined_type(data_type) and not nullable + +def field_is_user_defined_optional(field): + data_type, nullable = unwrap_nullable(field.data_type) + return is_user_defined_type(data_type) and nullable + +def field_is_user_defined_map(field): + data_type, _ = unwrap_nullable(field.data_type) + return is_map_type(data_type) and is_user_defined_type(data_type.value_data_type) + +def field_is_user_defined_list(field): + data_type, _ = unwrap_nullable(field.data_type) + if is_list_type(data_type): + list_data_type, _ = unwrap_nullable(data_type.data_type) + return is_user_defined_type(list_data_type) + else: + return False + +# List[typing.Tuple[let_name: str, swift_type: str, objc_type: str]] +def objc_datatype_value_type_tuples(data_type): + ret = [] + + # if list type get the data type of the item + if is_list_type(data_type): + data_type = data_type.data_type + + # if map type get the data type of the value + if is_map_type(data_type): + data_type = data_type.value_data_type + + # if data_type is a struct type and has subtypes, process them into labels and types + if is_struct_type(data_type) and data_type.has_enumerated_subtypes(): + all_subtypes = data_type.get_all_subtypes_with_tags() + + for subtype in all_subtypes: + # subtype[0] is the tag name and subtype[1] is the subtype struct itself + struct = subtype[1] + case_let_name = fmt_var(struct.name) + swift_type = fmt_type(struct) + objc_type = fmt_objc_type(struct) + ret.append((case_let_name, swift_type, objc_type)) + return ret + +def field_datatype_has_subtypes(field) -> bool: + return datatype_has_subtypes(field.data_type) + +def datatype_has_subtypes(data_type) -> bool: + return len(objc_datatype_value_type_tuples(data_type)) > 0 diff --git a/stone/backends/swift_rsrc/ObjCClient.jinja b/stone/backends/swift_rsrc/ObjCClient.jinja new file mode 100644 index 00000000..1ba29c8d --- /dev/null +++ b/stone/backends/swift_rsrc/ObjCClient.jinja @@ -0,0 +1,35 @@ +/// +/// Copyright (c) 2022 Dropbox, Inc. All rights reserved. +/// +/// Auto-generated by Stone, do not modify. +/// + +import Foundation +import SwiftyDropbox + +/// Objective-C compatible {{ class_name }}. +/// For Swift see {{ class_name }}. +@objc +public class DBX{{ class_name }}: NSObject { + let swift: {{ class_name }} + + {% for var, type in namespace_fields %} + /// Routes within the {{ var }} namespace. See DB{{ type }}Routes for details. + @objc + public var {{ var }}: DBX{{ type }}Routes! + {% endfor %} + + @objc + public convenience init(client: DBXDropboxTransportClient) { + self.init(swiftClient: client.swift) + } + + public init(swiftClient: DropboxTransportClient) { + self.swift = {{ class_name }}(client: swiftClient) + + {% for var, type in namespace_fields %} + self.{{ var }} = DBX{{ type }}Routes(swift: swift.{{ var }}) + {% endfor %} + } +} + diff --git a/stone/backends/swift_rsrc/ObjCRequestBox.jinja b/stone/backends/swift_rsrc/ObjCRequestBox.jinja new file mode 100644 index 00000000..735adae1 --- /dev/null +++ b/stone/backends/swift_rsrc/ObjCRequestBox.jinja @@ -0,0 +1,25 @@ +/// +/// Copyright (c) 2022 Dropbox, Inc. All rights reserved. +/// +/// Auto-generated by Stone, do not modify. +/// + +import Foundation +import SwiftyDropbox + +extension {{ class_name }} { + var objc: DBXRequest { + {% for route_args_data in background_objc_routes %} + {% set namespace = route_args_data[0] %} + {% set route = route_args_data[1] %} + {% set args_data = route_args_data[2] %} + if case .{{ fmt_func_namespace(route.name, route.version, namespace.name) }}(let swift) = self { + return {{ fmt_route_objc_class(namespace, route, args_data) }}(swift: swift) + } + {% endfor %} + else { + fatalError("For Obj-C compatibility, add this route to the Objective-C compatibility module allow-list") + } + } +} + diff --git a/stone/backends/swift_rsrc/ObjCRoutes.jinja b/stone/backends/swift_rsrc/ObjCRoutes.jinja new file mode 100644 index 00000000..df741744 --- /dev/null +++ b/stone/backends/swift_rsrc/ObjCRoutes.jinja @@ -0,0 +1,187 @@ +{% set namespace_name = fmt_class(class_name(namespace.name)) %} +/// +/// Copyright (c) 2022 Dropbox, Inc. All rights reserved. +/// +/// Auto-generated by Stone, do not modify. +/// + +import Foundation +import SwiftyDropbox + +/// Objective-C compatible routes for the {{ namespace.name }} namespace +/// For Swift routes see {{ namespace_name }}Routes +@objc +public class DBX{{ namespace_name }}Routes: NSObject { + private let swift: {{ namespace_name }}Routes + init(swift: {{ namespace_name }}Routes) { + self.swift = swift + self.client = swift.client.objc + } + + public let client: DBX{{ transport_client_name }} + + {% for route in namespace.routes %} + {% if valid_route_for_auth_type(route) is true and not route.deprecated %} + {% for args_data in route_client_args(route) %} + {{ route_doc(route) }} + /// + {% if route.attrs.get('scope') is not none %} + /// - scope: {{ route.attrs.get('scope') }} + /// + {% endif %} + {% for route_param_doc in route_param_docs(namespace, route, args_data) %} + {{ route_param_doc }} + {% endfor %} + /// + {{ route_returns_doc(route) }} + {% set func_name = fmt_func(route.name, route.version) %} + {% set objc_class = fmt_route_objc_class(namespace, route, args_data) %} + {% set suffix = route_objc_func_suffix(args_data) %} + {% set route_args_no_defaults = route_args(namespace, route, args_data, True, False) %} + {% set route_args = route_args(namespace, route, args_data, True) %} + @objc + @discardableResult public func {{ func_name }}{{ suffix }}({{ route_args }}) -> {{ objc_class }} { + {% if is_struct_type(route.arg_data_type) %} + let swift = swift.{{ func_name }}({{ objc_init_args_to_swift(route.arg_data_type, args_data) }}) + {% elif is_union_type(route.arg_data_type) %} + let swift = swift.{{ func_name }}({{ fmt_var(route.arg_data_type.name) }}: {{ fmt_var(route.arg_data_type.name) }}.swift) + {% else %} + let swift = swift.{{ func_name }}() + {% endif %} + return {{ objc_class }}(swift: swift) + } + + {% if route_args != route_args_no_defaults %} + {{ route_doc(route) }} + /// + {% if route.attrs.get('scope') is not none %} + /// - scope: {{ route.attrs.get('scope') }} + /// + {% endif %} + {{ route_returns_doc(route) }} + @objc + @discardableResult public func {{ func_name }}{{ suffix }}({{ route_args_no_defaults }}) -> {{ objc_class }} { + {% if is_struct_type(route.arg_data_type) %} + let swift = swift.{{ func_name }}({{ objc_init_args_to_swift(route.arg_data_type, args_data, False) }}) + {% elif is_union_type(route.arg_data_type) %} + let swift = swift.{{ func_name }}({{ fmt_var(route.arg_data_type.name) }}: {{ fmt_var(route.arg_data_type.name) }}.swift) + {% else %} + let swift = swift.{{ func_name }}() + {% endif %} + return {{ objc_class }}(swift: swift) + } + + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} +} + +{% for route_args_data in routes_for_objc_requests(namespace) %} +{% set route = route_args_data[0] %} +{% if valid_route_for_auth_type(route) is true %} + +{# do not redefine DBXRequests defined by the user auth client #} +{% if not objc_app_auth_route_wrapper_already_defined(route) %} +{% set args_data = route_args_data[1] %} +{% set request_object_name = request_object_name(route, args_data) %} +{% set result_serial_type = fmt_serial_type(route.result_data_type) %} +{% set error_serial_type = fmt_serial_type(route.error_data_type) %} +{% set result_type = route_objc_result_type(route, args_data) %} +{% set objc_data_type = fmt_objc_type(route.result_data_type) %} +@objc +public class {{ fmt_route_objc_class(namespace, route, args_data) }}: NSObject, DBXRequest { + var swift: {{ request_object_name }}<{{ result_serial_type }}, {{ error_serial_type }}> + + init(swift: {{ request_object_name }}<{{ result_serial_type }}, {{ error_serial_type }}>) { + self.swift = swift + } + + @objc + @discardableResult public func response( + completionHandler: @escaping ({{ result_type }}) -> Void + ) -> Self { + self.response(queue: nil, completionHandler: completionHandler) + } + + @objc + @discardableResult public func response( + queue: DispatchQueue?, + completionHandler: @escaping ({{ result_type }}) -> Void + ) -> Self { + swift.response(queue: queue) { result, error in + {% if route.error_data_type.name != 'Void' %} + {% set error_type = 'DBX' + fmt_class(route.error_data_type.namespace.name) + fmt_class(route.error_data_type.name) %} + {% set error_call = 'routeError, callError' %} + var routeError: {{ error_type }}? + var callError: DBXCallError? + switch error { + case .routeError(let box, _, _, _): + routeError = {{ error_type }}(swift: box.unboxed) + callError = nil + default: + routeError = nil + callError = error?.objc + } + + {% else %} + {% set error_call = 'error?.objc' %} + {% endif %} + {% if route.result_data_type.name == 'Void' %} + completionHandler({{ error_call }}) + {% elif request_object_name is in ['DownloadRequestFile', 'DownloadRequestMemory'] %} + var objc: {{ objc_data_type }}? = nil + var destination: {{ 'URL?' if request_object_name == 'DownloadRequestFile' else 'Data?' }} = nil + if let swift = result { + objc = {{ objc_result_from_swift(route.result_data_type, 'swift.0') }} + destination = swift.1 + } + completionHandler(objc, destination, {{ error_call }}) + {% else %} + var objc: {{ objc_data_type }}? = nil + if let swift = result { + objc = {{ objc_result_from_swift(route.result_data_type) }} + } + completionHandler(objc, {{ error_call }}) + {% endif %} + } + return self + } + + {% if request_object_name != 'RpcRequest' %} + @objc + public func progress(_ progressHandler: @escaping ((Progress) -> Void)) -> Self { + swift.progress(progressHandler) + return self + } + + {% endif %} + @objc + public var clientPersistedString: String? { swift.clientPersistedString } + + @available(iOS 13.0, macOS 10.13, *) + @objc + public var earliestBeginDate: Date? { swift.earliestBeginDate } + + @objc + public func persistingString(string: String?) -> Self { + swift.persistingString(string: string) + return self + } + + @available(iOS 13.0, macOS 10.13, *) + @objc + public func settingEarliestBeginDate(date: Date?) -> Self { + swift.settingEarliestBeginDate(date: date) + return self + } + + @objc + public func cancel() { + swift.cancel() + } +} + +{% endif %} +{% endif %} +{% endfor %} diff --git a/stone/backends/swift_rsrc/ObjcTypes.jinja b/stone/backends/swift_rsrc/ObjcTypes.jinja new file mode 100644 index 00000000..10bd3491 --- /dev/null +++ b/stone/backends/swift_rsrc/ObjcTypes.jinja @@ -0,0 +1,152 @@ +{% set namespace_class_name = fmt_class(namespace.name) %} +/// +/// Copyright (c) 2022 Dropbox, Inc. All rights reserved. +/// +/// Auto-generated by Stone, do not modify. +/// + +import Foundation +import SwiftyDropbox + +/// Objective-C compatible datatypes for the {{ namespace.name }} namespace +/// For Swift see {{ namespace.name }} + +{% for data_type in namespace.linearize_data_types() %} +{% set data_type_class_name = fmt_class(data_type.name) %} +{% set swift_var_name = objc_swift_var_name(data_type) %} +{% set swift_type = namespace_class_name + '.' + data_type_class_name %} +{{ data_objc_type_doc(data_type) }} +{% if is_struct_type(data_type) %} +@objc +public class DBX{{ namespace_class_name }}{{ data_type_class_name }}: {{ 'NSObject' if not data_type.parent_type else fmt_objc_type(data_type.parent_type) }} { + {% for field in data_type.fields %} + {{ struct_field_doc(field, ' ') }} + @objc + {% if field_datatype_has_subtypes(field) %} + public var {{ fmt_var(field.name) }}: {{ fmt_objc_type(field.data_type) }} { + {% if (field_is_user_defined(field)) or (field_is_user_defined_optional(field)) %} + {% if field_is_user_defined_optional(field) %} + return {{ swift_var_name }}.{{ fmt_var(field.name) }}.flatMap { {{ fmt_objc_type(field.data_type, False) }}.wrapPreservingSubtypes(swift: $0) } + {% else %} + return {{ fmt_objc_type(field.data_type) }}.wrapPreservingSubtypes(swift: {{ swift_var_name }}.{{ fmt_var(field.name) }}) + {% endif %} + {% elif (field_is_user_defined_map(field)) or (field_is_user_defined_list(field)) %} + {{ swift_var_name }}.{{ fmt_var(field.name) }}.{{ 'mapValues' if field_is_user_defined_map(field) else 'map' }} { + return {{ fmt_objc_type(field.data_type.data_type) }}.wrapPreservingSubtypes(swift: $0) + } + {% endif %} + } + {% else %} + public var {{ fmt_var(field.name) }}: {{ fmt_objc_type(field.data_type) }} { {{ objc_return_field_value_oneliner(data_type, field) }} } + {% endif %} + {% endfor %} + {% if data_type.fields %} + + @objc + public init({{ func_args(objc_init_args(data_type)) }}) { + {% if data_type.parent_type %} + let swift = {{ swift_type }}({{ objc_init_args_to_swift(data_type) }}) + self.{{ swift_var_name }} = swift + super.init(swift: swift) + {% else %} + self.{{ swift_var_name }} = {{ swift_type }}({{ objc_init_args_to_swift(data_type) }}) + {% endif %} + } + {% endif %} + + let {{ swift_var_name }}: {{ swift_type }} + + public init(swift: {{ swift_type }}) { + self.{{ swift_var_name }} = swift + {% if data_type.parent_type %} + super.init(swift: swift) + {% endif %} + } + + {% if objc_datatype_value_type_tuples(data_type)|length > 0 %} + public static func wrapPreservingSubtypes(swift: {{ swift_type }}) -> DBX{{ namespace_class_name }}{{ data_type_class_name }} { + switch swift { + {% for tuple in objc_datatype_value_type_tuples(data_type) %} + case let {{ tuple[0] }} as {{ tuple[1] }}: + return {{ tuple[2] }}(swift: {{ tuple[0] }}) + {% endfor %} + default: + return DBX{{ namespace_class_name }}{{ data_type_class_name }}(swift: swift) + } + } + {% endif %} + + @objc + public override var description: String { {{ 'swift' if not data_type.parent_type else 'subSwift' }}.description } +} + +{% elif is_union_type(data_type) %} +{% set union_class_name = 'DBX' + namespace_class_name + data_type_class_name %} +{% set swift_enum = namespace_class_name + '.' + fmt_class(data_type.name) %} +@objc +public class {{ union_class_name }}: NSObject { + let swift: {{ swift_enum }} + + public init(swift: {{ swift_enum }}) { + self.swift = swift + } + + public static func factory(swift: {{ swift_enum }}) -> {{ union_class_name }} { + switch swift { + {% for field in data_type.all_fields %} + {% set field_class_name = fmt_class(field.name) %} + {% set field_subclass = union_class_name + field_class_name %} + {% set guard = union_swift_arg_guard(field, field_subclass) %} + {% set tag_type = fmt_objc_type(field.data_type) %} + case .{{ fmt_var(field.name) }}{{ '(let swiftArg)' if tag_type else '' }}: + {% if tag_type %} + {% if guard %} + {{ guard }} + {% endif %} + let arg = {{ swift_union_arg_to_objc(field) }} + {% endif %} + return {{ field_subclass }}({{ 'arg' if tag_type else '' }}) + {% endfor %} + } + } + + @objc + public override var description: String { swift.description } + {% for field in data_type.all_fields %} + {% set field_class_name = fmt_class(field.name) %} + {% set field_subclass = union_class_name + field_class_name %} + + @objc + public var as{{ field_class_name }}: {{ field_subclass }}? { + return self as? {{ field_subclass }} + } + {% endfor %} +} + +{% for field in data_type.all_fields %} +{% set field_class_name = fmt_class(field.name) %} +{% set field_var_name = fmt_var(field.name) %} +{% set swift_enum_case = swift_enum + '.' + field_var_name %} +{% set tag_type = fmt_objc_type(field.data_type) %} +{{ union_field_doc(field) }} +@objc +public class {{ union_class_name }}{{ field_class_name }}: {{ union_class_name }} { + {% if tag_type %} + @objc + public var {{ field_var_name }}: {{ tag_type }} + + {% endif %} + @objc + public init({{ '_ arg: '+tag_type if tag_type else '' }}) { + {% if tag_type %} + {{ field_var_name }} = arg + {% endif %} + let swift = {{ swift_enum_case }}{{ objc_union_arg(field) }} + super.init(swift: swift) + } +} + +{% endfor %} +{% endif %} +{% endfor %} + diff --git a/stone/backends/swift_rsrc/StoneBase.swift b/stone/backends/swift_rsrc/StoneBase.swift index e81d5708..c9077672 100644 --- a/stone/backends/swift_rsrc/StoneBase.swift +++ b/stone/backends/swift_rsrc/StoneBase.swift @@ -6,7 +6,32 @@ import Foundation // The objects in this file are used by generated code and should not need to be invoked manually. -open class Route { +public enum RouteAuth: String { + case app + case user + case team + case noauth +} + +public enum RouteHost: String { + case api + case content + case notify +} + +public enum RouteStyle: String { + case rpc + case upload + case download +} + +public struct RouteAttributes { + let auth: [RouteAuth] + let host: RouteHost + let style: RouteStyle +} + +public class Route { public let name: String public let version: Int32 public let namespace: String @@ -14,11 +39,11 @@ open class Route JSON { - +public class SerializeUtil { + public class func objectToJSON(_ json: AnyObject) throws -> JSON { switch json { case _ as NSNull: return .null @@ -27,17 +26,17 @@ open class SerializeUtil { case let dict as [String: AnyObject]: var ret = [String: JSON]() for (k, v) in dict { - ret[k] = objectToJSON(v) + ret[k] = try objectToJSON(v) } return .dictionary(ret) case let array as [AnyObject]: - return .array(array.map(objectToJSON)) + return try .array(array.map(objectToJSON)) default: - fatalError("Unknown type trying to parse JSON.") + throw JSONSerializerError.unknownTypeOfJSON(json: json) } } - open class func prepareJSONForSerialization(_ json: JSON) -> AnyObject { + public class func prepareJSONForSerialization(_ json: JSON) -> AnyObject { switch json { case .array(let array): return array.map(prepareJSONForSerialization) as AnyObject @@ -62,51 +61,59 @@ open class SerializeUtil { } } - open class func dumpJSON(_ json: JSON) -> Data? { + public class func dumpJSON(_ json: JSON) throws -> Data? { switch json { case .null: return "null".data(using: String.Encoding.utf8, allowLossyConversion: false) default: let obj: AnyObject = prepareJSONForSerialization(json) if JSONSerialization.isValidJSONObject(obj) { - return try! JSONSerialization.data(withJSONObject: obj, options: JSONSerialization.WritingOptions()) + return try JSONSerialization.data(withJSONObject: obj, options: JSONSerialization.WritingOptions()) } else { - fatalError("Invalid JSON toplevel type") + throw JSONSerializerError.invalidTopLevelType(json: json, object: obj) } } } - open class func parseJSON(_ data: Data) -> JSON { - let obj: AnyObject = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as AnyObject - return objectToJSON(obj) + public class func parseJSON(_ data: Data) throws -> JSON { + let obj: AnyObject = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as AnyObject + return try objectToJSON(obj) } } - public protocol JSONSerializer { associatedtype ValueType - func serialize(_: ValueType) -> JSON - func deserialize(_: JSON) -> ValueType + func serialize(_: ValueType) throws -> JSON + func deserialize(_: JSON) throws -> ValueType } -open class VoidSerializer: JSONSerializer { - open func serialize(_ value: Void) -> JSON { +enum JSONSerializerError: Error { + case unknownTypeOfJSON(json: AnyObject) + case invalidTopLevelType(json: JSON, object: AnyObject) + case deserializeError(type: T.Type, json: JSON) + case missingOrMalformedFields(json: JSON) + case missingOrMalformedTag(dict: [String: JSON], tag: JSON?) + case unknownTag(type: T.Type, json: JSON, tag: String) + case unexpectedSubtype(type: T.Type, subtype: Any) +} + +public class VoidSerializer: JSONSerializer { + public func serialize(_ value: Void) throws -> JSON { return .null } - open func deserialize(_ json: JSON) -> Void { + public func deserialize(_ json: JSON) throws -> Void { switch json { case .null: return default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } - -open class ArraySerializer: JSONSerializer { +public class ArraySerializer: JSONSerializer { var elementSerializer: T @@ -114,36 +121,58 @@ open class ArraySerializer: JSONSerializer { self.elementSerializer = elementSerializer } - open func serialize(_ arr: Array) -> JSON { - return .array(arr.map { self.elementSerializer.serialize($0) }) + public func serialize(_ arr: Array) throws -> JSON { + return .array(try arr.map { try self.elementSerializer.serialize($0) }) } - open func deserialize(_ json: JSON) -> Array { + public func deserialize(_ json: JSON) throws -> Array { switch json { case .array(let arr): - return arr.map { self.elementSerializer.deserialize($0) } + return try arr.map { try self.elementSerializer.deserialize($0) } default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class StringSerializer: JSONSerializer { - open func serialize(_ value: String) -> JSON { +public class DictionarySerializer: JSONSerializer { + + var valueSerializer: T + + init(_ elementSerializer: T) { + self.valueSerializer = elementSerializer + } + + public func serialize(_ dict: Dictionary) throws -> JSON { + return .dictionary(try dict.mapValues { try self.valueSerializer.serialize($0) }) + } + + public func deserialize(_ json: JSON) throws ->Dictionary { + switch json { + case .dictionary(let dict): + return try dict.mapValues { try self.valueSerializer.deserialize($0) } + default: + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) + } + } +} + +public class StringSerializer: JSONSerializer { + public func serialize(_ value: String) throws -> JSON { return .str(value) } - open func deserialize(_ json: JSON) -> String { + public func deserialize(_ json: JSON) throws -> String { switch (json) { case .str(let s): return s default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class NSDateSerializer: JSONSerializer { +public class NSDateSerializer: JSONSerializer { var dateFormatter: DateFormatter @@ -243,125 +272,146 @@ open class NSDateSerializer: JSONSerializer { self.dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = self.convertFormat(dateFormat) } - open func serialize(_ value: Date) -> JSON { + public func serialize(_ value: Date) throws -> JSON { return .str(self.dateFormatter.string(from: value)) } - open func deserialize(_ json: JSON) -> Date { + public func deserialize(_ json: JSON) throws -> Date { switch json { case .str(let s): - return self.dateFormatter.date(from: s)! + guard let date = self.dateFormatter.date(from: s) else { + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) + } + return date default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class BoolSerializer: JSONSerializer { - open func serialize(_ value: Bool) -> JSON { +public class BoolSerializer: JSONSerializer { + public func serialize(_ value: Bool) throws -> JSON { return .number(NSNumber(value: value as Bool)) } - open func deserialize(_ json: JSON) -> Bool { + public func deserialize(_ json: JSON) throws -> Bool { switch json { case .number(let b): return b.boolValue default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class UInt64Serializer: JSONSerializer { - open func serialize(_ value: UInt64) -> JSON { +public class UInt64Serializer: JSONSerializer { + public func serialize(_ value: UInt64) throws -> JSON { return .number(NSNumber(value: value as UInt64)) } - open func deserialize(_ json: JSON) -> UInt64 { + public func deserialize(_ json: JSON) throws -> UInt64 { switch json { case .number(let n): return n.uint64Value default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class Int64Serializer: JSONSerializer { - open func serialize(_ value: Int64) -> JSON { +public class Int64Serializer: JSONSerializer { + public func serialize(_ value: Int64) throws -> JSON { return .number(NSNumber(value: value as Int64)) } - open func deserialize(_ json: JSON) -> Int64 { + public func deserialize(_ json: JSON) throws -> Int64 { switch json { case .number(let n): return n.int64Value default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class Int32Serializer: JSONSerializer { - open func serialize(_ value: Int32) -> JSON { +public class Int32Serializer: JSONSerializer { + public func serialize(_ value: Int32) throws -> JSON { return .number(NSNumber(value: value as Int32)) } - open func deserialize(_ json: JSON) -> Int32 { + public func deserialize(_ json: JSON) throws -> Int32 { switch json { case .number(let n): return n.int32Value default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class UInt32Serializer: JSONSerializer { - open func serialize(_ value: UInt32) -> JSON { +public class UInt32Serializer: JSONSerializer { + public func serialize(_ value: UInt32) throws -> JSON { return .number(NSNumber(value: value as UInt32)) } - open func deserialize(_ json: JSON) -> UInt32 { + public func deserialize(_ json: JSON) throws -> UInt32 { switch json { case .number(let n): return n.uint32Value default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class NSDataSerializer: JSONSerializer { - open func serialize(_ value: Data) -> JSON { +public class NSDataSerializer: JSONSerializer { + public func serialize(_ value: Data) throws -> JSON { return .str(value.base64EncodedString(options: [])) } - open func deserialize(_ json: JSON) -> Data { + public func deserialize(_ json: JSON) throws -> Data { switch(json) { case .str(let s): - return Data(base64Encoded: s, options: [])! + guard let data = Data(base64Encoded: s, options: []) else { + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) + } + return data + default: + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) + } + } +} + +public class FloatSerializer: JSONSerializer { + public func serialize(_ value: Float) throws -> JSON { + return .number(NSNumber(value: value as Float)) + } + + public func deserialize(_ json: JSON) throws -> Float { + switch json { + case .number(let n): + return n.floatValue default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class DoubleSerializer: JSONSerializer { - open func serialize(_ value: Double) -> JSON { +public class DoubleSerializer: JSONSerializer { + public func serialize(_ value: Double) throws -> JSON { return .number(NSNumber(value: value as Double)) } - open func deserialize(_ json: JSON) -> Double { + public func deserialize(_ json: JSON) throws -> Double { switch json { case .number(let n): return n.doubleValue default: - fatalError("Type error deserializing") + throw JSONSerializerError.deserializeError(type: ValueType.self, json: json) } } } -open class NullableSerializer: JSONSerializer { +public class NullableSerializer: JSONSerializer { var internalSerializer: T @@ -369,20 +419,20 @@ open class NullableSerializer: JSONSerializer { self.internalSerializer = serializer } - open func serialize(_ value: Optional) -> JSON { + public func serialize(_ value: Optional) throws -> JSON { if let v = value { - return internalSerializer.serialize(v) + return try internalSerializer.serialize(v) } else { return .null } } - open func deserialize(_ json: JSON) -> Optional { + public func deserialize(_ json: JSON) throws -> Optional { switch json { case .null: return nil default: - return internalSerializer.deserialize(json) + return try internalSerializer.deserialize(json) } } } @@ -397,19 +447,25 @@ struct Serialization { static var _VoidSerializer = VoidSerializer() static var _NSDataSerializer = NSDataSerializer() + static var _FloatSerializer = FloatSerializer() static var _DoubleSerializer = DoubleSerializer() - static func getFields(_ json: JSON) -> [String: JSON] { + static func getFields(_ json: JSON) throws -> [String: JSON] { switch json { case .dictionary(let dict): return dict default: - fatalError("Type error") + throw JSONSerializerError.missingOrMalformedFields(json: json) } } - static func getTag(_ d: [String: JSON]) -> String { - return _StringSerializer.deserialize(d[".tag"]!) + static func getTag(_ d: [String: JSON]) throws -> String { + let tag = d[".tag"] + switch tag { + case .str(let str): + return str + default: + throw JSONSerializerError.missingOrMalformedTag(dict: d, tag: tag) + } } - } diff --git a/stone/backends/swift_rsrc/SwiftClient.jinja b/stone/backends/swift_rsrc/SwiftClient.jinja new file mode 100644 index 00000000..29a7a389 --- /dev/null +++ b/stone/backends/swift_rsrc/SwiftClient.jinja @@ -0,0 +1,25 @@ +/// +/// Copyright (c) 2016 Dropbox, Inc. All rights reserved. +/// +/// Auto-generated by Stone, do not modify. +/// + +import Foundation + +public class {{ class_name }}: DropboxTransportClientOwning { + public var client: {{ transport_client_name }} + + {% for var, type in namespace_fields %} + /// Routes within the {{ var }} namespace. See {{ type }}Routes for details. + public var {{ var }}: {{ type }}Routes! + {% endfor %} + + public required init(client: {{ transport_client_name }}) { + self.client = client + + {% for var, type in namespace_fields %} + self.{{ var }} = {{ type }}Routes(client: client) + {% endfor %} + } +} + diff --git a/stone/backends/swift_rsrc/SwiftReconnectionHelpers.jinja b/stone/backends/swift_rsrc/SwiftReconnectionHelpers.jinja new file mode 100644 index 00000000..83e894f1 --- /dev/null +++ b/stone/backends/swift_rsrc/SwiftReconnectionHelpers.jinja @@ -0,0 +1,35 @@ +/// +/// Copyright (c) 2022 Dropbox, Inc. All rights reserved. +/// + +import Foundation + +// The case string below must match those created by ReconnectionHelpers+Handwritten.swift using +// the route name and namespace as formatted for the generated `Route` object in SwiftTypes.jinja +// Format: "/" e.g., "files/upload_session/append_v2" for Files.uploadSessionAppendV2 +enum {{ class_name }}: ReconnectionHelpersShared { + + static func rebuildRequest(apiRequest: ApiRequest, client: DropboxTransportClientInternal) throws -> {{ return_type }} { + let info = try persistedRequestInfo(from: apiRequest) + + switch info.namespaceRouteName { + {% for route_args_data in background_compatible_namespace_route_pairs %} + {% set namespace = route_args_data[0] %} + {% set route = route_args_data[1] %} + {% set args_data = route_args_data[2] %} + case "{{ fmt_route_name_namespace(route, namespace.name) }}": + return .{{ fmt_func_namespace(route.name, route.version, namespace.name) }}( + rebuildRequest( + apiRequest: apiRequest, + info: info, + route: {{ fmt_class(namespace.name) }}.{{ fmt_func(route.name, route.version) }}, + client: client + ) + ) + {% endfor %} + default: + throw ReconnectionErrorKind.missingReconnectionCase + } + } +} + diff --git a/stone/backends/swift_rsrc/SwiftRequestBox.jinja b/stone/backends/swift_rsrc/SwiftRequestBox.jinja new file mode 100644 index 00000000..a24e68c8 --- /dev/null +++ b/stone/backends/swift_rsrc/SwiftRequestBox.jinja @@ -0,0 +1,27 @@ +/// +/// Copyright (c) 2022 Dropbox, Inc. All rights reserved. +/// +/// Auto-generated by Stone, do not modify. +/// + +import Foundation + +/// Allows for heterogenous collections of typed requests +public enum {{ class_name }}: CustomStringConvertible { + {% for route_namespace_pair in background_compatible_routes %} + {% set namespace = route_namespace_pair[0] %} + {% set route = route_namespace_pair[1] %} + case {{ fmt_func_namespace(route.name, route.version, namespace.name) }}({{ request_type_signature(route) }}) + {% endfor %} + + public var description: String { + switch self { + {% for route_namespace_pair in background_compatible_routes %} + {% set namespace = route_namespace_pair[0] %} + {% set route = route_namespace_pair[1] %} + case .{{ fmt_func_namespace(route.name, route.version, namespace.name) }}: + return "{{ fmt_route_name_namespace(route, namespace.name) }}" + {% endfor %} + } + } +} diff --git a/stone/backends/swift_rsrc/SwiftRoutes.jinja b/stone/backends/swift_rsrc/SwiftRoutes.jinja new file mode 100644 index 00000000..89303e0b --- /dev/null +++ b/stone/backends/swift_rsrc/SwiftRoutes.jinja @@ -0,0 +1,48 @@ +/// +/// Copyright (c) 2016 Dropbox, Inc. All rights reserved. +/// +/// Auto-generated by Stone, do not modify. +/// + +import Foundation + +/// Routes for the {{ class_name(namespace.name) }} namespace +/// For Objective-C compatible routes see DB{{ fmt_class(namespace.name) }}Routes +public class {{ fmt_class(class_name(namespace.name)) }}Routes: DropboxTransportClientOwning { + public let client: {{ transport_client_name }} + required init(client: {{ transport_client_name }}) { + self.client = client + } + + {% for route in namespace.routes %} + {% if valid_route_for_auth_type(route) %} + {% for args_data in route_client_args(route) %} + {{ route_doc(route) }} + /// + {% if route.attrs.get('scope') is not none %} + /// - scope: {{ route.attrs.get('scope') }} + /// + {% endif %} + {% for route_param_doc in route_param_docs(namespace, route, args_data) %} + {{ route_param_doc }} + {% endfor %} + /// + {{ route_returns_doc(route) }} + {% if route.deprecated %} + {{ deprecation_warning(route) }} + {% endif %} + @discardableResult public func {{ fmt_func(route.name, route.version) }}({{ route_args(namespace, route, args_data) }}) -> {{ request_object_name(route, args_data) }}<{{ fmt_serial_type(route.result_data_type) }}, {{ fmt_serial_type(route.error_data_type) }}> { + let route = {{ fmt_class(namespace.name) }}.{{ fmt_func(route.name, route.version) }} + {% if is_struct_type(route.arg_data_type) %} + let serverArgs = {{ fmt_type(route.arg_data_type) }}({{ server_args(route) }}) + {% elif is_union_type(route.arg_data_type) %} + let serverArgs = {{ fmt_var(route.arg_data_type.name) }} + {% endif %} + return client.request({{ return_args(route, args_data) }}) + } + + {% endfor %} + {% endif %} + {% endfor %} +} + diff --git a/stone/backends/swift_rsrc/SwiftTypes.jinja b/stone/backends/swift_rsrc/SwiftTypes.jinja new file mode 100644 index 00000000..8e4c87e0 --- /dev/null +++ b/stone/backends/swift_rsrc/SwiftTypes.jinja @@ -0,0 +1,193 @@ +/// +/// Copyright (c) 2016 Dropbox, Inc. All rights reserved. +/// +/// Auto-generated by Stone, do not modify. +/// + +import Foundation + +/// Datatypes and serializers for the {{ namespace.name }} namespace +public class {{ fmt_class(namespace.name) }} { +{% for data_type in namespace.linearize_data_types() %} + {{ data_type_doc(data_type) }} + {% if is_struct_type(data_type) %} + public class {{ fmt_class(data_type.name) }}: {{ 'CustomStringConvertible, JSONRepresentable' if not data_type.parent_type else fmt_type(data_type.parent_type) }} { + {% for field in data_type.fields %} + {{ struct_field_doc(field) }} + public let {{ fmt_var(field.name) }}: {{ fmt_type(field.data_type) }} + {% endfor %} + {% if data_type.fields %} + public init({{ func_args(struct_init_args(data_type)) }}) { + {% for field in data_type.fields %} + {% if determine_validator_type(field.data_type, fmt_var(field.name)) %} + {{ determine_validator_type(field.data_type, fmt_var(field.name)) }}({{ fmt_var(field.name) }}) + {% endif %} + self.{{ fmt_var(field.name) }} = {{ fmt_var(field.name) }} + {% endfor %} + {% if data_type.parent_type %} + super.init({{ field_name_args(data_type.parent_type) }}) + {% endif %} + } + {% endif %} + + {% if not data_type.parent_type %} + func json() throws -> JSON { + try {{ fmt_class(data_type.name) }}Serializer().serialize(self) + } + {% endif %} + + {{ 'public var' if not data_type.parent_type else 'public override var' }} description: String { + do { + return "\(SerializeUtil.prepareJSONForSerialization(try {{ fmt_class(data_type.name) }}Serializer().serialize(self)))" + } catch { + return "Failed to generate description for {{ fmt_class(data_type.name) }}: \(error)" + } + } + } + public class {{ fmt_class(data_type.name) }}Serializer: JSONSerializer { + public init() { } + public func serialize(_ value: {{ fmt_class(data_type.name) }}) throws -> JSON { + {% if data_type.all_fields %} + {{ 'var' if data_type.has_enumerated_subtypes() else 'let' }} output = [ + {% for field in data_type.all_fields %} + "{{ field.name }}": try {{ fmt_serial_obj(field.data_type) }}.serialize(value.{{ fmt_var(field.name) }}), + {% endfor %} + ] + {% else %} + {{ 'var' if data_type.has_enumerated_subtypes() else 'let' }} output = [String: JSON]() + {% endif %} + {% if data_type.has_enumerated_subtypes() %} + switch value { + {% for tags, subtype in data_type.get_all_subtypes_with_tags() if tags %} + case let {{ fmt_var(tags[0]) }} as {{ fmt_type(subtype) }}: + for (k, v) in try Serialization.getFields({{ fmt_serial_obj(subtype) }}.serialize({{ fmt_var(tags[0]) }})) { + output[k] = v + } + output[".tag"] = .str("{{ tags[0] }}") + {% endfor %} + default: + throw JSONSerializerError.unexpectedSubtype(type: {{ fmt_class(data_type.name) }}.self, subtype: value) + } + {% endif %} + return .dictionary(output) + } + public func deserialize(_ json: JSON) throws -> {{ fmt_class(data_type.name) }} { + switch json { + case .dictionary({{ "let dict" if data_type.all_fields or data_type.has_enumerated_subtypes() else "_" }}): + {% if data_type.has_enumerated_subtypes() %} + let tag = try Serialization.getTag(dict) + switch tag { + {% for tags, subtype in data_type.get_all_subtypes_with_tags() if tags %} + case "{{ tags[0] }}": + return try {{ fmt_serial_obj(subtype) }}.deserialize(json) + {% endfor %} + default: + {% if data_type.is_catch_all() %} + {% for field in data_type.all_fields %} + let {{ fmt_var(field.name) }} = try {{ fmt_serial_obj(field.data_type) }}.deserialize(dict["{{ field.name }}"] ?? {{ fmt_default_value(field) if field.has_default else '.null' }}) + {% endfor %} + return {{ fmt_class(data_type.name) }}({{ field_name_args(data_type) }}) + {% else %} + throw JSONSerializerError.unknownTag(type: {{ fmt_class(data_type.name) }}.self, json: json, tag: tag) + {% endif %} + } + {% else %} + {% for field in data_type.all_fields %} + let {{ fmt_var(field.name) }} = try {{ fmt_serial_obj(field.data_type) }}.deserialize(dict["{{ field.name }}"] ?? {{ fmt_default_value(field) if field.has_default else '.null' }}) + {% endfor %} + return {{ fmt_class(data_type.name) }}({{ field_name_args(data_type) }}) + {% endif %} + default: + throw JSONSerializerError.deserializeError(type: {{ fmt_class(data_type.name) }}.self, json: json) + } + } + } + {% elif is_union_type(data_type) %} + public enum {{ fmt_class(data_type.name) }}: CustomStringConvertible, JSONRepresentable { + {% for field in data_type.all_fields %} + {{ union_field_doc(field) }} + case {{ fmt_var(field.name) }}{{ format_tag_type(field.data_type) }} + {% endfor %} + + func json() throws -> JSON { + try {{ fmt_class(data_type.name) }}Serializer().serialize(self) + } + + public var description: String { + do { + return "\(SerializeUtil.prepareJSONForSerialization(try {{ fmt_class(data_type.name) }}Serializer().serialize(self)))" + } catch { + return "Failed to generate description for {{ fmt_class(data_type.name) }}: \(error)" + } + } + } + public class {{ fmt_class(data_type.name) }}Serializer: JSONSerializer { + public init() { } + public func serialize(_ value: {{ fmt_class(data_type.name) }}) throws -> JSON { + switch value { + {% for field in data_type.all_fields %} + case .{{ fmt_var(field.name) }}{{ '' if is_void_type(field.data_type) else '(let arg)' }}: + {% if is_void_type(field.data_type) %} + var d = [String: JSON]() + {% elif is_struct_type(field.data_type) and not field.data_type.has_enumerated_subtypes() %} + var d = try Serialization.getFields({{ fmt_serial_obj(field.data_type) }}.serialize(arg)) + {% else %} + var d = try ["{{ field.name }}": {{ fmt_serial_obj(field.data_type) }}.serialize(arg)] + {% endif %} + d[".tag"] = .str("{{ field.name }}") + return .dictionary(d) + {% endfor %} + } + } + public func deserialize(_ json: JSON) throws -> {{ fmt_class(data_type.name) }} { + switch json { + case .dictionary(let d): + let tag = try Serialization.getTag(d) + switch tag { + {% for field in data_type.all_fields %} + case "{{ field.name }}": + {% if is_void_type(field.data_type) %} + return {{ tag_type(data_type, field) }} + {% else %} + {% if is_struct_type(field.data_type) and not field.data_type.has_enumerated_subtypes() %} + let v = try {{ fmt_serial_obj(field.data_type) }}.deserialize(json) + {% else %} + let v = try {{ fmt_serial_obj(field.data_type) }}.deserialize(d["{{field.name}}"] ?? .null) + {% endif %} + return {{ tag_type(data_type, field) }}(v) + {% endif %} + {% endfor %} + default: + {% if data_type.catch_all_field %} + return {{ tag_type(data_type, data_type.catch_all_field) }} + {% else %} + throw JSONSerializerError.unknownTag(type: {{ fmt_class(data_type.name) }}.self, json: json, tag: tag) + {% endif %} + } + default: + throw JSONSerializerError.deserializeError(type: {{ fmt_class(data_type.name) }}.self, json: json) + } + } + } + {% endif %} + +{% endfor %} +{% if namespace.routes %} + + /// Stone Route Objects + + {% for route in namespace.routes %} + static let {{ fmt_func(route.name, route.version) }} = Route( + name: "{{ fmt_route_name(route) }}", + version: {{ route.version }}, + namespace: "{{ namespace.name }}", + deprecated: {{ 'true' if route.deprecated is not none else 'false' }}, + argSerializer: {{ fmt_serial_obj(route.arg_data_type) }}, + responseSerializer: {{ fmt_serial_obj(route.result_data_type) }}, + errorSerializer: {{ fmt_serial_obj(route.error_data_type) }}, + attributes: RouteAttributes({{ route_schema_attrs(route_schema, route) }}) + ) + {% endfor %} +{% endif %} +} + diff --git a/stone/backends/swift_types.py b/stone/backends/swift_types.py index 6c67b9c2..f07851ca 100644 --- a/stone/backends/swift_types.py +++ b/stone/backends/swift_types.py @@ -1,16 +1,18 @@ import json import os import shutil -from contextlib import contextmanager import six +import jinja2 +import textwrap from stone.backends.swift import ( - base, fmt_serial_obj, SwiftBaseBackend, undocumented, + _nsnumber_type_table, ) + from stone.backends.swift_helpers import ( check_route_name_conflict, fmt_class, @@ -18,7 +20,17 @@ fmt_func, fmt_var, fmt_type, + fmt_route_name, + fmt_objc_type, + mapped_list_info, + field_is_user_defined, + field_is_user_defined_optional, + field_is_user_defined_map, + field_is_user_defined_list, + objc_datatype_value_type_tuples, + field_datatype_has_subtypes ) + from stone.ir import ( is_list_type, is_numeric_type, @@ -27,6 +39,9 @@ is_union_type, is_void_type, unwrap_nullable, + is_user_defined_type, + is_boolean_type, + is_map_type ) _MYPY = False @@ -43,7 +58,17 @@ 'given route; use {ns} as a placeholder for namespace name and ' '{route} for the route name.'), ) - +_cmdline_parser.add_argument( + '--objc', + action='store_true', + help='Generate the Objective-C compatibile files', +) +_cmdline_parser.add_argument( + '-d', + '--documentation', + action='store_true', + help=('Sets whether documentation is generated.'), +) class SwiftTypesBackend(SwiftBaseBackend): """ @@ -54,16 +79,16 @@ class SwiftTypesBackend(SwiftBaseBackend): Endpoint argument (struct): ``` - open class CopyArg: CustomStringConvertible { - open let fromPath: String - open let toPath: String + public class CopyArg: CustomStringConvertible { + public let fromPath: String + public let toPath: String public init(fromPath: String, toPath: String) { stringValidator(pattern: "/(.|[\\r\\n])*")(value: fromPath) self.fromPath = fromPath stringValidator(pattern: "/(.|[\\r\\n])*")(value: toPath) self.toPath = toPath } - open var description: String { + public var description: String { return "\\(SerializeUtil.prepareJSONForSerialization( CopyArgSerializer().serialize(self)))" } @@ -73,11 +98,11 @@ class SwiftTypesBackend(SwiftBaseBackend): Endpoint error (union): ``` - open enum CopyError: CustomStringConvertible { + public enum CopyError: CustomStringConvertible { case TooManyFiles case Other - open var description: String { + public var description: String { return "\\(SerializeUtil.prepareJSONForSerialization( CopyErrorSerializer().serialize(self)))" } @@ -87,16 +112,16 @@ class SwiftTypesBackend(SwiftBaseBackend): Argument serializer (error serializer not listed): ``` - open class CopyArgSerializer: JSONSerializer { + public class CopyArgSerializer: JSONSerializer { public init() { } - open func serialize(value: CopyArg) -> JSON { + public func serialize(value: CopyArg) -> JSON { let output = [ "from_path": Serialization.serialize(value.fromPath), "to_path": Serialization.serialize(value.toPath), ] return .Dictionary(output) } - open func deserialize(json: JSON) -> CopyArg { + public func deserialize(json: JSON) -> CopyArg { switch json { case .Dictionary(let dict): let fromPath = Serialization.deserialize(dict["from_path"] ?? .Null) @@ -113,96 +138,142 @@ class SwiftTypesBackend(SwiftBaseBackend): cmdline_parser = _cmdline_parser def generate(self, api): rsrc_folder = os.path.join(os.path.dirname(__file__), 'swift_rsrc') - self.logger.info('Copying StoneValidators.swift to output folder') - shutil.copy(os.path.join(rsrc_folder, 'StoneValidators.swift'), - self.target_folder_path) - self.logger.info('Copying StoneSerializers.swift to output folder') - shutil.copy(os.path.join(rsrc_folder, 'StoneSerializers.swift'), - self.target_folder_path) - self.logger.info('Copying StoneBase.swift to output folder') - shutil.copy(os.path.join(rsrc_folder, 'StoneBase.swift'), - self.target_folder_path) + if not self.args.objc: + self.logger.info('Copying StoneValidators.swift to output folder') + shutil.copy(os.path.join(rsrc_folder, 'StoneValidators.swift'), + self.target_folder_path) + self.logger.info('Copying StoneSerializers.swift to output folder') + shutil.copy(os.path.join(rsrc_folder, 'StoneSerializers.swift'), + self.target_folder_path) + self.logger.info('Copying StoneBase.swift to output folder') + shutil.copy(os.path.join(rsrc_folder, 'StoneBase.swift'), + self.target_folder_path) + + template_loader = jinja2.FileSystemLoader(searchpath=rsrc_folder) + template_env = jinja2.Environment(loader=template_loader, + trim_blocks=True, + lstrip_blocks=True, + autoescape=False) + + template_globals = {} + template_globals['fmt_class'] = fmt_class + template_globals['is_struct_type'] = is_struct_type + template_globals['is_union_type'] = is_union_type + template_globals['fmt_var'] = fmt_var + template_globals['fmt_type'] = fmt_type + template_globals['func_args'] = self._func_args + template_globals['struct_init_args'] = self._struct_init_args + template_globals['determine_validator_type'] = self._determine_validator_type + template_globals['fmt_serial_obj'] = fmt_serial_obj + template_globals['fmt_default_value'] = fmt_default_value + template_globals['format_tag_type'] = self._format_tag_type + template_globals['is_void_type'] = is_void_type + template_globals['tag_type'] = self._tag_type + template_globals['fmt_func'] = fmt_func + template_globals['data_type_doc'] = self._data_type_doc + template_globals['struct_field_doc'] = self._struct_field_doc + template_globals['union_field_doc'] = self._union_field_doc + template_globals['field_name_args'] = self._field_name_args + template_globals['route_schema_attrs'] = self._route_schema_attrs + template_globals['fmt_route_name'] = fmt_route_name + template_globals['data_objc_type_doc'] = self._data_objc_type_doc + template_globals['objc_init_args'] = self._objc_init_args + template_globals['fmt_objc_type'] = fmt_objc_type + oneliner_func_key = 'objc_return_field_value_oneliner' + template_globals[oneliner_func_key] = self._objc_return_field_value_oneliner + template_globals['field_is_user_defined'] = field_is_user_defined + template_globals['field_is_user_defined_optional'] = field_is_user_defined_optional + template_globals['field_is_user_defined_list'] = field_is_user_defined_list + template_globals['field_is_user_defined_map'] = field_is_user_defined_map + in_jinja_key = 'field_datatype_has_subtypes' + template_globals[in_jinja_key] = field_datatype_has_subtypes + template_globals['objc_datatype_value_type_tuples'] = objc_datatype_value_type_tuples + template_globals['objc_init_args_to_swift'] = self._objc_init_args_to_swift + template_globals['objc_union_arg'] = self._objc_union_arg + template_globals['objc_swift_var_name'] = self._objc_swift_var_name + template_globals['swift_union_arg_to_objc'] = self._swift_union_arg_to_objc + template_globals['union_swift_arg_guard'] = self._union_swift_arg_guard + + swift_template_file = "SwiftTypes.jinja" + swift_template = template_env.get_template(swift_template_file) + swift_template.globals = template_globals + + objc_template_file = "ObjcTypes.jinja" + objc_template = template_env.get_template(objc_template_file) + objc_template.globals = template_globals + for namespace in api.namespaces.values(): + ns_class = fmt_class(namespace.name) + + if self.args.objc: + objc_output = objc_template.render(namespace=namespace, + route_schema=api.route_schema) + self._write_output_in_target_folder(objc_output, + 'DBX{}.swift'.format(ns_class)) + else: + swift_output = swift_template.render(namespace=namespace, + route_schema=api.route_schema) + self._write_output_in_target_folder(swift_output, + '{}.swift'.format(ns_class)) + if self.args.documentation: + self._generate_jazzy_docs(api) + + def _generate_jazzy_docs(self, api): jazzy_cfg_path = os.path.join('../Format', 'jazzy.json') with open(jazzy_cfg_path, encoding='utf-8') as jazzy_file: jazzy_cfg = json.load(jazzy_file) for namespace in api.namespaces.values(): ns_class = fmt_class(namespace.name) - with self.output_to_relative_path('{}.swift'.format(ns_class)): - self._generate_base_namespace_module(api, namespace) jazzy_cfg['custom_categories'][1]['children'].append(ns_class) if namespace.routes: + check_route_name_conflict(namespace) jazzy_cfg['custom_categories'][0]['children'].append(ns_class + 'Routes') with self.output_to_relative_path('../../../../.jazzy.json'): self.emit_raw(json.dumps(jazzy_cfg, indent=2) + '\n') - def _generate_base_namespace_module(self, api, namespace): - self.emit_raw(base) - - routes_base = 'Datatypes and serializers for the {} namespace'.format(namespace.name) - self.emit_wrapped_text(routes_base, prefix='/// ', width=120) - - with self.block('open class {}'.format(fmt_class(namespace.name))): - for data_type in namespace.linearize_data_types(): - if is_struct_type(data_type): - self._generate_struct_class(namespace, data_type) - self.emit() - elif is_union_type(data_type): - self._generate_union_type(namespace, data_type) - self.emit() - if namespace.routes: - self._generate_route_objects(api.route_schema, namespace) - - def _generate_struct_class(self, namespace, data_type): + def _data_type_doc(self, data_type): if data_type.doc: doc = self.process_doc(data_type.doc, self._docf) else: - doc = 'The {} struct'.format(fmt_class(data_type.name)) - self.emit_wrapped_text(doc, prefix='/// ', width=120) - protocols = [] - if not data_type.parent_type: - protocols.append('CustomStringConvertible') - - with self.class_block(data_type, protocols=protocols): - for field in data_type.fields: - fdoc = self.process_doc(field.doc, - self._docf) if field.doc else undocumented - self.emit_wrapped_text(fdoc, prefix='/// ', width=120) - self.emit('public let {}: {}'.format( - fmt_var(field.name), - fmt_type(field.data_type), - )) - self._generate_struct_init(namespace, data_type) - - decl = 'open var' if not data_type.parent_type else 'open override var' - - with self.block('{} description: String'.format(decl)): - cls = fmt_class(data_type.name) + 'Serializer' - self.emit('return "\\(SerializeUtil.prepareJSONForSerialization' + - '({}().serialize(self)))"'.format(cls)) - - self._generate_struct_class_serializer(namespace, data_type) - - def _generate_struct_init(self, namespace, data_type): # pylint: disable=unused-argument - # init method - args = self._struct_init_args(data_type) - if data_type.parent_type and not data_type.fields: - return - with self.function_block('public init', self._func_args(args)): - for field in data_type.fields: - v = fmt_var(field.name) - validator = self._determine_validator_type(field.data_type, v) - if validator: - self.emit('{}({})'.format(validator, v)) - self.emit('self.{0} = {0}'.format(v)) - if data_type.parent_type: - func_args = [(fmt_var(f.name), - fmt_var(f.name)) - for f in data_type.parent_type.all_fields] - self.emit('super.init({})'.format(self._func_args(func_args))) + doc = 'The {} {}'.format(fmt_class(data_type.name), + 'struct' if is_struct_type(data_type) else 'union') + return textwrap.fill(doc, + initial_indent='/// ', + subsequent_indent=' /// ', + break_long_words=False, + break_on_hyphens=False, + width=116) + + def _data_objc_type_doc(self, data_type): + if data_type.doc: + doc = self.process_doc(data_type.doc, self._docf) + else: + doc = 'Objective-C compatible {} {}'.format(fmt_class(data_type.name), + 'struct' if is_struct_type(data_type) else 'union') + return textwrap.fill(doc, + initial_indent='/// ', + subsequent_indent='/// ', + break_long_words=False, + break_on_hyphens=False, + width=116) + + def _struct_field_doc(self, field, subsequent_indent=' '): + return self._field_doc(field, undocumented, subsequent_indent) + + def _union_field_doc(self, field, subsequent_indent=' '): + return self._field_doc(field, 'An unspecified error.', subsequent_indent) + + def _field_doc(self, field, error_text, subsequent_indent=' '): + fdoc = self.process_doc(field.doc, self._docf) if field.doc else error_text + return textwrap.fill(fdoc, + initial_indent='/// ', + subsequent_indent='{}/// '.format(subsequent_indent), + break_long_words=False, + break_on_hyphens=False, + width=112) def _determine_validator_type(self, data_type, value): data_type, nullable = unwrap_nullable(data_type) @@ -243,235 +314,178 @@ def _determine_validator_type(self, data_type, value): v = "nullableValidator({})".format(v) return v - def _generate_enumerated_subtype_serializer(self, namespace, # pylint: disable=unused-argument - data_type): - with self.block('switch value'): - for tags, subtype in data_type.get_all_subtypes_with_tags(): - assert len(tags) == 1, tags - tag = tags[0] - tagvar = fmt_var(tag) - self.emit('case let {} as {}:'.format( - tagvar, - fmt_type(subtype) - )) - - with self.indent(): - block_txt = 'for (k, v) in Serialization.getFields({}.serialize({}))'.format( - fmt_serial_obj(subtype), - tagvar, - ) - with self.block(block_txt): - self.emit('output[k] = v') - self.emit('output[".tag"] = .str("{}")'.format(tag)) - self.emit('default: fatalError("Tried to serialize unexpected subtype")') - - def _generate_struct_base_class_deserializer(self, namespace, data_type): - args = [] - for field in data_type.all_fields: - var = fmt_var(field.name) - value = 'dict["{}"]'.format(field.name) - self.emit('let {} = {}.deserialize({} ?? {})'.format( - var, - fmt_serial_obj(field.data_type), - value, - fmt_default_value(namespace, field) if field.has_default else '.null' - )) - - args.append((var, var)) - self.emit('return {}({})'.format( - fmt_class(data_type.name), - self._func_args(args) - )) - - def _generate_enumerated_subtype_deserializer(self, namespace, data_type): - self.emit('let tag = Serialization.getTag(dict)') - with self.block('switch tag'): - for tags, subtype in data_type.get_all_subtypes_with_tags(): - assert len(tags) == 1, tags - tag = tags[0] - self.emit('case "{}":'.format(tag)) - with self.indent(): - self.emit('return {}.deserialize(json)'.format(fmt_serial_obj(subtype))) - self.emit('default:') - with self.indent(): - if data_type.is_catch_all(): - self._generate_struct_base_class_deserializer(namespace, data_type) - else: - self.emit('fatalError("Unknown tag \\(tag)")') - - def _generate_struct_class_serializer(self, namespace, data_type): - with self.serializer_block(data_type): - with self.serializer_func(data_type): - if not data_type.all_fields: - self.emit('let output = [String: JSON]()') - else: - intro = 'var' if data_type.has_enumerated_subtypes() else 'let' - self.emit("{} output = [ ".format(intro)) - for field in data_type.all_fields: - self.emit('"{}": {}.serialize(value.{}),'.format( - field.name, - fmt_serial_obj(field.data_type), - fmt_var(field.name) - )) - self.emit(']') - - if data_type.has_enumerated_subtypes(): - self._generate_enumerated_subtype_serializer(namespace, data_type) - self.emit('return .dictionary(output)') - with self.deserializer_func(data_type): - with self.block("switch json"): - dict_name = "let dict" if data_type.all_fields else "_" - self.emit("case .dictionary({}):".format(dict_name)) - with self.indent(): - if data_type.has_enumerated_subtypes(): - self._generate_enumerated_subtype_deserializer(namespace, data_type) - else: - self._generate_struct_base_class_deserializer(namespace, data_type) - self.emit("default:") - with self.indent(): - self.emit('fatalError("Type error deserializing")') - - def _format_tag_type(self, namespace, data_type): # pylint: disable=unused-argument + def _format_tag_type(self, data_type): if is_void_type(data_type): return '' else: return '({})'.format(fmt_type(data_type)) - def _generate_union_type(self, namespace, data_type): - if data_type.doc: - doc = self.process_doc(data_type.doc, self._docf) - else: - doc = 'The {} union'.format(fmt_class(data_type.name)) - self.emit_wrapped_text(doc, prefix='/// ', width=120) - - class_type = fmt_class(data_type.name) - with self.block('public enum {}: CustomStringConvertible'.format(class_type)): - for field in data_type.all_fields: - typ = self._format_tag_type(namespace, field.data_type) - - fdoc = self.process_doc(field.doc, - self._docf) if field.doc else 'An unspecified error.' - self.emit_wrapped_text(fdoc, prefix='/// ', width=120) - self.emit('case {}{}'.format(fmt_var(field.name), typ)) - self.emit() - with self.block('public var description: String'): - cls = class_type + 'Serializer' - self.emit('return "\\(SerializeUtil.prepareJSONForSerialization' + - '({}().serialize(self)))"'.format(cls)) - - self._generate_union_serializer(data_type) - def _tag_type(self, data_type, field): return "{}.{}".format( fmt_class(data_type.name), fmt_var(field.name) ) - def _generate_union_serializer(self, data_type): - with self.serializer_block(data_type): - with self.serializer_func(data_type), self.block('switch value'): - for field in data_type.all_fields: - field_type = field.data_type - case = '.{}{}'.format(fmt_var(field.name), - '' if is_void_type(field_type) else '(let arg)') - self.emit('case {}:'.format(case)) - - with self.indent(): - if is_void_type(field_type): - self.emit('var d = [String: JSON]()') - elif (is_struct_type(field_type) and - not field_type.has_enumerated_subtypes()): - self.emit('var d = Serialization.getFields({}.serialize(arg))'.format( - fmt_serial_obj(field_type))) - else: - self.emit('var d = ["{}": {}.serialize(arg)]'.format( - field.name, - fmt_serial_obj(field_type))) - self.emit('d[".tag"] = .str("{}")'.format(field.name)) - self.emit('return .dictionary(d)') - with self.deserializer_func(data_type): - with self.block("switch json"): - self.emit("case .dictionary(let d):") - with self.indent(): - self.emit('let tag = Serialization.getTag(d)') - with self.block('switch tag'): - for field in data_type.all_fields: - field_type = field.data_type - self.emit('case "{}":'.format(field.name)) - - tag_type = self._tag_type(data_type, field) - with self.indent(): - if is_void_type(field_type): - self.emit('return {}'.format(tag_type)) - else: - if (is_struct_type(field_type) and - not field_type.has_enumerated_subtypes()): - subdict = 'json' - else: - subdict = 'd["{}"] ?? .null'.format(field.name) - - self.emit('let v = {}.deserialize({})'.format( - fmt_serial_obj(field_type), subdict - )) - self.emit('return {}(v)'.format(tag_type)) - self.emit('default:') - with self.indent(): - if data_type.catch_all_field: - self.emit('return {}'.format( - self._tag_type(data_type, data_type.catch_all_field) - )) - else: - self.emit('fatalError("Unknown tag \\(tag)")') - self.emit("default:") - with self.indent(): - - self.emit('fatalError("Failed to deserialize")') - - @contextmanager - def serializer_block(self, data_type): - with self.class_block(fmt_class(data_type.name) + 'Serializer', - protocols=['JSONSerializer']): - self.emit("public init() { }") - yield - - @contextmanager - def serializer_func(self, data_type): - with self.function_block('open func serialize', - args=self._func_args([('_ value', fmt_class(data_type.name))]), - return_type='JSON'): - yield - - @contextmanager - def deserializer_func(self, data_type): - with self.function_block('open func deserialize', - args=self._func_args([('_ json', 'JSON')]), - return_type=fmt_class(data_type.name)): - yield - - def _generate_route_objects(self, route_schema, namespace): - check_route_name_conflict(namespace) - - self.emit() - self.emit('/// Stone Route Objects') - self.emit() - for route in namespace.routes: - var_name = fmt_func(route.name, route.version) - with self.block('static let {} = Route('.format(var_name), - delim=(None, None), after=')'): - self.emit('name: \"{}\",'.format(route.name)) - self.emit('version: {},'.format(route.version)) - self.emit('namespace: \"{}\",'.format(namespace.name)) - self.emit('deprecated: {},'.format('true' if route.deprecated - is not None else 'false')) - self.emit('argSerializer: {},'.format(fmt_serial_obj(route.arg_data_type))) - self.emit('responseSerializer: {},'.format(fmt_serial_obj(route.result_data_type))) - self.emit('errorSerializer: {},'.format(fmt_serial_obj(route.error_data_type))) - attrs = [] - for field in route_schema.fields: - attr_key = field.name - attr_val = ("\"{}\"".format(route.attrs.get(attr_key)) - if route.attrs.get(attr_key) else 'nil') - attrs.append('\"{}\": {}'.format(attr_key, attr_val)) - - self.generate_multiline_list( - attrs, delim=('attrs: [', ']'), compact=True) + def _field_name_args(self, data_type): + args = [] + for field in data_type.all_fields: + name = fmt_var(field.name) + arg = (name, name) + args.append(arg) + + return self._func_args(args) + + def _route_schema_attrs(self, route_schema, route): + attrs = [] + for field in route_schema.fields: + attr_key = field.name + if route.attrs.get(attr_key): + attr_val = route.attrs.get(attr_key) + if attr_key == 'auth': + auths = attr_val.split(', ') + auths = ['.{}'.format(auth) for auth in auths] + attr_val = ', '.join(auths) + attrs.append('{}: [{}]'.format(attr_key, attr_val)) + else: + attrs.append('{}: .{}'.format(attr_key, attr_val)) + + result = ',\n '.join(attrs) + return result + + def _objc_return_field_value_oneliner(self, parent_type, field): + data_type, nullable = unwrap_nullable(field.data_type) + swift_var_name = self._objc_swift_var_name(parent_type) + + if is_list_type(data_type): + _, prefix, suffix, list_data_type, list_nullable = mapped_list_info(data_type) + + value = '{}.{}'.format(swift_var_name, + fmt_var(field.name)) + + if not is_numeric_type(list_data_type) and not is_user_defined_type(list_data_type): + return value + + if is_user_defined_type(list_data_type): + objc_type = fmt_objc_type(list_data_type, False) + value = '{}{}.map {}{{ {}(swift: $0) }}'.format(value, + '?' if nullable else '', + prefix, + objc_type) + elif is_numeric_type(list_data_type): + map_func = 'compactMap' if list_nullable else 'map' + value = '{}{}.{} {}{{ $0 as NSNumber{} }}'.format(value, + '?' if nullable else '', + map_func, + prefix, + '?' if list_nullable else '') + + value = '{}{}'.format(value, suffix) + return value + elif is_map_type(data_type) and is_user_defined_type(data_type.value_data_type): + objc_type = fmt_objc_type(data_type.value_data_type) + value = '{}.{}'.format(swift_var_name, + fmt_var(field.name)) + value = '{}{}.mapValues {{ {}(swift: $0) }}'.format(value, + '?' if nullable else '', + objc_type) + return value + elif is_user_defined_type(data_type): + value = '' + swift_arg_name = '{}.{}'.format(swift_var_name, + fmt_var(field.name)) + if nullable: + value = 'guard let swift = {}.{} else {{ return nil }}; return '.format( + swift_var_name, + fmt_var(field.name)) + swift_arg_name = 'swift' + return '{}{}(swift: {})'.format(value, + fmt_objc_type(field.data_type, False), + swift_arg_name) + elif is_numeric_type(data_type) or is_boolean_type(data_type): + return '{}.{} as NSNumber{}'.format(swift_var_name, + fmt_var(field.name), + '?' if nullable else '') + else: + return '{}.{}'.format(swift_var_name, + fmt_var(field.name)) + + def _objc_union_arg(self, field): + field_data_type, field_nullable = unwrap_nullable(field.data_type) + nsnumber_type = _nsnumber_type_table.get(field_data_type.__class__) + + if is_list_type(field_data_type): + _, prefix, suffix, list_data_type, _ = mapped_list_info(field_data_type) + + value = '(arg{}'.format('?' if field_nullable else '') + list_nsnumber_type = _nsnumber_type_table.get(list_data_type.__class__) + + if not is_user_defined_type(list_data_type) and not list_nsnumber_type: + value = '(arg' + else: + value = '{}.map {}'.format(value, + prefix) + + if is_user_defined_type(list_data_type): + value = '{}{{ $0.{} }}'.format(value, + self._objc_swift_var_name(list_data_type)) + else: + value = '{}{{ $0{} }}'.format(value, + list_nsnumber_type) + + value = '{}{})'.format(value, + suffix) + return value + elif is_user_defined_type(field_data_type): + return '(arg{}.{})'.format('?' if field_nullable else '', + self._objc_swift_var_name(field_data_type)) + elif is_void_type(field_data_type): + return '' + elif nsnumber_type: + return '(arg{}{})'.format('?' if field_nullable else '', + nsnumber_type) + else: + return '(arg)' + + def _swift_union_arg_to_objc(self, field): + field_data_type, field_nullable = unwrap_nullable(field.data_type) + nsnumber_type = _nsnumber_type_table.get(field_data_type.__class__) + + if is_list_type(field_data_type): + _, prefix, suffix, list_data_type, _ = mapped_list_info(field_data_type) + + value = 'swiftArg{}'.format('?' if field_nullable else '') + list_nsnumber_type = _nsnumber_type_table.get(list_data_type.__class__) + + if not is_user_defined_type(list_data_type) and not list_nsnumber_type: + return 'swiftArg' + else: + value = '{}.map {}'.format(value, + prefix) + + if is_user_defined_type(list_data_type): + factory_func = '.factory' if is_union_type(list_data_type) else '' + value = '{}{{ {}{}(swift: $0) }}'.format(value, + fmt_objc_type(list_data_type), + factory_func) + else: + value = '{}{{ NSNumber(value: $0) }}'.format(value) + + value = '{}{}'.format(value, + suffix) + return value + elif is_user_defined_type(field_data_type): + return '{}(swift: swiftArg)'.format(fmt_objc_type(field_data_type)) + elif is_void_type(field_data_type): + return '' + elif nsnumber_type: + return 'NSNumber(value: swiftArg)' + else: + return 'swiftArg' + + def _union_swift_arg_guard(self, field, class_name): + field_data_type, field_nullable = unwrap_nullable(field.data_type) + + if field_nullable and is_user_defined_type(field_data_type): + return 'guard let swiftArg = swiftArg else {{ return {}(nil) }}'.format(class_name) + else: + return '' diff --git a/stone/cli.py b/stone/cli.py index 232f7b20..0e8deb15 100644 --- a/stone/cli.py +++ b/stone/cli.py @@ -1,9 +1,9 @@ """ A command-line interface for StoneAPI. """ - import importlib.util import importlib.machinery + import io import json import logging diff --git a/tox.ini b/tox.ini index a694f6ec..87cb45f7 100644 --- a/tox.ini +++ b/tox.ini @@ -55,7 +55,6 @@ commands = deps = enum34 mypy - typed-ast usedevelop = true