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

1"""Generate the TypeScript API for the client.""" 

2 

3import json 

4import re 

5 

6from . import base 

7 

8 

9def create(gen: base.Generator): 

10 """Create the TypeScript API. 

11 

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. 

16 

17 Args: 

18 gen: Generator state. 

19 

20 Returns: 

21 The TypeScript source. 

22 """ 

23 

24 return _Creator(gen).run() 

25 

26 

27## 

28 

29 

30class _Creator: 

31 """Builds the TypeScript declarations.""" 

32 

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 = {} 

42 

43 def run(self): 

44 """Create the TypeScript API. 

45 

46 Returns: 

47 The TypeScript source. 

48 """ 

49 

50 self.make_client_classes() 

51 self.make_client_commands() 

52 return self.write() 

53 

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 } 

63 

64 def make_client_classes(self): 

65 """Declare ``gws.Request``, ``gws.Response``, ``gws.Props`` and all their subclasses.""" 

66 

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) 

74 

75 def make_client_commands(self): 

76 """Collect the API commands with their argument and return types.""" 

77 

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 ) 

86 

87 def make(self, uid): 

88 """Declare a type and get its TypeScript name. 

89 

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``. 

92 

93 Args: 

94 uid: Type uid. 

95 

96 Returns: 

97 The TypeScript type name or expression. 

98 """ 

99 

100 if uid in self._builtins_map: 

101 return self._builtins_map[uid] 

102 if uid in self.done: 

103 return self.done[uid] 

104 

105 typ = self.gen.require_type(uid) 

106 

107 tmp_name = f'[TMP:%d]' % (len(self.tmp_names) + 1) 

108 self.done[uid] = self.tmp_names[tmp_name] = tmp_name 

109 

110 self.stack.append(typ.uid) 

111 type_name = self.make2(typ) 

112 self.stack.pop() 

113 

114 self.done[uid] = self.tmp_names[tmp_name] = type_name 

115 return type_name 

116 

117 def make2(self, typ): 

118 """Create the TypeScript type expression or declaration for a type. 

119 

120 Args: 

121 typ: Type. 

122 

123 Returns: 

124 The TypeScript type name or expression. 

125 

126 Raises: 

127 ``Error``: If the type kind is not supported. 

128 """ 

129 

130 if typ.c == base.c.LITERAL: 

131 return _pipe(_val(v) for v in typ.literalValues) 

132 

133 if typ.c in {base.c.LIST, base.c.SET}: 

134 return 'Array<%s>' % self.make(typ.tItem) 

135 

136 if typ.c == base.c.OPTIONAL: 

137 return _pipe([self.make(typ.tTarget), 'null']) 

138 

139 if typ.c == base.c.TUPLE: 

140 return '[%s]' % _comma(self.make(t) for t in typ.tItems) 

141 

142 if typ.c == base.c.UNION: 

143 return _pipe(self.make(it) for it in typ.tItems) 

144 

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) 

151 

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 ) 

159 

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 ) 

166 

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 ) 

174 

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 ) 

181 

182 raise base.Error(f'unhandled type {typ.name!r}, stack: {self.stack!r}') 

183 

184 CORE_NAME = 'core' 

185 """Namespace for top-level names; this part is also removed from qualified names.""" 

186 

187 def namespace_entry(self, typ, template, **kwargs): 

188 """Add a declaration to the namespace of a type. 

189 

190 Args: 

191 typ: Type. 

192 template: Declaration template with ``$name`` placeholders. 

193 **kwargs: Template values. 

194 

195 Returns: 

196 The qualified TypeScript name of the type. 

197 """ 

198 

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 

208 

209 def make_props(self, typ): 

210 """Create the property declarations of a class, without inherited properties. 

211 

212 Args: 

213 typ: Class type. 

214 

215 Returns: 

216 The property declarations. 

217 """ 

218 

219 tpl = '/// $doc \n $name$opt: $type' 

220 props = [] 

221 

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 ) 

228 

229 return _nl(props) 

230 

231 ## 

232 

233 def write(self): 

234 """Write the API and replace the temporary names. 

235 

236 Returns: 

237 The indented TypeScript source. 

238 """ 

239 

240 text = _indent(self.write_api()) 

241 for tmp, name in self.tmp_names.items(): 

242 text = text.replace(tmp, name) 

243 return text 

244 

245 def write_api(self): 

246 """Fill the API template with the namespaces and commands. 

247 

248 Returns: 

249 The TypeScript source. 

250 """ 

251 

252 api_tpl = """ 

253 /** 

254 * Gws Server API. 

255 * Version $VERSION 

256 * 

257 */ 

258 

259 export const GWS_VERSION = '$VERSION'; 

260 

261 type _int = number; 

262 type _float = number; 

263 type _bytes = any; 

264 type _dict = {[k: string]: any}; 

265 

266 $globs 

267 

268 $namespaces 

269 

270 interface _ServerArgs { 

271 $server_args 

272 } 

273  

274 interface _ServerReturns { 

275 $server_rets 

276 } 

277  

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 } 

282  

283 export abstract class BaseServer implements Server { 

284 abstract execCall(cmd, r, options?): Promise<any>; 

285  

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 """ 

294 

295 namespace_tpl = 'export namespace $ns { \n $declarations \n }' 

296 

297 globs = self.format( 

298 namespace_tpl, 

299 ns='gws', 

300 declarations=_nl2(self.namespaces.pop('gws')), 

301 ) 

302 

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 ) 

313 

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())) 

316 

317 return self.format( 

318 api_tpl, 

319 globs=globs, 

320 namespaces=namespaces, 

321 server_args=server_args, 

322 server_rets=server_rets, 

323 ) 

324 

325 def format(self, template, **kwargs): 

326 """Fill a template. 

327 

328 ``$VERSION`` is set to the application version; of the ``doc`` value 

329 only the first line is used. 

330 

331 Args: 

332 template: Template with ``$name`` placeholders. 

333 **kwargs: Template values. 

334 

335 Returns: 

336 The filled template, stripped. 

337 """ 

338 

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() 

343 

344 

345def _indent(txt): 

346 r = [] 

347 

348 spaces = ' ' * 4 

349 indent = 0 

350 

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) 

359 

360 return _nl(r) 

361 

362 

363def _val(s): 

364 return json.dumps(s) 

365 

366 

367def _ucfirst(s): 

368 return s[0].upper() + s[1:] 

369 

370 

371_pipe = ' | '.join 

372_comma = ', '.join 

373_nl = '\n'.join 

374_nl2 = '\n\n'.join 

375 

376DOT = '.'