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

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 

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

25 

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 ) 

35 

36 return self.write() 

37 

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 } 

47 

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] 

53 

54 typ = self.gen.types[uid] 

55 

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

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

58 

59 self.stack.append(typ.uid) 

60 type_name = self.make2(typ) 

61 self.stack.pop() 

62 

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

64 return type_name 

65 

66 def make2(self, typ): 

67 if typ.c == base.C.LITERAL: 

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

69 

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

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

72 

73 if typ.c == base.C.OPTIONAL: 

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

75 

76 if typ.c == base.C.TUPLE: 

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

78 

79 if typ.c == base.C.UNION: 

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

81 

82 if typ.c == base.C.VARIANT: 

83 return _pipe(self.make(it) for it in typ.tMembers.values()) 

84 

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) 

91 

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

98 

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

104 

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

110 

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

112 

113 CORE_NAME = 'core' 

114 

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 

126 

127 def make_props(self, typ): 

128 tpl = "/// $doc \n $name$opt: $type" 

129 props = [] 

130 

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

140 

141 return _nl(props) 

142 

143 ## 

144 

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 

150 

151 def write_api(self): 

152 

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

159 

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

165 

166 api_tpl = """ 

167 /** 

168 * Gws Server API. 

169 * Version $VERSION 

170 * 

171 */ 

172 

173 export const VERSION = '$VERSION'; 

174 

175 type _int = number; 

176 type _float = number; 

177 type _bytes = any; 

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

179 

180 $globs 

181 

182 $namespaces 

183 

184 export interface Api { 

185 invoke(cmd: string, r: object, options?: any): Promise<any>; 

186 $commands 

187 } 

188 """ 

189 

190 return self.format(api_tpl, globs=globs, namespaces=namespaces, commands=commands) 

191 

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 ] 

198 

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

206 

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

216 

217 

218def _indent(txt): 

219 r = [] 

220 

221 spaces = ' ' * 4 

222 indent = 0 

223 

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) 

232 

233 return _nl(r) 

234 

235 

236def _val(s): 

237 return json.dumps(s) 

238 

239 

240def _ucfirst(s): 

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

242 

243 

244_pipe = ' | '.join 

245_comma = ', '.join 

246_nl = '\n'.join 

247_nl2 = '\n\n'.join 

248 

249DOT = '.'