frappe/env/lib/python3.12/site-packages/stone/backends/swift_client.py

628 lines
26 KiB
Python

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 (
fmt_serial_type,
SwiftBaseBackend,
undocumented,
)
from stone.backends.swift_helpers import (
check_route_name_conflict,
fmt_class,
fmt_func,
fmt_var,
fmt_type,
fmt_route_name,
fmt_route_name_namespace,
fmt_func_namespace,
fmt_objc_type,
mapped_list_info,
datatype_has_subtypes,
)
_MYPY = False
if _MYPY:
import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression
import argparse
_cmdline_parser = argparse.ArgumentParser(
prog='swift-client-backend',
description=(
'Generates a Swift class with an object for each namespace, and in each '
'namespace object, a method for each route. This class assumes that the '
'swift_types backend was used with the same output directory.'),
)
_cmdline_parser.add_argument(
'-m',
'--module-name',
required=True,
type=str,
help=('The name of the Swift module to generate. Please exclude the .swift '
'file extension.'),
)
_cmdline_parser.add_argument(
'-c',
'--class-name',
required=True,
type=str,
help=('The name of the Swift class that contains an object for each namespace, '
'and in each namespace object, a method for each route.')
)
_cmdline_parser.add_argument(
'-t',
'--transport-client-name',
required=True,
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',
required=True,
type=str,
help='The client-side route arguments to append to each route by style type.',
)
_cmdline_parser.add_argument(
'-z',
'--style-to-request',
required=True,
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):
"""
Generates Swift client base that implements route interfaces.
Examples:
```
public class ExampleClientBase {
/// Routes within the namespace1 namespace. See Namespace1 for details.
public var namespace1: Namespace1!
/// Routes within the namespace2 namespace. See Namespace2 for details.
public var namespace2: Namespace2!
public init(client: ExampleTransportClient) {
self.namespace1 = Namespace1(client: client)
self.namespace2 = Namespace2(client: client)
}
}
```
Here, `ExampleTransportClient` would contain the implementation of a handwritten,
project-specific networking client. Additionally, the `Namespace1` object would
have as its methods all routes in the `Namespace1` namespace. A hypothetical 'copy'
enpoding might be implemented like:
```
public func copy(fromPath fromPath: String, toPath: String) ->
ExampleRequestType<Namespace1.CopySerializer, Namespace1.CopyErrorSerializer> {
let route = Namespace1.copy
let serverArgs = Namespace1.CopyArg(fromPath: fromPath, toPath: toPath)
return client.request(route, serverArgs: serverArgs)
}
```
Here, ExampleRequestType is a project-specific request type, parameterized by response and
error serializers.
"""
cmdline_parser = _cmdline_parser
def generate(self, api):
for namespace in api.namespaces.values():
if namespace.routes:
self._generate_routes(namespace)
self._generate_client(api)
self._generate_request_boxes(api)
if not self.args.objc:
self._generate_reconnection_helpers(api)
def _generate_client(self, api):
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)
if len(background_compatible_routes) == 0:
return
background_objc_routes = self._background_compatible_routes_for_objc_requests(api)
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)
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:
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
# 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
if objc is False:
arg_type = fmt_type(data_type)
else:
arg_type = fmt_objc_type(data_type)
if is_struct_type(data_type):
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):
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)))]
else:
arg_list = [] if is_void_type(data_type) else [('request', arg_type)]
doc_list = []
return arg_list, doc_list
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]
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:
rdoc = self.process_doc(route.doc, self._docf)
else:
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)
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, _, extra_type in extra_args:
if objc and '=' in extra_type:
extra_type = extra_type.split(' = ', 1)[0]
arg_list.append((name, extra_type))
return self._func_args(arg_list, force_first=False)
def _request_object_name_for_key(self, key):
style_to_request = json.loads(self.args.style_to_request)
return style_to_request[key]
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)
def _server_args(self, route):
args = [(name, name) for name, _ in self._struct_init_args(route.arg_data_type)]
return self._func_args(args)
def _return_args(self, route, args_data):
return_args = [('route', 'route')]
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))
request_object_name = self._request_object_name(route, args_data)
if request_object_name == 'DownloadRequestFile':
result_type = '{}, URL?'.format(result_type)
elif request_object_name == 'DownloadRequestMemory':
result_type = '{}, Data?'.format(result_type)
result_type = '{}, {}'.format(result_type, error_type)
return result_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:
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)