1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
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)
|