Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add first party Objective-C to Swift SDK RPC migration shim. #350

Merged
merged 8 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading