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

1"""Generate typescript API files from the server spec""" 

2 

3import json 

4import re 

5 

6from . import base 

7 

8 

9def create(gen: base.Generator): 

10 return _Creator(gen).run() 

11 

12 

13## 

14 

15 

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

26 

27 def run(self): 

28 self.make_client_classes() 

29 self.make_client_commands() 

30 return self.write() 

31 

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 } 

41 

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) 

50 

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 ) 

60 

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] 

66 

67 typ = self.gen.require_type(uid) 

68 

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

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

71 

72 self.stack.append(typ.uid) 

73 type_name = self.make2(typ) 

74 self.stack.pop() 

75 

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

77 return type_name 

78 

79 def make2(self, typ): 

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

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

82 

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

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

85 

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

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

88 

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

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

91 

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

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

94 

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) 

101 

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 ) 

109 

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 ) 

116 

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 ) 

124 

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 ) 

131 

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

133 

134 CORE_NAME = 'core' 

135 

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 

146 

147 def make_props(self, typ): 

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

149 props = [] 

150 

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 ) 

157 

158 return _nl(props) 

159 

160 ## 

161 

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 

167 

168 def write_api(self): 

169 api_tpl = """ 

170 /** 

171 * Gws Server API. 

172 * Version $VERSION 

173 * 

174 */ 

175 

176 export const GWS_VERSION = '$VERSION'; 

177 

178 type _int = number; 

179 type _float = number; 

180 type _bytes = any; 

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

182 

183 $globs 

184 

185 $namespaces 

186 

187 interface _ServerArgs { 

188 $server_args 

189 } 

190  

191 interface _ServerReturns { 

192 $server_rets 

193 } 

194  

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 } 

199  

200 export abstract class BaseServer implements Server { 

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

202  

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

211 

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

213 

214 globs = self.format( 

215 namespace_tpl, 

216 ns='gws', 

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

218 ) 

219 

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 ) 

230 

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

233 

234 return self.format( 

235 api_tpl, 

236 globs=globs, 

237 namespaces=namespaces, 

238 server_args=server_args, 

239 server_rets=server_rets, 

240 ) 

241 

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

247 

248 

249def _indent(txt): 

250 r = [] 

251 

252 spaces = ' ' * 4 

253 indent = 0 

254 

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) 

263 

264 return _nl(r) 

265 

266 

267def _val(s): 

268 return json.dumps(s) 

269 

270 

271def _ucfirst(s): 

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

273 

274 

275_pipe = ' | '.join 

276_comma = ', '.join 

277_nl = '\n'.join 

278_nl2 = '\n\n'.join 

279 

280DOT = '.'