Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript Definition Generation #14

Merged
merged 1 commit into from
Jan 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion doc/builtin_generators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ command-line interface (CLI)::
a generator module. Paths to generator modules must
end with a .stoneg.py extension. The following
generators are built-in: js_client, js_types,
python_types, python_client, swift_client
tsd_client, tsd_types, python_types, python_client,
swift_client
output The folder to save generated files to.
spec Path to API specifications. Each must have a .stone
extension. If omitted or set to "-", the spec is read
Expand Down
2 changes: 2 additions & 0 deletions stone/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
_builtin_generators = (
'js_client',
'js_types',
'tsd_client',
'tsd_types',
'python_types',
'python_client',
'swift_types',
Expand Down
127 changes: 127 additions & 0 deletions stone/target/tsd_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from __future__ import absolute_import, division, print_function, unicode_literals

import os
import re

# Hack to get around some of Python 2's standard library modules that
# accept ascii-encodable unicode literals in lieu of strs, but where
# actually passing such literals results in errors with mypy --py2. See
# <https:/python/typeshed/issues/756> and
# <https:/python/mypy/issues/2536>.
import importlib
import typing # noqa: F401 # pylint: disable=unused-import
argparse = importlib.import_module(str('argparse')) # type: typing.Any

from stone.generator import CodeGenerator
from stone.target.tsd_helpers import (
fmt_error_type,
fmt_func,
fmt_tag,
fmt_type,
)


_cmdline_parser = argparse.ArgumentParser(prog='tsd-client-generator')
_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.')
)

_header = """\
// Auto-generated by Stone, do not modify.
"""

class TSDClientGenerator(CodeGenerator):
"""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, 'r') as template_file:
template = template_file.read()
else:
exit('TypeScript template file does not exist.')

# /*ROUTES*/
r_match = re.search("/\\*%s\\*/" % (template_string), template)
if not r_match:
exit('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'

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_routes(self, api, spaces_per_indent, indent_level):
with self.indent(dent=spaces_per_indent * (indent_level + 1)):
for namespace in api.namespaces.values():
for route in namespace.routes:
self._generate_route(
namespace, route)

def _generate_route(self, namespace, route):
function_name = fmt_func(namespace.name + '_' + route.name)
self.emit()
self.emit('/**')
if route.doc:
self.emit_wrapped_text(self.process_doc(route.doc, self._docf), prefix=' * ')
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), prefix=' * ')
if route.deprecated:
self.emit(' * @deprecated')

self.emit(' * @param arg The request parameters.')
self.emit(' */')

self.emit('public %s(arg: %s): Promise<%s>;' %
(function_name, fmt_type(route.arg_data_type), fmt_type(route.result_data_type)))

def _docf(self, tag, val):
"""
Callback to process documentation references.
"""
return fmt_tag(None, tag, val)
130 changes: 130 additions & 0 deletions stone/target/tsd_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from __future__ import absolute_import, division, print_function, unicode_literals

from stone.data_type import (
Boolean,
Bytes,
Float32,
Float64,
Int32,
Int64,
List,
String,
Timestamp,
UInt32,
UInt64,
Void,
is_alias,
is_list_type,
is_struct_type,
is_user_defined_type,
)
from stone.target.helpers import (
fmt_camel,
)

_base_type_table = {
Boolean: 'boolean',
Bytes: 'string',
Float32: 'number',
Float64: 'number',
Int32: 'number',
Int64: 'number',
List: 'Array',
String: 'string',
UInt32: 'number',
UInt64: 'number',
Timestamp: 'Timestamp',
Void: 'void',
}


def fmt_error_type(data_type, inside_namespace=None):
"""
Converts the error type into a TypeScript type.
inside_namespace should be set to the namespace that the reference
occurs in, or None if this parameter is not relevant.
"""
return 'Error<%s>' % fmt_type(data_type, inside_namespace)

def fmt_type_name(data_type, inside_namespace=None):
"""
Produces a TypeScript type name for the given data type.
inside_namespace should be set to the namespace that the reference
occurs in, or None if this parameter is not relevant.
"""
if is_user_defined_type(data_type) or is_alias(data_type):
if data_type.namespace == inside_namespace:
return data_type.name
else:
return '%s.%s' % (data_type.namespace.name, data_type.name)
else:
fmted_type = _base_type_table.get(data_type.__class__, 'Object')
if is_list_type(data_type):
fmted_type += '<' + fmt_type(data_type.data_type, inside_namespace) + '>'
return fmted_type

def fmt_polymorphic_type_reference(data_type, inside_namespace=None):
"""
Produces a TypeScript type name for the meta-type that refers to the given
struct, which belongs to an enumerated subtypes tree. This meta-type contains the
.tag field that lets developers discriminate between subtypes.
"""
# NOTE: These types are not properly namespaced, so there could be a conflict
# with other user-defined types. If this ever surfaces as a problem, we
# can defer emitting these types until the end, and emit them in a
# nested namespace (e.g., files.references.MetadataReference).
return fmt_type_name(data_type, inside_namespace) + "Reference"

def fmt_type(data_type, inside_namespace=None):
"""
Returns a TypeScript type annotation for a data type.
May contain a union of enumerated subtypes.
inside_namespace should be set to the namespace that the type reference
occurs in, or None if this parameter is not relevant.
"""
if is_struct_type(data_type) and data_type.has_enumerated_subtypes():
possible_types = []
possible_subtypes = data_type.get_all_subtypes_with_tags()
for _, subtype in possible_subtypes:
possible_types.append(fmt_polymorphic_type_reference(subtype, inside_namespace))
if data_type.is_catch_all():
possible_types.append(fmt_polymorphic_type_reference(data_type, inside_namespace))
return fmt_union(possible_types)
else:
return fmt_type_name(data_type, inside_namespace)

def fmt_union(type_strings):
"""
Returns a union type of the given types.
"""
return '|'.join(type_strings) if len(type_strings) > 1 else type_strings[0]

def fmt_func(name):
return fmt_camel(name)

def fmt_var(name):
return fmt_camel(name)

def fmt_tag(cur_namespace, tag, val):
"""
Processes a documentation reference.
"""
if tag == 'type':
fq_val = val
if '.' not in val and cur_namespace is not None:
fq_val = cur_namespace.name + '.' + fq_val
return fq_val
elif tag == 'route':
return fmt_func(val) + "()"
elif tag == 'link':
anchor, link = val.rsplit(' ', 1)
# There's no way to have links in TSDoc, so simply use JSDoc's formatting.
# It's entirely possible some editors support this.
return '[%s]{@link %s}' % (anchor, link)
elif tag == 'val':
# Value types seem to match JavaScript (true, false, null)
return val
elif tag == 'field':
return val
else:
raise RuntimeError('Unknown doc ref tag %r' % tag)
Loading