_MYPY = False if _MYPY: import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression from stone.ir import ApiNamespace import argparse from stone.backend import CodeBackend from stone.backends.js_helpers import ( check_route_name_conflict, fmt_error_type, fmt_func, fmt_obj, fmt_type, fmt_url, ) from stone.ir import Void _cmdline_parser = argparse.ArgumentParser(prog='js-client-backend') _cmdline_parser.add_argument( 'filename', help=('The name to give the single Javascript file that is created and ' 'contains all of the routes.'), ) _cmdline_parser.add_argument( '-c', '--class-name', type=str, help=('The name of the class the generated functions will be attached to. ' 'The name will be added to each function documentation, which makes ' 'it available for tools like JSDoc.'), ) _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( '-a', '--attribute-comment', action='append', type=str, default=[], help=('Attributes to include in route documentation comments.'), ) _header = """\ // Auto-generated by Stone, do not modify. var routes = {}; """ class JavascriptClientBackend(CodeBackend): """Generates a single Javascript file with all of the routes defined.""" cmdline_parser = _cmdline_parser # Instance var of the current namespace being generated cur_namespace = None # type: typing.Optional[ApiNamespace] preserve_aliases = True def generate(self, api): # first check for route name conflict with self.output_to_relative_path(self.args.filename): self.emit_raw(_header) for namespace in api.namespaces.values(): # Hack: needed for _docf() self.cur_namespace = namespace check_route_name_conflict(namespace) for route in namespace.routes: self._generate_route(api.route_schema, namespace, route) self.emit() self.emit('export { routes };') def _generate_route(self, route_schema, 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=' * ') 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) if self.args.class_name: self.emit(' * @function {}#{}'.format(self.args.class_name, function_name)) if route.deprecated: self.emit(' * @deprecated') return_type = None if self.args.wrap_response_in: return_type = '{}<{}>'.format(self.args.wrap_response_in, fmt_type(route.result_data_type)) else: return_type = fmt_type(route.result_data_type) if route.arg_data_type.__class__ != Void: self.emit(' * @arg {%s} arg - The request parameters.' % fmt_type(route.arg_data_type)) self.emit(' * @returns {Promise.<%s, %s>}' % (return_type, fmt_error_type(route.error_data_type, self.args.wrap_error_in))) self.emit(' */') if route.arg_data_type.__class__ != Void: self.emit('routes.%s = function (arg) {' % (function_name)) else: self.emit('routes.%s = function () {' % (function_name)) with self.indent(dent=2): url = fmt_url(namespace.name, route.name, route.version) if route_schema.fields: additional_args = [] for field in route_schema.fields: additional_args.append(fmt_obj(route.attrs[field.name])) if route.arg_data_type.__class__ != Void: self.emit( "return this.request('{}', arg, {});".format( url, ', '.join(additional_args))) else: self.emit( "return this.request('{}', null, {});".format( url, ', '.join(additional_args))) else: if route.arg_data_type.__class__ != Void: self.emit( 'return this.request("%s", arg);' % url) else: self.emit( 'return this.request("%s", null);' % url) self.emit('};') def _docf(self, tag, val): """ Callback used as the handler argument to process_docs(). This converts Stone doc references to JSDoc-friendly annotations. """ # TODO(kelkabany): We're currently just dropping all doc ref tags ... # NOTE(praneshp): ... except for versioned routes if tag == 'route': if ':' in val: val, version = val.split(':', 1) version = int(version) else: version = 1 url = fmt_url(self.cur_namespace.name, val, version) # NOTE: In js, for comments, we drop the namespace name and the '/' when # documenting URLs return url[(len(self.cur_namespace.name) + 1):] return val