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