220 lines
7.8 KiB
Python
220 lines
7.8 KiB
Python
import os
|
|
import re
|
|
|
|
_MYPY = False
|
|
if _MYPY:
|
|
import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression
|
|
|
|
import argparse
|
|
|
|
from stone.backend import CodeBackend
|
|
from stone.backends.tsd_helpers import (
|
|
check_route_name_conflict,
|
|
fmt_error_type,
|
|
fmt_func,
|
|
fmt_tag,
|
|
fmt_type,
|
|
get_data_types_for_namespace,
|
|
)
|
|
from stone.ir import Void
|
|
|
|
|
|
_cmdline_parser = argparse.ArgumentParser(prog='tsd-client-backend')
|
|
_cmdline_parser.add_argument(
|
|
'template',
|
|
help=('A template to use when generating the TypeScript definition file.')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'filename',
|
|
help=('The name to give the single TypeScript definition file to contain '
|
|
'all of the emitted types.'),
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'-t',
|
|
'--template-string',
|
|
type=str,
|
|
default='ROUTES',
|
|
help=('The name of the template string to replace with route definitions. '
|
|
'Defaults to ROUTES, which replaces the string /*ROUTES*/ with route '
|
|
'definitions.')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'-i',
|
|
'--indent-level',
|
|
type=int,
|
|
default=1,
|
|
help=('Indentation level to emit types at. Routes are automatically '
|
|
'indented one level further than this.')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'-s',
|
|
'--spaces-per-indent',
|
|
type=int,
|
|
default=2,
|
|
help=('Number of spaces to use per indentation level.')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'--wrap-response-in',
|
|
type=str,
|
|
default='',
|
|
help=('Wraps the response in a response class')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'--wrap-error-in',
|
|
type=str,
|
|
default='',
|
|
help=('Wraps the error in an error class')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'--import-namespaces',
|
|
default=False,
|
|
action='store_true',
|
|
help=('Adds an import statement at the top of the file to import each '
|
|
'namespace from the as a named import. Must be used in conjunction '
|
|
'with the --export-namespaces command when generating the ts_types.')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'--import-template-string',
|
|
type=str,
|
|
default='IMPORT',
|
|
help=('The name of the template string to replace with import statement. '
|
|
'Defaults to IMPORT, which replaces the string /*IMPORT*/ with import.')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'--types-file',
|
|
type=str,
|
|
default='',
|
|
help=('If using the --import-namespaces flag, this is the file that contains '
|
|
'the named exports to import here.')
|
|
)
|
|
_cmdline_parser.add_argument(
|
|
'-a',
|
|
'--attribute-comment',
|
|
action='append',
|
|
type=str,
|
|
default=[],
|
|
help=('Attributes to include in route documentation comments.'),
|
|
)
|
|
|
|
_header = """\
|
|
// Auto-generated by Stone, do not modify.
|
|
"""
|
|
|
|
class TSDClientBackend(CodeBackend):
|
|
"""Generates a TypeScript definition file with routes defined."""
|
|
|
|
cmdline_parser = _cmdline_parser
|
|
|
|
preserve_aliases = True
|
|
|
|
def generate(self, api):
|
|
spaces_per_indent = self.args.spaces_per_indent
|
|
indent_level = self.args.indent_level
|
|
template_path = os.path.join(self.target_folder_path, self.args.template)
|
|
template_string = self.args.template_string
|
|
|
|
with self.output_to_relative_path(self.args.filename):
|
|
if os.path.isfile(template_path):
|
|
with open(template_path, encoding='utf-8') as template_file:
|
|
template = template_file.read()
|
|
else:
|
|
raise AssertionError('TypeScript template file does not exist.')
|
|
|
|
# /*ROUTES*/
|
|
r_match = re.search("/\\*%s\\*/" % (template_string), template)
|
|
if not r_match:
|
|
raise AssertionError(
|
|
'Missing /*%s*/ in TypeScript template file.' % template_string)
|
|
|
|
r_start = r_match.start()
|
|
r_end = r_match.end()
|
|
r_ends_with_newline = template[r_end - 1] == '\n'
|
|
t_end = len(template)
|
|
t_ends_with_newline = template[t_end - 1] == '\n'
|
|
|
|
if self.args.import_namespaces:
|
|
import_template_string = self.args.import_template_string
|
|
import_from_file = self.args.types_file
|
|
# /*IMPORT*/
|
|
i_match = re.search("/\\*%s\\*/" % (import_template_string), template)
|
|
if not i_match:
|
|
raise AssertionError(
|
|
'Missing /*%s*/ in TypeScript template file.' % import_template_string)
|
|
i_start = i_match.start()
|
|
i_end = i_match.end()
|
|
i_ends_with_newline = template[i_end - 1] == '\n'
|
|
t_end = len(template)
|
|
t_ends_with_newline = template[t_end - 1] == '\n'
|
|
|
|
self.emit_raw(template[0:i_start] + ('\n' if not i_ends_with_newline else ''))
|
|
self._generate_import(api, import_from_file)
|
|
self.emit_raw(template[i_end + 1:r_end] + ('\n' if not r_ends_with_newline else ''))
|
|
else:
|
|
self.emit_raw(template[0:r_start] + ('\n' if not r_ends_with_newline else ''))
|
|
self._generate_routes(api, spaces_per_indent, indent_level)
|
|
self.emit_raw(template[r_end + 1:t_end] + ('\n' if not t_ends_with_newline else ''))
|
|
|
|
def _generate_import(self, api, type_file):
|
|
# identify which routes belong to
|
|
namespaces_with_types = filter(
|
|
lambda namespace: len(get_data_types_for_namespace(namespace)) != 0,
|
|
api.namespaces.values())
|
|
namespaces = ", ".join(map(lambda namespace: namespace.name, namespaces_with_types))
|
|
self.emit("import {{ {} }} from '{}';".format(namespaces, type_file))
|
|
|
|
def _generate_routes(self, api, spaces_per_indent, indent_level):
|
|
with self.indent(dent=spaces_per_indent * (indent_level + 1)):
|
|
for namespace in api.namespaces.values():
|
|
# first check for route name conflict
|
|
check_route_name_conflict(namespace)
|
|
for route in namespace.routes:
|
|
self._generate_route(
|
|
namespace, route)
|
|
|
|
def _generate_route(self, namespace, route):
|
|
function_name = fmt_func(namespace.name + '_' + route.name, route.version)
|
|
self.emit()
|
|
self.emit('/**')
|
|
if route.doc:
|
|
self.emit_wrapped_text(self.process_doc(route.doc, self._docf), prefix=' * ')
|
|
self.emit(' *')
|
|
|
|
attrs_lines = []
|
|
if self.args.attribute_comment and route.attrs:
|
|
for attribute in self.args.attribute_comment:
|
|
if attribute in route.attrs and route.attrs[attribute] is not None:
|
|
attrs_lines.append(' * {}: {}'.format(attribute, route.attrs[attribute]))
|
|
if attrs_lines:
|
|
self.emit(' * Route attributes:')
|
|
for a in attrs_lines:
|
|
self.emit(a)
|
|
self.emit(' *')
|
|
|
|
self.emit_wrapped_text('When an error occurs, the route rejects the promise with type %s.'
|
|
% fmt_error_type(route.error_data_type,
|
|
wrap_error_in=self.args.wrap_error_in), prefix=' * ')
|
|
if route.deprecated:
|
|
self.emit(' * @deprecated')
|
|
|
|
if route.arg_data_type.__class__ != Void:
|
|
self.emit(' * @param arg The request parameters.')
|
|
self.emit(' */')
|
|
|
|
return_type = None
|
|
if self.args.wrap_response_in:
|
|
return_type = 'Promise<{}<{}>>;'.format(self.args.wrap_response_in,
|
|
fmt_type(route.result_data_type))
|
|
else:
|
|
return_type = 'Promise<%s>;' % (fmt_type(route.result_data_type))
|
|
|
|
arg = ''
|
|
if route.arg_data_type.__class__ != Void:
|
|
arg = 'arg: %s' % fmt_type(route.arg_data_type)
|
|
|
|
self.emit('public {}({}): {}'.format(function_name, arg, return_type))
|
|
|
|
def _docf(self, tag, val):
|
|
"""
|
|
Callback to process documentation references.
|
|
"""
|
|
return fmt_tag(None, tag, val)
|