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