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)