Skip to content

Commit

Permalink
Add first party Objective-C to Swift SDK RPC migration shim. (#350)
Browse files Browse the repository at this point in the history
  • Loading branch information
julianlocke authored Oct 24, 2024
1 parent 40bd164 commit f33f7c2
Show file tree
Hide file tree
Showing 11 changed files with 823 additions and 48 deletions.
3 changes: 3 additions & 0 deletions stone/backends/obj_c_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@
'id',
'delete',
'hash',
'boolvalue',
'floatvalue',
'intvalue',
}

_reserved_prefixes = {
Expand Down
277 changes: 277 additions & 0 deletions stone/backends/swift.py

Large diffs are not rendered by default.

72 changes: 70 additions & 2 deletions stone/backends/swift_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
datatype_has_subtypes,
)

from stone.backends.obj_c_helpers import (
fmt_class_prefix
)

_MYPY = False
if _MYPY:
import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression
Expand Down Expand Up @@ -93,6 +97,11 @@
action='store_true',
help='Generate the Objective-C compatibile files.',
)
_cmdline_parser.add_argument(
'--objc-shim',
action='store_true',
help='Generate the Objective-C to Swift migration files.',
)


class SwiftBackend(SwiftBaseBackend):
Expand Down Expand Up @@ -137,14 +146,23 @@ class SwiftBackend(SwiftBaseBackend):

def generate(self, api):
for namespace in api.namespaces.values():
if namespace.routes:
if self._namespace_contains_valid_routes_for_auth_type(namespace):
self._generate_routes(namespace)

self._generate_client(api)
self._generate_request_boxes(api)
if not self.args.objc:
if not self.args.objc and not self.args.objc_shim:
self._generate_reconnection_helpers(api)

# Argument cast when mapping a legacy objc route to an objc route
# ', let args = args as? DBUSERSGetAccountBatchArg'
def shim_rpc_function_argument_if_necessary(self, data_type):
if is_user_defined_type(data_type) and len(data_type.fields) > 0:
class_name = fmt_class_prefix(data_type)
return ', let args = args as? {}'.format(class_name)
else:
return ''

def _generate_client(self, api):
template_globals = {}
template_globals['class_name'] = self.args.class_name
Expand All @@ -158,6 +176,9 @@ def _generate_client(self, api):

self._write_output_in_target_folder(template.render(),
'DBX{}.swift'.format(self.args.module_name))

elif self.args.objc_shim:
self._generate_sdk_migration_shim(api)
else:
template = self._jinja_template("SwiftClient.jinja")
template.globals = template_globals
Expand Down Expand Up @@ -220,6 +241,8 @@ def _generate_routes(self, namespace):

self._write_output_in_target_folder(output_from_parsed_template,
'DBX{}Routes.swift'.format(ns_class))
elif self.args.objc_shim:
return
else:
template = self._jinja_template("SwiftRoutes.jinja")
template.globals = template_globals
Expand Down Expand Up @@ -258,6 +281,8 @@ def _generate_request_boxes(self, api):
file_name = 'DBX{}RequestBox.swift'.format(self.args.class_name)
self._write_output_in_target_folder(output,
file_name)
elif self.args.objc_shim:
return
else:
template = self._jinja_template("SwiftRequestBox.jinja")
template.globals = template_globals
Expand Down Expand Up @@ -296,6 +321,32 @@ def _generate_reconnection_helpers(self, api):
output_from_parsed_template, '{}.swift'.format(class_name)
)

def _generate_sdk_migration_shim(self, api):
template = self._jinja_template("SwiftObjcShimHelpers.jinja")
template_globals = {}
template_globals['namespaces'] = api.namespaces.values()
template_globals['route_client_args'] = self._route_client_args
template_globals['fmt_route_objc_class'] = self._fmt_route_objc_class
template_globals['fmt_func'] = fmt_func
template_globals['fmt_objc_type'] = fmt_objc_type
template_globals['objc_to_legacy_objc_mapper'] = self._shim_objc_to_legacy_objc_type_mapper
template_globals['fmt_route_name_namespace'] = fmt_route_name_namespace
template_globals['shim_rpc_function_arg'] = self.shim_rpc_function_argument_if_necessary
template_globals['route_args'] = self._route_args
template_globals['is_struct_type'] = is_struct_type
template_globals['is_union_type'] = is_union_type
template_globals['fmt_var'] = fmt_var
objc_init_key = 'shim_legacy_objc_init_args_to_objc'
template_globals[objc_init_key] = self._shim_legacy_objc_init_args_to_objc
template_globals['fmt_class_prefix'] = fmt_class_prefix

template.globals = template_globals

output_from_parsed_template = template.render()

