Coverage for gws-app/gws/spec/generator/typescript.py: 93%
130 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +0200
1"""Generate typescript API files from the server spec"""
3import json
4import re
6from . import base
9def create(gen: base.Generator):
10 return _Creator(gen).run()
13##
16class _Creator:
17 def __init__(self, gen: base.Generator):
18 self.gen = gen
19 self.commands = {}
20 self.namespaces = {}
21 self.stub = []
22 self.done = {}
23 self.stack = []
24 self.tmp_names = {}
25 self.object_names = {}
27 def run(self):
28 self.make_client_classes()
29 self.make_client_commands()
30 return self.write()
32 _builtins_map = {
33 'any': 'any',
34 'bool': 'boolean',
35 'bytes': '_bytes',
36 'float': '_float',
37 'int': '_int',
38 'str': 'string',
39 'dict': '_dict',
40 }
42 def make_client_classes(self):
43 queue = ['gws.Request', 'gws.Response', 'gws.Props']
44 while queue:
45 uid = queue.pop(0)
46 self.make(uid)
47 for typ in self.gen.typeDict.values():
48 if typ.c == base.c.CLASS and uid in typ.tSupers:
49 queue.append(typ.uid)
51 def make_client_commands(self):
52 for typ in self.gen.typeDict.values():
53 if typ.extName.startswith(base.v.EXT_COMMAND_API_PREFIX):
54 self.commands[typ.extName] = base.Data(
55 cmdName=typ.extName.replace(base.v.EXT_COMMAND_API_PREFIX, ''),
56 doc=typ.doc,
57 arg=self.make(typ.tArg),
58 ret=self.make(typ.tReturn),
59 )
61 def make(self, uid):
62 if uid in self._builtins_map:
63 return self._builtins_map[uid]
64 if uid in self.done:
65 return self.done[uid]
67 typ = self.gen.require_type(uid)
69 tmp_name = f'[TMP:%d]' % (len(self.tmp_names) + 1)
70 self.done[uid] = self.tmp_names[tmp_name] = tmp_name
72 self.stack.append(typ.uid)
73 type_name = self.make2(typ)
74 self.stack.pop()
76 self.done[uid] = self.tmp_names[tmp_name] = type_name
77 return type_name
79 def make2(self, typ):
80 if typ.c == base.c.LITERAL:
81 return _pipe(_val(v) for v in typ.literalValues)
83 if typ.c in {base.c.LIST, base.c.SET}:
84 return 'Array<%s>' % self.make(typ.tItem)
86 if typ.c == base.c.OPTIONAL:
87 return _pipe([self.make(typ.tTarget), 'null'])
89 if typ.c == base.c.TUPLE:
90 return '[%s]' % _comma(self.make(t) for t in typ.tItems)
92 if typ.c == base.c.UNION:
93 return _pipe(self.make(it) for it in typ.tItems)
95 if typ.c == base.c.DICT:
96 k = self.make(typ.tKey)
97 v = self.make(typ.tValue)
98 if k == 'string' and v == 'any':
99 return '_dict'
100 return '{[key: %s]: %s}' % (k, v)
102 if typ.c == base.c.CLASS:
103 return self.namespace_entry(
104 typ,
105 template='/// $doc \n export interface $name$extends { \n $props \n }',
106 props=self.make_props(typ),
107 extends=' extends ' + self.make(typ.tSupers[0]) if typ.tSupers else '',
108 )
110 if typ.c == base.c.ENUM:
111 return self.namespace_entry(
112 typ,
113 template='/// $doc \n export enum $name { \n $items \n }',
114 items=_nl('%s = %s,' % (k, _val(v)) for k, v in sorted(typ.enumValues.items())),
115 )
117 if typ.c == base.c.VARIANT:
118 target = _pipe(self.make(it) for it in typ.tMembers.values())
119 return self.namespace_entry(
120 typ,
121 template='/// $doc \n export type $name = $target;',
122 target=target,
123 )
125 if typ.c in base.c.TYPE:
126 return self.namespace_entry(
127 typ,
128 template='/// $doc \n export type $name = $target;',
129 target=self.make(typ.tTarget),
130 )
132 raise base.Error(f'unhandled type {typ.name!r}, stack: {self.stack!r}')
134 CORE_NAME = 'core'
136 def namespace_entry(self, typ, template, **kwargs):
137 ps = typ.name.split(DOT)
138 if len(ps) == 1:
139 ns, name, qname = self.CORE_NAME, ps[-1], self.CORE_NAME + DOT + ps[0]
140 else:
141 if self.CORE_NAME in ps:
142 ps.remove(self.CORE_NAME)
143 ns, name, qname = DOT.join(ps[:-1]), ps[-1], DOT.join(ps)
144 self.namespaces.setdefault(ns, []).append(self.format(template, name=name, doc=typ.doc, **kwargs))
145 return qname
147 def make_props(self, typ):
148 tpl = '/// $doc \n $name$opt: $type'
149 props = []
151 for name, uid in typ.tProperties.items():
152 property_typ = self.gen.require_type(uid)
153 if property_typ.tOwner == typ.name:
154 props.append(
155 self.format(tpl, name=name, doc=property_typ.doc, opt='?' if property_typ.hasDefault else '', type=self.make(property_typ.tValue))
156 )
158 return _nl(props)
160 ##
162 def write(self):
163 text = _indent(self.write_api())
164 for tmp, name in self.tmp_names.items():
165 text = text.replace(tmp, name)
166 return text
168 def write_api(self):
169 api_tpl = """
170 /**
171 * Gws Server API.
172 * Version $VERSION
173 *
174 */
176 export const GWS_VERSION = '$VERSION';
178 type _int = number;
179 type _float = number;
180 type _bytes = any;
181 type _dict = {[k: string]: any};
183 $globs
185 $namespaces
187 interface _ServerArgs {
188 $server_args
189 }
191 interface _ServerReturns {
192 $server_rets
193 }
195 export interface Server {
196 call<T extends keyof _ServerArgs>(cmd: T, r: _ServerArgs[T], options?: object): Promise<_ServerReturns[T]>;
197 callAny(cmd: string, r: any, options?: object): Promise<any>;
198 }
200 export abstract class BaseServer implements Server {
201 abstract execCall(cmd, r, options?): Promise<any>;
203 call<T extends keyof _ServerArgs>(cmd: T, r: _ServerArgs[T], options?: object): Promise<_ServerReturns[T]> {
204 return this.execCall(cmd, r, options);
205 }
206 callAny(cmd: string, r: any, options?: object): Promise<any> {
207 return this.execCall(cmd, r, options);
208 }
209 }
210 """
212 namespace_tpl = 'export namespace $ns { \n $declarations \n }'
214 globs = self.format(
215 namespace_tpl,
216 ns='gws',
217 declarations=_nl2(self.namespaces.pop('gws')),
218 )
220 namespaces = _nl2(
221 [
222 self.format(
223 namespace_tpl,
224 ns=ns,
225 declarations=_nl2(d),
226 )
227 for ns, d in sorted(self.namespaces.items())
228 ]
229 )
231 server_args = _nl(f'"{cc.cmdName}": {cc.arg}' for _, cc in sorted(self.commands.items()))
232 server_rets = _nl(f'"{cc.cmdName}": {cc.ret}' for _, cc in sorted(self.commands.items()))
234 return self.format(
235 api_tpl,
236 globs=globs,
237 namespaces=namespaces,
238 server_args=server_args,
239 server_rets=server_rets,
240 )
242 def format(self, template, **kwargs):
243 kwargs['VERSION'] = self.gen.meta['version']
244 if 'doc' in kwargs:
245 kwargs['doc'] = kwargs['doc'].split('\n')[0]
246 return re.sub(r'\$(\w+)', lambda m: kwargs[m.group(1)], template).strip()
249def _indent(txt):
250 r = []
252 spaces = ' ' * 4
253 indent = 0
255 for ln in txt.strip().split('\n'):
256 ln = ln.strip()
257 if ln == '}':
258 indent -= 1
259 ln = (spaces * indent) + ln
260 if ln.endswith('{'):
261 indent += 1
262 r.append(ln)
264 return _nl(r)
267def _val(s):
268 return json.dumps(s)
271def _ucfirst(s):
272 return s[0].upper() + s[1:]
275_pipe = ' | '.join
276_comma = ', '.join
277_nl = '\n'.join
278_nl2 = '\n\n'.join
280DOT = '.'