github.com/AlpineAIO/wails/v2@v2.0.0-beta.32.0.20240505041856-1047a8fa5fef/internal/typescriptify/typescriptify.go (about)

     1  package typescriptify
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"log"
     8  	"os"
     9  	"path"
    10  	"reflect"
    11  	"regexp"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/leaanthony/slicer"
    16  
    17  	"github.com/tkrajina/go-reflector/reflector"
    18  )
    19  
    20  const (
    21  	tsTransformTag      = "ts_transform"
    22  	tsType              = "ts_type"
    23  	tsConvertValuesFunc = `convertValues(a: any, classs: any, asMap: boolean = false): any {
    24  	if (!a) {
    25  		return a;
    26  	}
    27  	if (a.slice) {
    28  		return (a as any[]).map(elem => this.convertValues(elem, classs));
    29  	} else if ("object" === typeof a) {
    30  		if (asMap) {
    31  			for (const key of Object.keys(a)) {
    32  				a[key] = new classs(a[key]);
    33  			}
    34  			return a;
    35  		}
    36  		return new classs(a);
    37  	}
    38  	return a;
    39  }`
    40  	jsVariableNameRegex = `^([A-Z]|[a-z]|\$|_)([A-Z]|[a-z]|[0-9]|\$|_)*$`
    41  )
    42  
    43  // TypeOptions overrides options set by `ts_*` tags.
    44  type TypeOptions struct {
    45  	TSType      string
    46  	TSTransform string
    47  }
    48  
    49  // StructType stores settings for transforming one Golang struct.
    50  type StructType struct {
    51  	Type         reflect.Type
    52  	FieldOptions map[reflect.Type]TypeOptions
    53  }
    54  
    55  func NewStruct(i interface{}) *StructType {
    56  	return &StructType{
    57  		Type: reflect.TypeOf(i),
    58  	}
    59  }
    60  
    61  func (st *StructType) WithFieldOpts(i interface{}, opts TypeOptions) *StructType {
    62  	if st.FieldOptions == nil {
    63  		st.FieldOptions = map[reflect.Type]TypeOptions{}
    64  	}
    65  	var typ reflect.Type
    66  	if ty, is := i.(reflect.Type); is {
    67  		typ = ty
    68  	} else {
    69  		typ = reflect.TypeOf(i)
    70  	}
    71  	st.FieldOptions[typ] = opts
    72  	return st
    73  }
    74  
    75  type EnumType struct {
    76  	Type reflect.Type
    77  }
    78  
    79  type enumElement struct {
    80  	value interface{}
    81  	name  string
    82  }
    83  
    84  type TypeScriptify struct {
    85  	Prefix            string
    86  	Suffix            string
    87  	Indent            string
    88  	CreateFromMethod  bool
    89  	CreateConstructor bool
    90  	BackupDir         string // If empty no backup
    91  	DontExport        bool
    92  	CreateInterface   bool
    93  	customImports     []string
    94  
    95  	structTypes []StructType
    96  	enumTypes   []EnumType
    97  	enums       map[reflect.Type][]enumElement
    98  	kinds       map[reflect.Kind]string
    99  
   100  	fieldTypeOptions map[reflect.Type]TypeOptions
   101  
   102  	// throwaway, used when converting
   103  	alreadyConverted map[string]bool
   104  
   105  	Namespace    string
   106  	KnownStructs *slicer.StringSlicer
   107  	KnownEnums   *slicer.StringSlicer
   108  }
   109  
   110  func New() *TypeScriptify {
   111  	result := new(TypeScriptify)
   112  	result.Indent = "\t"
   113  	result.BackupDir = "."
   114  
   115  	kinds := make(map[reflect.Kind]string)
   116  
   117  	kinds[reflect.Bool] = "boolean"
   118  	kinds[reflect.Interface] = "any"
   119  
   120  	kinds[reflect.Int] = "number"
   121  	kinds[reflect.Int8] = "number"
   122  	kinds[reflect.Int16] = "number"
   123  	kinds[reflect.Int32] = "number"
   124  	kinds[reflect.Int64] = "number"
   125  	kinds[reflect.Uint] = "number"
   126  	kinds[reflect.Uint8] = "number"
   127  	kinds[reflect.Uint16] = "number"
   128  	kinds[reflect.Uint32] = "number"
   129  	kinds[reflect.Uint64] = "number"
   130  	kinds[reflect.Float32] = "number"
   131  	kinds[reflect.Float64] = "number"
   132  
   133  	kinds[reflect.String] = "string"
   134  
   135  	result.kinds = kinds
   136  
   137  	result.Indent = "    "
   138  	result.CreateFromMethod = true
   139  	result.CreateConstructor = true
   140  
   141  	return result
   142  }
   143  
   144  func (t *TypeScriptify) deepFields(typeOf reflect.Type) []reflect.StructField {
   145  	fields := make([]reflect.StructField, 0)
   146  
   147  	if typeOf.Kind() == reflect.Ptr {
   148  		typeOf = typeOf.Elem()
   149  	}
   150  
   151  	if typeOf.Kind() != reflect.Struct {
   152  		return fields
   153  	}
   154  
   155  	for i := 0; i < typeOf.NumField(); i++ {
   156  		f := typeOf.Field(i)
   157  		kind := f.Type.Kind()
   158  		isPointer := kind == reflect.Ptr && f.Type.Elem().Kind() == reflect.Struct
   159  		if f.Anonymous && kind == reflect.Struct {
   160  			// fmt.Println(v.Interface())
   161  			fields = append(fields, t.deepFields(f.Type)...)
   162  		} else if f.Anonymous && isPointer {
   163  			// fmt.Println(v.Interface())
   164  			fields = append(fields, t.deepFields(f.Type.Elem())...)
   165  		} else {
   166  			// Check we have a json tag
   167  			jsonTag := t.getJSONFieldName(f, isPointer)
   168  			if jsonTag != "" {
   169  				fields = append(fields, f)
   170  			}
   171  		}
   172  	}
   173  
   174  	return fields
   175  }
   176  
   177  func (ts TypeScriptify) logf(depth int, s string, args ...interface{}) {
   178  	fmt.Printf(strings.Repeat("   ", depth)+s+"\n", args...)
   179  }
   180  
   181  // ManageType can define custom options for fields of a specified type.
   182  //
   183  // This can be used instead of setting ts_type and ts_transform for all fields of a certain type.
   184  func (t *TypeScriptify) ManageType(fld interface{}, opts TypeOptions) *TypeScriptify {
   185  	var typ reflect.Type
   186  	switch t := fld.(type) {
   187  	case reflect.Type:
   188  		typ = t
   189  	default:
   190  		typ = reflect.TypeOf(fld)
   191  	}
   192  	if t.fieldTypeOptions == nil {
   193  		t.fieldTypeOptions = map[reflect.Type]TypeOptions{}
   194  	}
   195  	t.fieldTypeOptions[typ] = opts
   196  	return t
   197  }
   198  
   199  func (t *TypeScriptify) GetGeneratedStructs() []string {
   200  	var result []string
   201  	for key := range t.alreadyConverted {
   202  		result = append(result, key)
   203  	}
   204  	return result
   205  }
   206  
   207  func (t *TypeScriptify) WithCreateFromMethod(b bool) *TypeScriptify {
   208  	t.CreateFromMethod = b
   209  	return t
   210  }
   211  
   212  func (t *TypeScriptify) WithInterface(b bool) *TypeScriptify {
   213  	t.CreateInterface = b
   214  	return t
   215  }
   216  
   217  func (t *TypeScriptify) WithConstructor(b bool) *TypeScriptify {
   218  	t.CreateConstructor = b
   219  	return t
   220  }
   221  
   222  func (t *TypeScriptify) WithIndent(i string) *TypeScriptify {
   223  	t.Indent = i
   224  	return t
   225  }
   226  
   227  func (t *TypeScriptify) WithBackupDir(b string) *TypeScriptify {
   228  	t.BackupDir = b
   229  	return t
   230  }
   231  
   232  func (t *TypeScriptify) WithPrefix(p string) *TypeScriptify {
   233  	t.Prefix = p
   234  	return t
   235  }
   236  
   237  func (t *TypeScriptify) WithSuffix(s string) *TypeScriptify {
   238  	t.Suffix = s
   239  	return t
   240  }
   241  
   242  func (t *TypeScriptify) Add(obj interface{}) *TypeScriptify {
   243  	switch ty := obj.(type) {
   244  	case StructType:
   245  		t.structTypes = append(t.structTypes, ty)
   246  	case *StructType:
   247  		t.structTypes = append(t.structTypes, *ty)
   248  	case reflect.Type:
   249  		t.AddType(ty)
   250  	default:
   251  		t.AddType(reflect.TypeOf(obj))
   252  	}
   253  	return t
   254  }
   255  
   256  func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify {
   257  	t.structTypes = append(t.structTypes, StructType{Type: typeOf})
   258  	return t
   259  }
   260  
   261  func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) {
   262  	keyType := field.Type.Key()
   263  	valueType := field.Type.Elem()
   264  	valueTypeName := valueType.Name()
   265  	if name, ok := t.types[valueType.Kind()]; ok {
   266  		valueTypeName = name
   267  	}
   268  	if valueType.Kind() == reflect.Array || valueType.Kind() == reflect.Slice {
   269  		valueTypeName = valueType.Elem().Name() + "[]"
   270  	}
   271  	if valueType.Kind() == reflect.Ptr {
   272  		valueTypeName = valueType.Elem().Name()
   273  	}
   274  	if valueType.Kind() == reflect.Struct && differentNamespaces(t.namespace, valueType) {
   275  		valueTypeName = valueType.String()
   276  	}
   277  	strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
   278  	isOptional := strings.HasSuffix(fieldName, "?")
   279  
   280  	keyTypeStr := ""
   281  	// Key should always be a JS primitive. JS will read it as a string either way.
   282  	if typeStr, isSimple := t.types[keyType.Kind()]; isSimple {
   283  		keyTypeStr = typeStr
   284  	} else {
   285  		keyTypeStr = t.types[reflect.String]
   286  	}
   287  
   288  	var dotField string
   289  	if regexp.MustCompile(jsVariableNameRegex).Match([]byte(strippedFieldName)) {
   290  		dotField = fmt.Sprintf(".%s", strippedFieldName)
   291  	} else {
   292  		dotField = fmt.Sprintf(`["%s"]`, strippedFieldName)
   293  		if isOptional {
   294  			fieldName = fmt.Sprintf(`"%s"?`, strippedFieldName)
   295  		}
   296  	}
   297  	t.fields = append(t.fields, fmt.Sprintf("%s%s: {[key: %s]: %s};", t.indent, fieldName, keyTypeStr, valueTypeName))
   298  	if valueType.Kind() == reflect.Struct {
   299  		t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = this.convertValues(source[\"%s\"], %s, true);", t.indent, t.indent, dotField, strippedFieldName, t.prefix+valueTypeName+t.suffix))
   300  	} else {
   301  		t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = source[\"%s\"];", t.indent, t.indent, dotField, strippedFieldName))
   302  	}
   303  }
   304  
   305  func (t *TypeScriptify) AddEnum(values interface{}) *TypeScriptify {
   306  	if t.enums == nil {
   307  		t.enums = map[reflect.Type][]enumElement{}
   308  	}
   309  	items := reflect.ValueOf(values)
   310  	if items.Kind() != reflect.Slice {
   311  		panic(fmt.Sprintf("Values for %T isn't a slice", values))
   312  	}
   313  
   314  	var elements []enumElement
   315  	for i := 0; i < items.Len(); i++ {
   316  		item := items.Index(i)
   317  
   318  		var el enumElement
   319  		if item.Kind() == reflect.Struct {
   320  			r := reflector.New(item.Interface())
   321  			val, err := r.Field("Value").Get()
   322  			if err != nil {
   323  				panic(fmt.Sprint("missing Type field in ", item.Type().String()))
   324  			}
   325  			name, err := r.Field("TSName").Get()
   326  			if err != nil {
   327  				panic(fmt.Sprint("missing TSName field in ", item.Type().String()))
   328  			}
   329  			el.value = val
   330  			el.name = name.(string)
   331  		} else {
   332  			el.value = item.Interface()
   333  			if tsNamer, is := item.Interface().(TSNamer); is {
   334  				el.name = tsNamer.TSName()
   335  			} else {
   336  				panic(fmt.Sprint(item.Type().String(), " has no TSName method"))
   337  			}
   338  		}
   339  
   340  		elements = append(elements, el)
   341  	}
   342  	ty := reflect.TypeOf(elements[0].value)
   343  	t.enums[ty] = elements
   344  	t.enumTypes = append(t.enumTypes, EnumType{Type: ty})
   345  
   346  	return t
   347  }
   348  
   349  // AddEnumValues is deprecated, use `AddEnum()`
   350  func (t *TypeScriptify) AddEnumValues(typeOf reflect.Type, values interface{}) *TypeScriptify {
   351  	t.AddEnum(values)
   352  	return t
   353  }
   354  
   355  func (t *TypeScriptify) Convert(customCode map[string]string) (string, error) {
   356  	t.alreadyConverted = make(map[string]bool)
   357  	depth := 0
   358  
   359  	result := ""
   360  	if len(t.customImports) > 0 {
   361  		// Put the custom imports, i.e.: `import Decimal from 'decimal.js'`
   362  		for _, cimport := range t.customImports {
   363  			result += cimport + "\n"
   364  		}
   365  	}
   366  
   367  	for _, enumTyp := range t.enumTypes {
   368  		elements := t.enums[enumTyp.Type]
   369  		typeScriptCode, err := t.convertEnum(depth, enumTyp.Type, elements)
   370  		if err != nil {
   371  			return "", err
   372  		}
   373  		result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n")
   374  	}
   375  
   376  	for _, strctTyp := range t.structTypes {
   377  		typeScriptCode, err := t.convertType(depth, strctTyp.Type, customCode)
   378  		if err != nil {
   379  			return "", err
   380  		}
   381  		result += "\n" + strings.Trim(typeScriptCode, " "+t.Indent+"\r\n")
   382  	}
   383  	return result, nil
   384  }
   385  
   386  func loadCustomCode(fileName string) (map[string]string, error) {
   387  	result := make(map[string]string)
   388  	f, err := os.Open(fileName)
   389  	if err != nil {
   390  		if os.IsNotExist(err) {
   391  			return result, nil
   392  		}
   393  		return result, err
   394  	}
   395  	defer f.Close()
   396  
   397  	bytes, err := ioutil.ReadAll(f)
   398  	if err != nil {
   399  		return result, err
   400  	}
   401  
   402  	var currentName string
   403  	var currentValue string
   404  	lines := strings.Split(string(bytes), "\n")
   405  	for _, line := range lines {
   406  		trimmedLine := strings.TrimSpace(line)
   407  		if strings.HasPrefix(trimmedLine, "//[") && strings.HasSuffix(trimmedLine, ":]") {
   408  			currentName = strings.Replace(strings.Replace(trimmedLine, "//[", "", -1), ":]", "", -1)
   409  			currentValue = ""
   410  		} else if trimmedLine == "//[end]" {
   411  			result[currentName] = strings.TrimRight(currentValue, " \t\r\n")
   412  			currentName = ""
   413  			currentValue = ""
   414  		} else if len(currentName) > 0 {
   415  			currentValue += line + "\n"
   416  		}
   417  	}
   418  
   419  	return result, nil
   420  }
   421  
   422  func (t TypeScriptify) backup(fileName string) error {
   423  	fileIn, err := os.Open(fileName)
   424  	if err != nil {
   425  		if !os.IsNotExist(err) {
   426  			return err
   427  		}
   428  		// No neet to backup, just return:
   429  		return nil
   430  	}
   431  	defer fileIn.Close()
   432  
   433  	bytes, err := ioutil.ReadAll(fileIn)
   434  	if err != nil {
   435  		return err
   436  	}
   437  
   438  	_, backupFn := path.Split(fmt.Sprintf("%s-%s.backup", fileName, time.Now().Format("2006-01-02T15_04_05.99")))
   439  	if t.BackupDir != "" {
   440  		backupFn = path.Join(t.BackupDir, backupFn)
   441  	}
   442  
   443  	return ioutil.WriteFile(backupFn, bytes, os.FileMode(0o700))
   444  }
   445  
   446  func (t TypeScriptify) ConvertToFile(fileName string, packageName string) error {
   447  	if len(t.BackupDir) > 0 {
   448  		err := t.backup(fileName)
   449  		if err != nil {
   450  			return err
   451  		}
   452  	}
   453  
   454  	customCode, err := loadCustomCode(fileName)
   455  	if err != nil {
   456  		return err
   457  	}
   458  
   459  	f, err := os.Create(fileName)
   460  	if err != nil {
   461  		return err
   462  	}
   463  	defer f.Close()
   464  
   465  	converted, err := t.Convert(customCode)
   466  	if err != nil {
   467  		return err
   468  	}
   469  
   470  	var lines []string
   471  	sc := bufio.NewScanner(strings.NewReader(converted))
   472  	for sc.Scan() {
   473  		lines = append(lines, "\t"+sc.Text())
   474  	}
   475  
   476  	converted = "export namespace " + packageName + " {\n"
   477  	converted += strings.Join(lines, "\n")
   478  	converted += "\n}\n"
   479  
   480  	if _, err := f.WriteString("/* Do not change, this code is generated from Golang structs */\n\n"); err != nil {
   481  		return err
   482  	}
   483  	if _, err := f.WriteString(converted); err != nil {
   484  		return err
   485  	}
   486  	if err != nil {
   487  		return err
   488  	}
   489  
   490  	return nil
   491  }
   492  
   493  type TSNamer interface {
   494  	TSName() string
   495  }
   496  
   497  func (t *TypeScriptify) convertEnum(depth int, typeOf reflect.Type, elements []enumElement) (string, error) {
   498  	t.logf(depth, "Converting enum %s", typeOf.String())
   499  	if _, found := t.alreadyConverted[typeOf.String()]; found { // Already converted
   500  		return "", nil
   501  	}
   502  	t.alreadyConverted[typeOf.String()] = true
   503  
   504  	entityName := t.Prefix + typeOf.Name() + t.Suffix
   505  	result := "enum " + entityName + " {\n"
   506  
   507  	for _, val := range elements {
   508  		result += fmt.Sprintf("%s%s = %#v,\n", t.Indent, val.name, val.value)
   509  	}
   510  
   511  	result += "}"
   512  
   513  	if !t.DontExport {
   514  		result = "export " + result
   515  	}
   516  
   517  	return result, nil
   518  }
   519  
   520  func (t *TypeScriptify) getFieldOptions(structType reflect.Type, field reflect.StructField) TypeOptions {
   521  	// By default use options defined by tags:
   522  	opts := TypeOptions{TSTransform: field.Tag.Get(tsTransformTag), TSType: field.Tag.Get(tsType)}
   523  
   524  	overrides := []TypeOptions{}
   525  
   526  	// But there is maybe an struct-specific override:
   527  	for _, strct := range t.structTypes {
   528  		if strct.FieldOptions == nil {
   529  			continue
   530  		}
   531  		if strct.Type == structType {
   532  			if fldOpts, found := strct.FieldOptions[field.Type]; found {
   533  				overrides = append(overrides, fldOpts)
   534  			}
   535  		}
   536  	}
   537  
   538  	if fldOpts, found := t.fieldTypeOptions[field.Type]; found {
   539  		overrides = append(overrides, fldOpts)
   540  	}
   541  
   542  	for _, o := range overrides {
   543  		if o.TSTransform != "" {
   544  			opts.TSTransform = o.TSTransform
   545  		}
   546  		if o.TSType != "" {
   547  			opts.TSType = o.TSType
   548  		}
   549  	}
   550  
   551  	return opts
   552  }
   553  
   554  func (t *TypeScriptify) getJSONFieldName(field reflect.StructField, isPtr bool) string {
   555  	jsonFieldName := ""
   556  	jsonTag := field.Tag.Get("json")
   557  	if len(jsonTag) > 0 {
   558  		jsonTagParts := strings.Split(jsonTag, ",")
   559  		if len(jsonTagParts) > 0 {
   560  			jsonFieldName = strings.Trim(jsonTagParts[0], t.Indent)
   561  		}
   562  		hasOmitEmpty := false
   563  		ignored := false
   564  		for _, t := range jsonTagParts {
   565  			if t == "" {
   566  				break
   567  			}
   568  			if t == "omitempty" {
   569  				hasOmitEmpty = true
   570  				break
   571  			}
   572  			if t == "-" {
   573  				ignored = true
   574  				break
   575  			}
   576  		}
   577  		if !ignored && isPtr || hasOmitEmpty {
   578  			jsonFieldName = fmt.Sprintf("%s?", jsonFieldName)
   579  		}
   580  	}
   581  	return jsonFieldName
   582  }
   583  
   584  func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode map[string]string) (string, error) {
   585  	if _, found := t.alreadyConverted[typeOf.String()]; found { // Already converted
   586  		return "", nil
   587  	}
   588  	fields := t.deepFields(typeOf)
   589  	if len(fields) == 0 {
   590  		return "", nil
   591  	}
   592  	t.logf(depth, "Converting type %s", typeOf.String())
   593  	if differentNamespaces(t.Namespace, typeOf) {
   594  		return "", nil
   595  	}
   596  
   597  	t.alreadyConverted[typeOf.String()] = true
   598  
   599  	entityName := t.Prefix + typeOf.Name() + t.Suffix
   600  
   601  	if typeClashWithReservedKeyword(entityName) {
   602  		warnAboutTypesClash(entityName)
   603  	}
   604  
   605  	result := ""
   606  	if t.CreateInterface {
   607  		result += fmt.Sprintf("interface %s {\n", entityName)
   608  	} else {
   609  		result += fmt.Sprintf("class %s {\n", entityName)
   610  	}
   611  	if !t.DontExport {
   612  		result = "export " + result
   613  	}
   614  	builder := typeScriptClassBuilder{
   615  		types:     t.kinds,
   616  		indent:    t.Indent,
   617  		prefix:    t.Prefix,
   618  		suffix:    t.Suffix,
   619  		namespace: t.Namespace,
   620  	}
   621  
   622  	for _, field := range fields {
   623  		isPtr := field.Type.Kind() == reflect.Ptr
   624  		if isPtr {
   625  			field.Type = field.Type.Elem()
   626  		}
   627  		jsonFieldName := t.getJSONFieldName(field, isPtr)
   628  		if len(jsonFieldName) == 0 || jsonFieldName == "-" {
   629  			continue
   630  		}
   631  
   632  		var err error
   633  		fldOpts := t.getFieldOptions(typeOf, field)
   634  		if fldOpts.TSTransform != "" {
   635  			t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
   636  			err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
   637  		} else if _, isEnum := t.enums[field.Type]; isEnum {
   638  			t.logf(depth, "- enum field %s.%s", typeOf.Name(), field.Name)
   639  			builder.AddEnumField(jsonFieldName, field)
   640  		} else if fldOpts.TSType != "" { // Struct:
   641  			t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
   642  			err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
   643  		} else if field.Type.Kind() == reflect.Struct { // Struct:
   644  			t.logf(depth, "- struct %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String())
   645  
   646  			// Anonymous structures is ignored
   647  			// It is possible to generate them but hard to generate correct name
   648  			if field.Type.Name() != "" {
   649  				typeScriptChunk, err := t.convertType(depth+1, field.Type, customCode)
   650  				if err != nil {
   651  					return "", err
   652  				}
   653  				if typeScriptChunk != "" {
   654  					result = typeScriptChunk + "\n" + result
   655  				}
   656  			}
   657  
   658  			isKnownType := t.KnownStructs.Contains(getStructFQN(field.Type.String()))
   659  			println("KnownStructs:", t.KnownStructs.Join("\t"))
   660  			println(getStructFQN(field.Type.String()))
   661  			builder.AddStructField(jsonFieldName, field, !isKnownType)
   662  		} else if field.Type.Kind() == reflect.Map {
   663  			t.logf(depth, "- map field %s.%s", typeOf.Name(), field.Name)
   664  			// Also convert map key types if needed
   665  			var keyTypeToConvert reflect.Type
   666  			switch field.Type.Key().Kind() {
   667  			case reflect.Struct:
   668  				keyTypeToConvert = field.Type.Key()
   669  			case reflect.Ptr:
   670  				keyTypeToConvert = field.Type.Key().Elem()
   671  			}
   672  			if keyTypeToConvert != nil {
   673  				typeScriptChunk, err := t.convertType(depth+1, keyTypeToConvert, customCode)
   674  				if err != nil {
   675  					return "", err
   676  				}
   677  				if typeScriptChunk != "" {
   678  					result = typeScriptChunk + "\n" + result
   679  				}
   680  			}
   681  			// Also convert map value types if needed
   682  			var valueTypeToConvert reflect.Type
   683  			switch field.Type.Elem().Kind() {
   684  			case reflect.Struct:
   685  				valueTypeToConvert = field.Type.Elem()
   686  			case reflect.Ptr:
   687  				valueTypeToConvert = field.Type.Elem().Elem()
   688  			}
   689  			if valueTypeToConvert != nil {
   690  				typeScriptChunk, err := t.convertType(depth+1, valueTypeToConvert, customCode)
   691  				if err != nil {
   692  					return "", err
   693  				}
   694  				if typeScriptChunk != "" {
   695  					result = typeScriptChunk + "\n" + result
   696  				}
   697  			}
   698  
   699  			builder.AddMapField(jsonFieldName, field)
   700  		} else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice:
   701  			if field.Type.Elem().Kind() == reflect.Ptr { // extract ptr type
   702  				field.Type = field.Type.Elem()
   703  			}
   704  
   705  			arrayDepth := 1
   706  			for field.Type.Elem().Kind() == reflect.Slice { // Slice of slices:
   707  				field.Type = field.Type.Elem()
   708  				arrayDepth++
   709  			}
   710  
   711  			if field.Type.Elem().Kind() == reflect.Struct { // Slice of structs:
   712  				t.logf(depth, "- struct slice %s.%s (%s)", typeOf.Name(), field.Name, field.Type.String())
   713  				typeScriptChunk, err := t.convertType(depth+1, field.Type.Elem(), customCode)
   714  				if err != nil {
   715  					return "", err
   716  				}
   717  				if typeScriptChunk != "" {
   718  					result = typeScriptChunk + "\n" + result
   719  				}
   720  				builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth)
   721  			} else { // Slice of simple fields:
   722  				t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name)
   723  				err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts)
   724  			}
   725  		} else { // Simple field:
   726  			t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
   727  			// check if type is in known enum. If so, then replace TStype with enum name to avoid missing types
   728  			isKnownEnum := t.KnownEnums.Contains(getStructFQN(field.Type.String()))
   729  			if isKnownEnum {
   730  				err = builder.AddSimpleField(jsonFieldName, field, TypeOptions{
   731  					TSType:      getStructFQN(field.Type.String()),
   732  					TSTransform: fldOpts.TSTransform,
   733  				})
   734  			} else {
   735  				err = builder.AddSimpleField(jsonFieldName, field, fldOpts)
   736  			}
   737  		}
   738  		if err != nil {
   739  			return "", err
   740  		}
   741  	}
   742  
   743  	if t.CreateFromMethod {
   744  		t.CreateConstructor = true
   745  	}
   746  
   747  	result += strings.Join(builder.fields, "\n") + "\n"
   748  	if !t.CreateInterface {
   749  		constructorBody := strings.Join(builder.constructorBody, "\n")
   750  		needsConvertValue := strings.Contains(constructorBody, "this.convertValues")
   751  		if t.CreateFromMethod {
   752  			result += fmt.Sprintf("\n%sstatic createFrom(source: any = {}) {\n", t.Indent)
   753  			result += fmt.Sprintf("%s%sreturn new %s(source);\n", t.Indent, t.Indent, entityName)
   754  			result += fmt.Sprintf("%s}\n", t.Indent)
   755  		}
   756  		if t.CreateConstructor {
   757  			result += fmt.Sprintf("\n%sconstructor(source: any = {}) {\n", t.Indent)
   758  			result += t.Indent + t.Indent + "if ('string' === typeof source) source = JSON.parse(source);\n"
   759  			result += constructorBody + "\n"
   760  			result += fmt.Sprintf("%s}\n", t.Indent)
   761  		}
   762  		if needsConvertValue && (t.CreateConstructor || t.CreateFromMethod) {
   763  			result += "\n" + indentLines(strings.ReplaceAll(tsConvertValuesFunc, "\t", t.Indent), 1) + "\n"
   764  		}
   765  	}
   766  
   767  	if customCode != nil {
   768  		code := customCode[entityName]
   769  		if len(code) != 0 {
   770  			result += t.Indent + "//[" + entityName + ":]\n" + code + "\n\n" + t.Indent + "//[end]\n"
   771  		}
   772  	}
   773  
   774  	result += "}"
   775  
   776  	return result, nil
   777  }
   778  
   779  func (t *TypeScriptify) AddImport(i string) {
   780  	for _, cimport := range t.customImports {
   781  		if cimport == i {
   782  			return
   783  		}
   784  	}
   785  
   786  	t.customImports = append(t.customImports, i)
   787  }
   788  
   789  type typeScriptClassBuilder struct {
   790  	types                map[reflect.Kind]string
   791  	indent               string
   792  	fields               []string
   793  	createFromMethodBody []string
   794  	constructorBody      []string
   795  	prefix, suffix       string
   796  	namespace            string
   797  }
   798  
   799  func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error {
   800  	fieldType, kind := field.Type.Elem().Name(), field.Type.Elem().Kind()
   801  	typeScriptType := t.types[kind]
   802  
   803  	if len(fieldName) > 0 {
   804  		strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
   805  		if len(opts.TSType) > 0 {
   806  			t.addField(fieldName, opts.TSType, false)
   807  			t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
   808  			return nil
   809  		} else if len(typeScriptType) > 0 {
   810  			t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth)), false)
   811  			t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
   812  			return nil
   813  		}
   814  	}
   815  
   816  	return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType)
   817  }
   818  
   819  func (t *typeScriptClassBuilder) AddSimpleField(fieldName string, field reflect.StructField, opts TypeOptions) error {
   820  	fieldType, kind := field.Type.Name(), field.Type.Kind()
   821  
   822  	typeScriptType := t.types[kind]
   823  	if len(opts.TSType) > 0 {
   824  		typeScriptType = opts.TSType
   825  	}
   826  
   827  	if len(typeScriptType) > 0 && len(fieldName) > 0 {
   828  		strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
   829  		t.addField(fieldName, typeScriptType, false)
   830  		if opts.TSTransform == "" {
   831  			t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
   832  		} else {
   833  			val := fmt.Sprintf(`source["%s"]`, strippedFieldName)
   834  			expression := strings.Replace(opts.TSTransform, "__VALUE__", val, -1)
   835  			t.addInitializerFieldLine(strippedFieldName, expression)
   836  		}
   837  		return nil
   838  	}
   839  
   840  	return fmt.Errorf("cannot find type for %s (%s/%s)", kind.String(), fieldName, fieldType)
   841  }
   842  
   843  func (t *typeScriptClassBuilder) AddEnumField(fieldName string, field reflect.StructField) {
   844  	fieldType := field.Type.Name()
   845  	t.addField(fieldName, t.prefix+fieldType+t.suffix, false)
   846  	strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
   847  	t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
   848  }
   849  
   850  func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.StructField, isAnyType bool) {
   851  	strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
   852  	classname := "null"
   853  	namespace := strings.Split(field.Type.String(), ".")[0]
   854  	fqname := t.prefix + field.Type.Name() + t.suffix
   855  	if namespace != t.namespace {
   856  		fqname = namespace + "." + fqname
   857  	}
   858  
   859  	if !isAnyType {
   860  		classname = fqname
   861  	}
   862  
   863  	// Anonymous struct
   864  	if field.Type.Name() == "" {
   865  		classname = "Object"
   866  	}
   867  
   868  	t.addField(fieldName, fqname, isAnyType)
   869  	t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, classname))
   870  }
   871  
   872  func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) {
   873  	fieldType := field.Type.Elem().Name()
   874  	if differentNamespaces(t.namespace, field.Type.Elem()) {
   875  		fieldType = field.Type.Elem().String()
   876  	}
   877  	strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
   878  	t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth)), false)
   879  	t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix))
   880  }
   881  
   882  func (t *typeScriptClassBuilder) addInitializerFieldLine(fld, initializer string) {
   883  	var dotField string
   884  	if regexp.MustCompile(jsVariableNameRegex).Match([]byte(fld)) {
   885  		dotField = fmt.Sprintf(".%s", fld)
   886  	} else {
   887  		dotField = fmt.Sprintf(`["%s"]`, fld)
   888  	}
   889  	t.createFromMethodBody = append(t.createFromMethodBody, fmt.Sprint(t.indent, t.indent, "result", dotField, " = ", initializer, ";"))
   890  	t.constructorBody = append(t.constructorBody, fmt.Sprint(t.indent, t.indent, "this", dotField, " = ", initializer, ";"))
   891  }
   892  
   893  func (t *typeScriptClassBuilder) addField(fld, fldType string, isAnyType bool) {
   894  	isOptional := strings.HasSuffix(fld, "?")
   895  	strippedFieldName := strings.ReplaceAll(fld, "?", "")
   896  	if !regexp.MustCompile(jsVariableNameRegex).Match([]byte(strippedFieldName)) {
   897  		fld = fmt.Sprintf(`"%s"`, fld)
   898  		if isOptional {
   899  			fld += "?"
   900  		}
   901  	}
   902  	if isAnyType {
   903  		fldType = strings.Split(fldType, ".")[0]
   904  		t.fields = append(t.fields, fmt.Sprint(t.indent, "// Go type: ", fldType, "\n", t.indent, fld, ": any;"))
   905  	} else {
   906  		t.fields = append(t.fields, fmt.Sprint(t.indent, fld, ": ", fldType, ";"))
   907  	}
   908  }
   909  
   910  func indentLines(str string, i int) string {
   911  	lines := strings.Split(str, "\n")
   912  	for n := range lines {
   913  		lines[n] = strings.Repeat("\t", i) + lines[n]
   914  	}
   915  	return strings.Join(lines, "\n")
   916  }
   917  
   918  func getStructFQN(in string) string {
   919  	result := strings.ReplaceAll(in, "[]", "")
   920  	result = strings.ReplaceAll(result, "*", "")
   921  	return result
   922  }
   923  
   924  func differentNamespaces(namespace string, typeOf reflect.Type) bool {
   925  	if strings.ContainsRune(typeOf.String(), '.') {
   926  		typeNamespace := strings.Split(typeOf.String(), ".")[0]
   927  		if namespace != typeNamespace {
   928  			return true
   929  		}
   930  	}
   931  	return false
   932  }
   933  
   934  func typeClashWithReservedKeyword(input string) bool {
   935  	in := strings.ToLower(strings.TrimSpace(input))
   936  	for _, v := range jsReservedKeywords {
   937  		if in == v {
   938  			return true
   939  		}
   940  	}
   941  
   942  	return false
   943  }
   944  
   945  func warnAboutTypesClash(entity string) {
   946  	// TODO: Refactor logging
   947  	l := log.New(os.Stderr, "", 0)
   948  	l.Printf("Usage of reserved keyword found and not supported: %s", entity)
   949  	log.Println("Please rename returned type or consider adding bindings config to your wails.json")
   950  }