self._write_output_in_target_folder(output_from_parsed_template,
'ShimSwiftObjcHelpers.swift')

def _background_compatible_namespace_route_pairs(self, api):
namespaces = api.namespaces.values()
background_compatible_routes = []
Expand Down Expand Up @@ -557,6 +608,23 @@ def _route_objc_result_type(self, route, args_data):
result_type = '{}, {}'.format(result_type, error_type)
return result_type

# Used in objc to legacy objc type RPC completion mapping, optionals only.
# 'mapDBXCameraUploadsMobileCommitCameraUploadResultToDBOptional(object: result)'
# 'mapDBXCameraUploadsMobileCommitCameraUploadErrorToDBOptional(object: routeError)'
def _shim_objc_to_legacy_objc_type_mapper(self, data_type, mapped_object_name):
if data_type.name == 'Void':
return 'nil'
elif is_list_type(data_type):
if is_user_defined_type(data_type.data_type):
list_data_type = fmt_objc_type(data_type.data_type)
return '{}?.map {{ map{}ToDBOptional(object: $0) }}'.format(mapped_object_name,
list_data_type)
else:
return '{}'.format(mapped_object_name)
else:
return 'map{}ToDBOptional(object: {})'.format(fmt_objc_type(data_type),
mapped_object_name)

def _background_compatible_routes_for_objc_requests(self, api):
namespaces = api.namespaces.values()
objc_class_to_route = {}
Expand Down
26 changes: 17 additions & 9 deletions stone/backends/swift_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ def fmt_func(name, version):
name = _format_camelcase(name)
return name


def fmt_type(data_type):
data_type, nullable = unwrap_nullable(data_type)

Expand Down Expand Up @@ -175,6 +174,11 @@ def fmt_objc_type(data_type, allow_nullable=True):
def fmt_var(name):
return _format_camelcase(name)

# Type check component when mapping an objc union to a legacy objc union
# e.g.: `asPricePlanTypePremium`
def fmt_shim_union_psuedo_cast(name):
arg = _format_camelcase(name, lower_first=False)
return 'as{}'.format(arg)

def fmt_default_value(field):
if is_tag_ref(field.default):
Expand Down Expand Up @@ -267,6 +271,17 @@ def field_is_user_defined_list(field):
# List[typing.Tuple[let_name: str, swift_type: str, objc_type: str]]
def objc_datatype_value_type_tuples(data_type):
ret = []
for d_type in datatype_subtype_value_types(data_type):
case_let_name = fmt_var(d_type.name)
swift_type = fmt_type(d_type)
objc_type = fmt_objc_type(d_type)
ret.append((case_let_name, swift_type, objc_type))

return ret

# List[typing.Tuple[let_name: str, type: DataType]]
def datatype_subtype_value_types(data_type):
ret = []

# if list type get the data type of the item
if is_list_type(data_type):
Expand All @@ -282,15 +297,8 @@ def objc_datatype_value_type_tuples(data_type):

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))
ret.append((subtype[1]))
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
21 changes: 21 additions & 0 deletions stone/backends/swift_rsrc/ObjCRoutes.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -104,20 +104,41 @@ public class {{ fmt_route_objc_class(namespace, route, args_data) }}: NSObject,
self.response(queue: nil, completionHandler: completionHandler)
}

{% if route.attrs.get('style') == 'rpc' and ('user' in route.attrs.get('auth') or 'noauth' in route.attrs.get('auth')) and not route.deprecated %}
@objc
@discardableResult public func response(
queue: DispatchQueue?,
completionHandler: @escaping ({{ result_type }}) -> Void
) -> Self {
self.response(queue: nil, analyticsBlock: nil, completionHandler: completionHandler)
}
{% endif %}

