Coverage for gws-app/gws/spec/generator/typescript.py: 93%
131 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Generate the TypeScript API for the client."""
3import json
4import re
6from . import base
9def create(gen: base.Generator):
10 """Create the TypeScript API.
12 The API contains interfaces for ``gws.Request``, ``gws.Response`` and
13 ``gws.Props`` and all their subclasses, the types they use, grouped in
14 namespaces by module, and the ``Server`` interface with a typed ``call``
15 method for each API command.
17 Args:
18 gen: Generator state.
20 Returns:
21 The TypeScript source.
22 """
24 return _Creator(gen).run()
27##
30class _Creator:
31 """Builds the TypeScript declarations."""
33 def __init__(self, gen: base.Generator):
34 self.gen = gen
35 self.commands = {}
36 self.namespaces = {}
37 self.stub = []
38 self.done = {}
39 self.stack = []
40 self.tmp_names = {}
41 self.object_names = {}
43 def run(self):
44 """Create the TypeScript API.
46 Returns:
47 The TypeScript source.
48 """
50 self.make_client_classes()
51 self.make_client_commands()
52 return self.write()
54 _builtins_map = {
55 'any': 'any',
56 'bool': 'boolean',
57 'bytes': '_bytes',
58 'float': '_float',
59 'int': '_int',
60 'str': 'string',
61 'dict': '_dict',
62 }
64 def make_client_classes(self):
65 """Declare ``gws.Request``, ``gws.Response``, ``gws.Props`` and all their subclasses."""
67 queue = ['gws.Request', 'gws.Response', 'gws.Props']
68 while queue:
69 uid = queue.pop(0)
70 self.make(uid)
71 for typ in self.gen.typeDict.values():
72 if typ.c == base.c.CLASS and uid in typ.tSupers:
73 queue.append(typ.uid)
75 def make_client_commands(self):
76 """Collect the API commands with their argument and return types."""
78 for typ in self.gen.typeDict.values():
79 if typ.extName.startswith(base.v.EXT_COMMAND_API_PREFIX):
80 self.commands[typ.extName] = base.Data(
81 cmdName=typ.extName.replace(base.v.EXT_COMMAND_API_PREFIX, ''),
82 doc=typ.doc,
83 arg=self.make(typ.tArg),
84 ret=self.make(typ.tReturn),
85 )
87 def make(self, uid):
88 """Declare a type and get its TypeScript name.
90 A temporary name is used while the type is being declared, so that
91 recursive types can refer to it; it is replaced in ``write``.
93 Args:
94 uid: Type uid.
96 Returns:
97 The TypeScript type name or expression.
98 """
100 if uid in self._builtins_map:
101 return self._builtins_map[uid]
102 if uid in self.done:
103 return self.done[uid]
105 typ = self.gen.require_type(uid)
107 tmp_name = f'[TMP:%d]' % (len(self.tmp_names) + 1)
108 self.done[uid] = self.tmp_names[tmp_name] = tmp_name
110 self.stack.append(typ.uid)
111 type_name = self.make2(typ)
112 self.stack.pop()
114 self.done[uid] = self.tmp_names[tmp_name] = type_name
115 return type_name
117 def make2(self, typ):
118 """Create the TypeScript type expression or declaration for a type.
120 Args:
121 typ: Type.
123 Returns:
124 The TypeScript type name or expression.
126 Raises:
127 ``Error``: If the type kind is not supported.
128 """
130 if typ.c == base.c.LITERAL:
131 return _pipe(_val(v) for v in typ.literalValues)
133 if typ.c in {base.c.LIST, base.c.SET}:
134 return 'Array<%s>' % self.make(typ.tItem)
136 if typ.c == base.c.OPTIONAL:
137 return _pipe([self.make(typ.tTarget), 'null'])
139 if typ.c == base.c.TUPLE:
140 return '[%s]' % _comma(self.make(t) for t in typ.tItems)
142 if typ.c == base.c.UNION:
143 return _pipe(self.make(it) for it in typ.tItems)
145 if typ.c == base.c.DICT:
146 k = self.make(typ.tKey)
147 v = self.make(typ.tValue)
148 if k == 'string' and v == 'any':
149 return '_dict'
150 return '{[key: %s]: %s}' % (k, v)
152 if typ.c == base.c.CLASS:
153 return self.namespace_entry(
154 typ,
155 template='/// $doc \n export interface $name$extends { \n $props \n }',
156 props=self.make_props(typ),
157 extends=' extends ' + self.make(typ.tSupers[0]) if typ.tSupers else '',
158 )
160 if typ.c == base.c.ENUM:
161 return self.namespace_entry(
162 typ,
163 template='/// $doc \n export enum $name { \n $items \n }',
164 items=_nl('%s = %s,' % (k, _val(v)) for k, v in sorted(typ.enumValues.items())),
165 )
167 if typ.c == base.c.VARIANT:
168 target = _pipe(self.make(it) for it in typ.tMembers.values())
169 return self.namespace_entry(
170 typ,
171 template='/// $doc \n export type $name = $target;',
172 target=target,
173 )
175 if typ.c in base.c.TYPE:
176 return self.namespace_entry(
177 typ,
178 template='/// $doc \n export type $name = $target;',
179 target=self.make(typ.tTarget),
180 )
182 raise base.Error(f'unhandled type {typ.name!r}, stack: {self.stack!r}')
184 CORE_NAME = 'core'
185 """Namespace for top-level names; this part is also removed from qualified names."""
187 def namespace_entry(self, typ, template, **kwargs):
188 """Add a declaration to the namespace of a type.
190 Args:
191 typ: Type.
192 template: Declaration template with ``$name`` placeholders.
193 **kwargs: Template values.
195 Returns:
196 The qualified TypeScript name of the type.
197 """
199 ps = typ.name.split(DOT)
200 if len(ps) == 1:
201 ns, name, qname = self.CORE_NAME, ps[-1], self.CORE_NAME + DOT + ps[0]
202 else:
203 if self.CORE_NAME in ps:
204 ps.remove(self.CORE_NAME)
205 ns, name, qname = DOT.join(ps[:-1]), ps[-1], DOT.join(ps)
206 self.namespaces.setdefault(ns, []).append(self.format(template, name=name, doc=typ.doc, **kwargs))
207 return qname
209 def make_props(self, typ):
210 """Create the property declarations of a class, without inherited properties.
212 Args:
213 typ: Class type.
215 Returns:
216 The property declarations.
217 """
219 tpl = '/// $doc \n $name$opt: $type'
220 props = []
222 for name, uid in typ.tProperties.items():
223 property_typ = self.gen.require_type(uid)
224 if property_typ.tOwner == typ.name:
225 props.append(
226 self.format(tpl, name=name, doc=property_typ.doc, opt='?' if property_typ.hasDefault else '', type=self.make(property_typ.tValue))
227 )
229 return _nl(props)
231 ##
233 def write(self):
234 """Write the API and replace the temporary names.
236 Returns:
237 The indented TypeScript source.
238 """
240 text = _indent(self.write_api())
241 for tmp, name in self.tmp_names.items():
242 text = text.replace(tmp, name)
243 return text
245 def write_api(self):
246 """Fill the API template with the namespaces and commands.
248 Returns:
249 The TypeScript source.
250 """
252 api_tpl = """
253 /**
254 * Gws Server API.
255 * Version $VERSION
256 *
257 */
259 export const GWS_VERSION = '$VERSION';
261 type _int = number;
262 type _float = number;
263 type _bytes = any;
264 type _dict = {[k: string]: any};
266 $globs
268 $namespaces
270 interface _ServerArgs {
271 $server_args
272 }
274 interface _ServerReturns {
275 $server_rets
276 }
278 export interface Server {
279 call<T extends keyof _ServerArgs>(cmd: T, r: _ServerArgs[T], options?: object): Promise<_ServerReturns[T]>;
280 callAny(cmd: string, r: any, options?: object): Promise<any>;
281 }
283 export abstract class BaseServer implements Server {
284 abstract execCall(cmd, r, options?): Promise<any>;
286 call<T extends keyof _ServerArgs>(cmd: T, r: _ServerArgs[T], options?: object): Promise<_ServerReturns[T]> {
287 return this.execCall(cmd, r, options);
288 }
289 callAny(cmd: string, r: any, options?: object): Promise<any> {
290 return this.execCall(cmd, r, options);
291 }
292 }
293 """
295 namespace_tpl = 'export namespace $ns { \n $declarations \n }'
297 globs = self.format(
298 namespace_tpl,
299 ns='gws',
300 declarations=_nl2(self.namespaces.pop('gws')),
301 )
303 namespaces = _nl2(
304 [
305 self.format(
306 namespace_tpl,
307 ns=ns,
308 declarations=_nl2(d),
309 )
310 for ns, d in sorted(self.namespaces.items())
311 ]
312 )
314 server_args = _nl(f'"{cc.cmdName}": {cc.arg}' for _, cc in sorted(self.commands.items()))
315 server_rets = _nl(f'"{cc.cmdName}": {cc.ret}' for _, cc in sorted(self.commands.items()))
317 return self.format(
318 api_tpl,
319 globs=globs,
320 namespaces=namespaces,
321 server_args=server_args,
322 server_rets=server_rets,
323 )
325 def format(self, template, **kwargs):
326 """Fill a template.
328 ``$VERSION`` is set to the application version; of the ``doc`` value
329 only the first line is used.
331 Args:
332 template: Template with ``$name`` placeholders.
333 **kwargs: Template values.
335 Returns:
336 The filled template, stripped.
337 """
339 kwargs['VERSION'] = self.gen.meta['version']
340 if 'doc' in kwargs:
341 kwargs['doc'] = kwargs['doc'].split('\n')[0]
342 return re.sub(r'\$(\w+)', lambda m: kwargs[m.group(1)], template).strip()
345def _indent(txt):
346 r = []
348 spaces = ' ' * 4
349 indent = 0
351 for ln in txt.strip().split('\n'):
352 ln = ln.strip()
353 if ln == '}':
354 indent -= 1
355 ln = (spaces * indent) + ln
356 if ln.endswith('{'):
357 indent += 1
358 r.append(ln)
360 return _nl(r)
363def _val(s):
364 return json.dumps(s)
367def _ucfirst(s):
368 return s[0].upper() + s[1:]
371_pipe = ' | '.join
372_comma = ', '.join
373_nl = '\n'.join
374_nl2 = '\n\n'.join
376DOT = '.'