Coverage for gws-app/gws/spec/generator/typescript.py: 20%
124 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +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##
15class _Creator:
16 def __init__(self, gen: base.Generator):
17 self.gen = gen
18 self.commands = {}
19 self.namespaces = {}
20 self.stub = []
21 self.done = {}
22 self.stack = []
23 self.tmp_names = {}
24 self.object_names = {}
26 def run(self):
27 for typ in self.gen.types.values():
28 if typ.extName.startswith(base.EXT_COMMAND_API_PREFIX):
29 self.commands[typ.extName] = base.Data(
30 cmdName=typ.extName.replace(base.EXT_COMMAND_API_PREFIX, ''),
31 doc=typ.doc,
32 arg=self.make(typ.tArgs[-1]),
33 ret=self.make(typ.tReturn)
34 )
36 return self.write()
38 _builtins_map = {
39 'any': 'any',
40 'bool': 'boolean',
41 'bytes': '_bytes',
42 'float': '_float',
43 'int': '_int',
44 'str': 'string',
45 'dict': '_dict',
46 }
48 def make(self, uid):
49 if uid in self._builtins_map:
50 return self._builtins_map[uid]
51 if uid in self.done:
52 return self.done[uid]
54 typ = self.gen.types[uid]
56 tmp_name = f'[TMP:%d]' % (len(self.tmp_names) + 1)
57 self.done[uid] = self.tmp_names[tmp_name] = tmp_name
59 self.stack.append(typ.uid)
60 type_name = self.make2(typ)
61 self.stack.pop()
63 self.done[uid] = self.tmp_names[tmp_name] = type_name
64 return type_name
66 def make2(self, typ):
67 if typ.c == base.C.LITERAL:
68 return _pipe(_val(v) for v in typ.literalValues)
70 if typ.c in {base.C.LIST, base.C.SET}:
71 return 'Array<%s>' % self.make(typ.tItem)
73 if typ.c == base.C.OPTIONAL:
74 return _pipe([self.make(typ.tTarget), 'null'])
76 if typ.c == base.C.TUPLE:
77 return '[%s]' % _comma(self.make(t) for t in typ.tItems)
79 if typ.c == base.C.UNION:
80 return _pipe(self.make(it) for it in typ.tItems)
82 if typ.c == base.C.VARIANT:
83 return _pipe(self.make(it) for it in typ.tMembers.values())
85 if typ.c == base.C.DICT:
86 k = self.make(typ.tKey)
87 v = self.make(typ.tValue)
88 if k == 'string' and v == 'any':
89 return '_dict'
90 return '{[key: %s]: %s}' % (k, v)
92 if typ.c == base.C.CLASS:
93 return self.namespace_entry(
94 typ,
95 template="/// $doc \n export interface $name$extends { \n $props \n }",
96 props=self.make_props(typ),
97 extends=' extends ' + self.make(typ.tSupers[0]) if typ.tSupers else '')
99 if typ.c == base.C.ENUM:
100 return self.namespace_entry(
101 typ,
102 template="/// $doc \n export enum $name { \n $items \n }",
103 items=_nl('%s = %s,' % (k, _val(v)) for k, v in sorted(typ.enumValues.items())))
105 if typ.c == base.C.TYPE:
106 return self.namespace_entry(
107 typ,
108 template="/// $doc \n export type $name = $target;",
109 target=self.make(typ.tTarget))
111 raise base.Error(f'unhandled type {typ.name!r}, stack: {self.stack!r}')
113 CORE_NAME = 'core'
115 def namespace_entry(self, typ, template, **kwargs):
116 ps = typ.name.split(DOT)
117 ps.pop(0) # remove 'gws.'
118 if len(ps) == 1:
119 ns, name, qname = self.CORE_NAME, ps[-1], self.CORE_NAME + DOT + ps[0]
120 else:
121 if self.CORE_NAME in ps:
122 ps.remove(self.CORE_NAME)
123 ns, name, qname = DOT.join(ps[:-1]), ps[-1], DOT.join(ps)
124 self.namespaces.setdefault(ns, []).append(self.format(template, name=name, doc=typ.doc, **kwargs))
125 return qname
127 def make_props(self, typ):
128 tpl = "/// $doc \n $name$opt: $type"
129 props = []
131 for name, uid in typ.tProperties.items():
132 property_typ = self.gen.types[uid]
133 if property_typ.tOwner == typ.name:
134 props.append(self.format(
135 tpl,
136 name=name,
137 doc=property_typ.doc,
138 opt='?' if property_typ.hasDefault else '',
139 type=self.make(property_typ.tValue)))
141 return _nl(props)
143 ##
145 def write(self):
146 text = _indent(self.write_api()) + '\n\n' + _indent(self.write_stub())
147 for tmp, name in self.tmp_names.items():
148 text = text.replace(tmp, name)
149 return text
151 def write_api(self):
153 namespace_tpl = "export namespace $ns { \n $declarations \n }"
154 globs = self.format(namespace_tpl, ns=self.CORE_NAME, declarations=_nl2(self.namespaces.pop(self.CORE_NAME)))
155 namespaces = _nl2([
156 self.format(namespace_tpl, ns=ns, declarations=_nl2(d))
157 for ns, d in sorted(self.namespaces.items())
158 ])
160 command_tpl = "/// $doc \n $name (p: $arg, options?: any): Promise<$ret>;"
161 commands = _nl2([
162 self.format(command_tpl, name=cc.cmdName, doc=cc.doc, arg=cc.arg, ret=cc.ret)
163 for _, cc in sorted(self.commands.items())
164 ])
166 api_tpl = """
167 /**
168 * Gws Server API.
169 * Version $VERSION
170 *
171 */
173 export const VERSION = '$VERSION';
175 type _int = number;
176 type _float = number;
177 type _bytes = any;
178 type _dict = {[k: string]: any};
180 $globs
182 $namespaces
184 export interface Api {
185 invoke(cmd: string, r: object, options?: any): Promise<any>;
186 $commands
187 }
188 """
190 return self.format(api_tpl, globs=globs, namespaces=namespaces, commands=commands)
192 def write_stub(self):
193 command_tpl = """$name(r: $arg, options?: any): Promise<$ret> { \n return this.invoke("$name", r, options); \n }"""
194 commands = [
195 self.format(command_tpl, name=cc.cmdName, doc=cc.doc, arg=cc.arg, ret=cc.ret)
196 for _, cc in sorted(self.commands.items())
197 ]
199 stub_tpl = """
200 export abstract class BaseServer implements Api {
201 abstract invoke(cmd, r, options): Promise<any>;
202 $commands
203 }
204 """
205 return self.format(stub_tpl, commands=_nl(commands))
207 def format(self, template, **kwargs):
208 kwargs['VERSION'] = self.gen.meta['version']
209 if 'doc' in kwargs:
210 kwargs['doc'] = kwargs['doc'].split('\n')[0]
211 return re.sub(
212 r'\$(\w+)',
213 lambda m: kwargs[m.group(1)],
214 template
215 ).strip()
218def _indent(txt):
219 r = []
221 spaces = ' ' * 4
222 indent = 0
224 for ln in txt.strip().split('\n'):
225 ln = ln.strip()
226 if ln == '}':
227 indent -= 1
228 ln = (spaces * indent) + ln
229 if ln.endswith('{'):
230 indent += 1
231 r.append(ln)
233 return _nl(r)
236def _val(s):
237 return json.dumps(s)
240def _ucfirst(s):
241 return s[0].upper() + s[1:]
244_pipe = ' | '.join
245_comma = ', '.join
246_nl = '\n'.join
247_nl2 = '\n\n'.join
249DOT = '.'