@objc
@discardableResult public func response(
queue: DispatchQueue? = nil,
{% if route.attrs.get('style') == 'rpc' and ('user' in route.attrs.get('auth') or 'noauth' in route.attrs.get('auth')) and not route.deprecated %}
analyticsBlock: AnalyticsBlock? = nil,
{% endif %}
completionHandler: @escaping ({{ result_type }}) -> Void
) -> Self {
{% if route.attrs.get('style') == 'rpc' and ('user' in route.attrs.get('auth') or 'noauth' in route.attrs.get('auth')) and not route.deprecated %}
swift.response(queue: queue, analyticsBlock: analyticsBlock) { result, error in
{% else %}
swift.response(queue: queue) { result, error in
{% endif %}
{% 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, _, _, _):
{% if is_union_type(route.error_data_type) %}
routeError = {{ error_type }}.factory(swift: box.unboxed)
{% else %}
routeError = {{ error_type }}(swift: box.unboxed)
{% endif %}
callError = nil
default:
routeError = nil
Expand Down
38 changes: 18 additions & 20 deletions stone/backends/swift_rsrc/ObjcTypes.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,7 @@ public class DBX{{ namespace_class_name }}{{ data_type_class_name }}: {{ 'NSObje
{% 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 %}

Expand All @@ -52,11 +36,24 @@ public class DBX{{ namespace_class_name }}{{ data_type_class_name }}: {{ 'NSObje
self.{{ swift_var_name }} = {{ swift_type }}({{ objc_init_args_to_swift(data_type) }})
{% endif %}
}
{% elif data_type.parent_type.fields %}

@objc
public override init({{ func_args(objc_init_args(data_type.parent_type)) }}) {
let swift = {{ swift_type }}({{ objc_init_args_to_swift(data_type.parent_type) }})
self.{{ swift_var_name }} = swift
super.init(swift: swift)
}
{% elif not data_type.parent_type %}
public override init() {
self.{{ swift_var_name }} = {{ swift_type }}()
super.init()
}
{% endif %}

let {{ swift_var_name }}: {{ swift_type }}
public let {{ swift_var_name }}: {{ swift_type }}

public init(swift: {{ swift_type }}) {
{{ "fileprivate" if (objc_datatype_value_type_tuples(data_type)|length > 0) else "public" }} init(swift: {{ swift_type }}) {
self.{{ swift_var_name }} = swift
{% if data_type.parent_type %}
super.init(swift: swift)
Expand All @@ -74,6 +71,7 @@ public class DBX{{ namespace_class_name }}{{ data_type_class_name }}: {{ 'NSObje
return DBX{{ namespace_class_name }}{{ data_type_class_name }}(swift: swift)
}
}
{% else %}
{% endif %}

@objc
Expand All @@ -85,9 +83,9 @@ public class DBX{{ namespace_class_name }}{{ data_type_class_name }}: {{ 'NSObje
{% set swift_enum = namespace_class_name + '.' + fmt_class(data_type.name) %}
@objc
public class {{ union_class_name }}: NSObject {
let swift: {{ swift_enum }}
public let swift: {{ swift_enum }}

public init(swift: {{ swift_enum }}) {
fileprivate init(swift: {{ swift_enum }}) {
self.swift = swift
}

Expand Down
51 changes: 51 additions & 0 deletions stone/backends/swift_rsrc/SwiftObjcArgTypeMappings.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
///
/// Copyright (c) 2024 Dropbox, Inc. All rights reserved.
///
/// Auto-generated by Stone, do not modify.
///

import Foundation
import stone_sdk_objc
import stone_sdk_swift
import stone_sdk_swift_objc

{% for data_type in namespace.linearize_data_types() %}
{% set dbx_data_type_class_name = fmt_objc_type(data_type) %}
{% set db_data_type_class_name = fmt_class_prefix(data_type) %}
func map{{ db_data_type_class_name }}ToDBXOptional(object: {{ db_data_type_class_name}}?) -> {{ dbx_data_type_class_name }}? {
guard let object = object else { return nil }
return map{{ db_data_type_class_name }}ToDBX(object: object)
}

func map{{ db_data_type_class_name }}ToDBX(object: {{ db_data_type_class_name}}) -> {{ dbx_data_type_class_name }} {
{% if is_struct_type(data_type) %}
{% if datatype_subtype_value_types(data_type)|length > 0 %}
switch object {
{% for type in datatype_subtype_value_types(data_type) %}
case let object as {{ fmt_class_prefix(type) }}:
return {{ fmt_objc_type(type) }}({{ shim_legacy_objc_init_args_to_objc(type, 'object') }})
{% endfor %}
default:
return {{ dbx_data_type_class_name }}({{ shim_legacy_objc_init_args_to_objc(data_type, 'object') }})
}
{% else %}
return {{ dbx_data_type_class_name }}({{ shim_legacy_objc_init_args_to_objc(data_type, 'object') }})
{% endif %}
{% elif is_union_type(data_type) %}
{% for field in data_type.all_fields %}
{% set case_class = dbx_data_type_class_name + fmt_class(field.name) %}
{% set case_var_name = fmt_var(field.name) %}
if object.{{ fmt_legacy_objc_union_case_check(field.name, data_type) }}() {
{% if not is_void_type(field.data_type) %}
let {{ case_var_name }} = {{ shim_legacy_objc_union_associated_type(field, data_type) }}
return {{ dbx_data_type_class_name }}.factory(swift: {{ shim_legacy_objc_union_associated_type_init(field) }})
{% else %}
return {{ case_class }}()
{% endif %}
}
{% endfor %}
fatalError("codegen error")
{% endif %}
}

{% endfor %}
Loading

0 comments on commit f33f7c2

Please sign in to comment.