github.com/tada-team/tdproto@v1.51.57/codegen/typescript/main.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "os" 6 "sort" 7 "text/template" 8 9 "github.com/tada-team/tdproto/codegen" 10 ) 11 12 func main() { 13 tdprotoInfo, err := codegen.ParseTdproto() 14 if err != nil { 15 panic(err) 16 } 17 18 if err := generateTypeScript(tdprotoInfo.TdModels); err != nil { 19 panic(err) 20 } 21 } 22 23 var tsTypesMap = map[string]string{ 24 "string": "string", 25 "int": "number", 26 "int32": "number", 27 "int64": "number", // TODO: must be string 28 "uint": "number", 29 "uint16": "number", 30 "uint32": "number", 31 "uint64": "number", 32 "float": "number", 33 "float32": "number", 34 "float64": "number", 35 "bool": "boolean", 36 "interface{}": "any", 37 "ISODateTimeString": "string", 38 "time.Time": "string", 39 "UiSettings": "UiSettings", 40 "UiSettingsData": "UiSettingsData", 41 } 42 43 var tsFieldNameSubstitutions = map[string]string{ 44 "Default": "isDefault", 45 "New": "isNew", 46 "Public": "isPublic", 47 "Static": "isStatic", 48 } 49 50 type TypeScriptSumType struct { 51 Name string 52 Values []string 53 } 54 55 type TypeScriptTypeAliasInfo struct { 56 Name string 57 BaseType string 58 } 59 60 type TypeScriptFieldInfo struct { 61 Name string 62 JsonName string 63 IsReadOnly bool 64 IsOmitEmpty bool 65 TypeName string 66 IsNotPrimitive bool 67 IsList bool 68 Help string 69 } 70 71 type TypeScriptClassInfo struct { 72 Name string 73 Fields []TypeScriptFieldInfo 74 Help string 75 } 76 77 type TypeScriptInfo struct { 78 Classes []TypeScriptClassInfo 79 TypesAliases []TypeScriptTypeAliasInfo 80 SumTypes []TypeScriptSumType 81 } 82 83 func getHelpClass(input string) string { 84 if input != "" { 85 return input 86 } else { 87 return "MISSING CLASS DOCUMENTATION" 88 } 89 } 90 91 func getHelpField(input string) string { 92 if input != "" { 93 return input 94 } else { 95 return "DOCUMENTATION MISSING" 96 } 97 } 98 99 func convertTdprotoInfoToTypeScript(tdprotoInfo *codegen.TdPackage) (tsInfo TypeScriptInfo, err error) { 100 var unwrapStructArrays = make(map[string]string) 101 var enumTypes = make(map[string]string) 102 103 for _, tdprotoEnumInfo := range tdprotoInfo.GetEnums() { 104 var tsEnumValues []string 105 tsEnumValues = append(tsEnumValues, tdprotoEnumInfo.Values...) 106 107 tsInfo.SumTypes = append(tsInfo.SumTypes, TypeScriptSumType{ 108 Name: tdprotoEnumInfo.Name, 109 Values: tsEnumValues, 110 }) 111 112 tsTypesMap[tdprotoEnumInfo.Name] = tdprotoEnumInfo.Name 113 enumTypes[tdprotoEnumInfo.Name] = "" 114 } 115 116 for _, tdprotoTypeInfo := range tdprotoInfo.TdTypes { 117 _, isEnum := enumTypes[tdprotoTypeInfo.Name] 118 119 if isEnum { 120 // Enums are handled elsewhere 121 continue 122 } 123 124 // Unwrap arrays of structs 125 if tdprotoTypeInfo.IsArray { 126 unwrapStructArrays[tdprotoTypeInfo.Name] = tdprotoTypeInfo.BaseType 127 continue 128 } 129 130 typeAliasName := tdprotoTypeInfo.Name 131 typeAliasBaseType := tsTypesMap[tdprotoTypeInfo.BaseType] 132 133 tsNewTypeAlias := TypeScriptTypeAliasInfo{ 134 Name: typeAliasName, 135 BaseType: typeAliasBaseType, 136 } 137 138 tsInfo.TypesAliases = append(tsInfo.TypesAliases, tsNewTypeAlias) 139 tsTypesMap[typeAliasName] = typeAliasName 140 } 141 142 for _, tdprotoStructInfo := range tdprotoInfo.TdStructs { 143 tsNewClass := TypeScriptClassInfo{ 144 Name: codegen.UppercaseFirstLetter(tdprotoStructInfo.Name), 145 Help: getHelpClass(tdprotoStructInfo.Help), 146 } 147 148 for _, tdprotoStructField := range tdprotoStructInfo.GetAllJsonFields(tdprotoInfo) { 149 if tdprotoStructField.IsNotSerialized { 150 continue 151 } 152 153 tsFieldName, isSubstituted := tsFieldNameSubstitutions[tdprotoStructField.Name] 154 if !isSubstituted { 155 tsFieldName = codegen.SnakeCaseToLowerCamel(tdprotoStructField.JsonName) 156 } 157 158 isNotPrimitive := false 159 160 tsTypeName, ok := tsTypesMap[tdprotoStructField.TypeStr] 161 if !ok { 162 tsTypeName = codegen.UppercaseFirstLetter(tdprotoStructField.TypeStr) 163 isNotPrimitive = true 164 } 165 166 isList := tdprotoStructField.IsList 167 unwrappedTypeName, doUnwrap := unwrapStructArrays[tsTypeName] 168 if doUnwrap { 169 tsTypeName = unwrappedTypeName 170 isList = true 171 } 172 if tdprotoStructField.KeyTypeStr != "" { 173 tsTypeName = fmt.Sprintf("Record<%s, %s>", tdprotoStructField.KeyTypeStr, tsTypeName) 174 } 175 176 tsNewClass.Fields = append(tsNewClass.Fields, TypeScriptFieldInfo{ 177 Name: tsFieldName, 178 JsonName: tdprotoStructField.JsonName, 179 IsReadOnly: tdprotoStructField.IsReadOnly, 180 IsOmitEmpty: tdprotoStructField.IsOmitEmpty, 181 TypeName: tsTypeName, 182 IsNotPrimitive: isNotPrimitive, 183 IsList: isList, 184 Help: getHelpField(tdprotoStructField.Help), 185 }) 186 } 187 188 // Put non-optional arguments before optional 189 sort.Slice(tsNewClass.Fields, func(i, j int) bool { 190 if tsNewClass.Fields[i].IsOmitEmpty != tsNewClass.Fields[j].IsOmitEmpty { 191 return !tsNewClass.Fields[i].IsOmitEmpty && tsNewClass.Fields[j].IsOmitEmpty 192 } else { 193 return tsNewClass.Fields[i].Name < tsNewClass.Fields[j].Name 194 } 195 }) 196 197 tsInfo.Classes = append(tsInfo.Classes, tsNewClass) 198 } 199 200 // Sort everything by name 201 sort.Slice(tsInfo.Classes, func(i, j int) bool { 202 return tsInfo.Classes[i].Name < tsInfo.Classes[j].Name 203 }) 204 205 sort.Slice(tsInfo.SumTypes, func(i, j int) bool { 206 return tsInfo.SumTypes[i].Name < tsInfo.SumTypes[j].Name 207 }) 208 209 sort.Slice(tsInfo.TypesAliases, func(i, j int) bool { 210 return tsInfo.TypesAliases[i].Name < tsInfo.TypesAliases[j].Name 211 }) 212 213 return tsInfo, nil 214 } 215 216 func generateTypeScript(tdprotoInfo *codegen.TdPackage) error { 217 tsInfo, err := convertTdprotoInfoToTypeScript(tdprotoInfo) 218 if err != nil { 219 return err 220 } 221 222 _, _ = fmt.Fprint(os.Stdout, tsHeader) 223 224 for _, tsSumTypeInfo := range tsInfo.SumTypes { 225 if err := tsSumTypesTemplate.Execute(os.Stdout, tsSumTypeInfo); err != nil { 226 return err 227 } 228 } 229 230 for _, tsTypeAliasInfo := range tsInfo.TypesAliases { 231 if err := tsTypeTemplate.Execute(os.Stdout, tsTypeAliasInfo); err != nil { 232 return err 233 } 234 } 235 236 _, err = fmt.Fprint(os.Stdout, tsManualClasses) 237 if err != nil { 238 return nil 239 } 240 241 for _, tsClassInfo := range tsInfo.Classes { 242 if err := tsInterfaceTemplate.Execute(os.Stdout, tsClassInfo); err != nil { 243 return err 244 } 245 } 246 247 return nil 248 } 249 250 const tsHeader = `/* eslint-disable @typescript-eslint/no-use-before-define */ 251 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 252 /* eslint-disable @typescript-eslint/no-unused-vars */ 253 254 interface TDProtoClass<T> { 255 readonly mappableFields: ReadonlyArray<keyof T>; 256 } 257 258 259 // eslint-disable-next-line @typescript-eslint/no-explicit-any 260 export type UiSettings = Record<string, any> 261 // eslint-disable-next-line @typescript-eslint/no-explicit-any 262 export type UiSettingsData = Record<string, any> 263 264 ` 265 266 const tsManualClasses = ` 267 export interface TeamUnreadJSON { 268 /* eslint-disable camelcase */ 269 direct: UnreadJSON; 270 group: UnreadJSON; 271 task: UnreadJSON; 272 meeting: UnreadJSON; 273 thread: UnreadJSON; 274 /* eslint-enable camelcase */ 275 } 276 277 export class TeamUnread implements TDProtoClass<TeamUnread> { 278 constructor ( 279 public direct: Unread, 280 public group: Unread, 281 public task: Unread, 282 public meeting: Unread, 283 public thread: Unread, 284 ) {} 285 286 public static fromJSON (raw: TeamUnreadJSON): TeamUnread { 287 return new TeamUnread( 288 Unread.fromJSON(raw.direct), 289 Unread.fromJSON(raw.group), 290 Unread.fromJSON(raw.task), 291 Unread.fromJSON(raw.meeting), 292 Unread.fromJSON(raw.thread), 293 ) 294 } 295 296 public mappableFields = [ 297 'direct', 298 'group', 299 'task', 300 'meeting', 301 'thread', 302 ] as const 303 304 readonly #mapper = { 305 /* eslint-disable camelcase */ 306 direct: () => ({ direct: this.direct.toJSON() }), 307 group: () => ({ group: this.group.toJSON() }), 308 task: () => ({ task: this.task.toJSON() }), 309 meeting: () => ({ meeting: this.meeting.toJSON() }), 310 thread: () => ({ thread: this.thread.toJSON() }), 311 /* eslint-enable camelcase */ 312 } 313 314 public toJSON (): TeamUnreadJSON 315 public toJSON (fields: Array<this['mappableFields'][number]>): Partial<TeamUnreadJSON> 316 public toJSON (fields?: Array<this['mappableFields'][number]>) { 317 if (fields && fields.length > 0) { 318 return Object.assign({}, ...fields.map(f => this.#mapper[f]())) 319 } else { 320 return Object.assign({}, ...Object.values(this.#mapper).map(v => v())) 321 } 322 } 323 } 324 325 ` 326 327 var tsInterfaceTemplate = template.Must(template.New("tsInterface").Parse(`export interface {{.Name -}}JSON { 328 /* eslint-disable camelcase */ 329 {{- range $field := .Fields}} 330 {{- if eq $field.TypeName "any"}} 331 // eslint-disable-next-line @typescript-eslint/no-explicit-any{{- end}} 332 {{$field.JsonName}}{{if $field.IsOmitEmpty}}?{{end}}: {{$field.TypeName -}} 333 {{- if $field.IsNotPrimitive -}}JSON{{end}} 334 {{- if $field.IsList -}}[]{{end}};{{end}} 335 /* eslint-enable camelcase */ 336 } 337 338 export class {{.Name}} implements TDProtoClass<{{- .Name -}}> { 339 /** 340 * {{.Help}} 341 {{range $field := .Fields}}* @param {{$field.Name}} {{$field.Help}} 342 {{end}}*/ 343 constructor ( 344 {{- range $field := .Fields}} 345 {{- if eq $field.TypeName "any"}} 346 // eslint-disable-next-line @typescript-eslint/no-explicit-any{{- end}} 347 public {{if $field.IsReadOnly}}readonly {{end}}{{$field.Name}}{{if $field.IsOmitEmpty}}?{{end}}: {{$field.TypeName -}} 348 {{- if $field.IsList -}}[]{{end}},{{end}} 349 ) {} 350 351 public static fromJSON (raw: {{.Name -}}JSON): {{.Name}} { 352 return new {{.Name -}}( 353 {{- range $field := .Fields}} 354 {{- if $field.IsNotPrimitive}} 355 {{if $field.IsOmitEmpty -}} raw.{{- $field.JsonName}} && {{end}} 356 {{- if $field.IsList}}raw.{{- $field.JsonName}}.map({{$field.TypeName}}.fromJSON) 357 {{- else -}} {{- $field.TypeName -}}.fromJSON(raw.{{- $field.JsonName -}}){{end}} 358 {{- else}} 359 raw.{{- $field.JsonName -}} 360 {{- end -}} 361 ,{{end}} 362 ) 363 } 364 365 public mappableFields = [ 366 {{- range $field := .Fields}} 367 '{{- $field.Name -}}',{{end}} 368 ] as const 369 370 readonly #mapper = { 371 /* eslint-disable camelcase */ 372 {{- range $field := .Fields}} 373 {{$field.Name -}}: () => ({ {{$field.JsonName -}}: this.{{$field.Name}} 374 {{- if $field.IsNotPrimitive -}}{{- if $field.IsOmitEmpty}}?{{end}} 375 {{- if $field.IsList}}.map(u => u.toJSON()){{else}}.toJSON(){{end}}{{end}} }),{{end}} 376 /* eslint-enable camelcase */ 377 } 378 379 public toJSON (): {{.Name -}}JSON 380 public toJSON (fields: Array<this['mappableFields'][number]>): Partial<{{- .Name -}}JSON> 381 public toJSON (fields?: Array<this['mappableFields'][number]>) { 382 if (fields && fields.length > 0) { 383 return Object.assign({}, ...fields.map(f => this.#mapper[f]())) 384 } else { 385 return Object.assign({}, ...Object.values(this.#mapper).map(v => v())) 386 } 387 } 388 } 389 390 `)) 391 392 var tsTypeTemplate = template.Must(template.New("tsType").Parse(` 393 export type {{.Name}} = {{.BaseType}} 394 395 `)) 396 397 var tsSumTypesTemplate = template.Must(template.New("tsSumTypes").Parse(`export type {{.Name}} = 398 {{range $value := .Values}} | '{{- $value -}}' 399 {{end}} 400 `))