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

     1  package binding
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"reflect"
    10  	"runtime"
    11  	"sort"
    12  	"strings"
    13  
    14  	"github.com/AlpineAIO/wails/v2/internal/typescriptify"
    15  
    16  	"github.com/AlpineAIO/wails/v2/internal/logger"
    17  	"github.com/leaanthony/slicer"
    18  )
    19  
    20  type Bindings struct {
    21  	db         *DB
    22  	logger     logger.CustomLogger
    23  	exemptions slicer.StringSlicer
    24  
    25  	structsToGenerateTS map[string]map[string]interface{}
    26  	enumsToGenerateTS   map[string]map[string]interface{}
    27  	tsPrefix            string
    28  	tsSuffix            string
    29  	tsInterface         bool
    30  	obfuscate           bool
    31  }
    32  
    33  // NewBindings returns a new Bindings object
    34  func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings {
    35  	result := &Bindings{
    36  		db:                  newDB(),
    37  		logger:              logger.CustomLogger("Bindings"),
    38  		structsToGenerateTS: make(map[string]map[string]interface{}),
    39  		enumsToGenerateTS:   make(map[string]map[string]interface{}),
    40  		obfuscate:           obfuscate,
    41  	}
    42  
    43  	for _, exemption := range exemptions {
    44  		if exemption == nil {
    45  			continue
    46  		}
    47  		name := runtime.FuncForPC(reflect.ValueOf(exemption).Pointer()).Name()
    48  		// Yuk yuk yuk! Is there a better way?
    49  		name = strings.TrimSuffix(name, "-fm")
    50  		result.exemptions.Add(name)
    51  	}
    52  
    53  	for _, enum := range enumsToBind {
    54  		result.AddEnumToGenerateTS(enum)
    55  	}
    56  
    57  	// Add the structs to bind
    58  	for _, ptr := range structPointersToBind {
    59  		err := result.Add(ptr)
    60  		if err != nil {
    61  			logger.Fatal("Error during binding: " + err.Error())
    62  		}
    63  	}
    64  
    65  	return result
    66  }
    67  
    68  // Add the given struct methods to the Bindings
    69  func (b *Bindings) Add(structPtr interface{}) error {
    70  	methods, err := b.getMethods(structPtr)
    71  	if err != nil {
    72  		return fmt.Errorf("cannot bind value to app: %s", err.Error())
    73  	}
    74  
    75  	for _, method := range methods {
    76  		splitName := strings.Split(method.Name, ".")
    77  		packageName := splitName[0]
    78  		structName := splitName[1]
    79  		methodName := splitName[2]
    80  
    81  		// Add it as a regular method
    82  		b.db.AddMethod(packageName, structName, methodName, method)
    83  	}
    84  	return nil
    85  }
    86  
    87  func (b *Bindings) DB() *DB {
    88  	return b.db
    89  }
    90  
    91  func (b *Bindings) ToJSON() (string, error) {
    92  	return b.db.ToJSON()
    93  }
    94  
    95  func (b *Bindings) GenerateModels() ([]byte, error) {
    96  	models := map[string]string{}
    97  	var seen slicer.StringSlicer
    98  	var seenEnumsPackages slicer.StringSlicer
    99  	allStructNames := b.getAllStructNames()
   100  	allStructNames.Sort()
   101  	allEnumNames := b.getAllEnumNames()
   102  	allEnumNames.Sort()
   103  	for packageName, structsToGenerate := range b.structsToGenerateTS {
   104  		thisPackageCode := ""
   105  		w := typescriptify.New()
   106  		w.WithPrefix(b.tsPrefix)
   107  		w.WithSuffix(b.tsSuffix)
   108  		w.WithInterface(b.tsInterface)
   109  		w.Namespace = packageName
   110  		w.WithBackupDir("")
   111  		w.KnownStructs = allStructNames
   112  		w.KnownEnums = allEnumNames
   113  		// sort the structs
   114  		var structNames []string
   115  		for structName := range structsToGenerate {
   116  			structNames = append(structNames, structName)
   117  		}
   118  		sort.Strings(structNames)
   119  		for _, structName := range structNames {
   120  			fqstructname := packageName + "." + structName
   121  			if seen.Contains(fqstructname) {
   122  				continue
   123  			}
   124  			structInterface := structsToGenerate[structName]
   125  			w.Add(structInterface)
   126  		}
   127  
   128  		// if we have enums for this package, add them as well
   129  		var enums, enumsExist = b.enumsToGenerateTS[packageName]
   130  		if enumsExist {
   131  			for enumName, enum := range enums {
   132  				fqemumname := packageName + "." + enumName
   133  				if seen.Contains(fqemumname) {
   134  					continue
   135  				}
   136  				w.AddEnum(enum)
   137  			}
   138  			seenEnumsPackages.Add(packageName)
   139  		}
   140  
   141  		str, err := w.Convert(nil)
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  		thisPackageCode += str
   146  		seen.AddSlice(w.GetGeneratedStructs())
   147  		models[packageName] = thisPackageCode
   148  	}
   149  
   150  	// Add outstanding enums to the models that were not in packages with structs
   151  	for packageName, enumsToGenerate := range b.enumsToGenerateTS {
   152  		if seenEnumsPackages.Contains(packageName) {
   153  			continue
   154  		}
   155  
   156  		thisPackageCode := ""
   157  		w := typescriptify.New()
   158  		w.WithPrefix(b.tsPrefix)
   159  		w.WithSuffix(b.tsSuffix)
   160  		w.WithInterface(b.tsInterface)
   161  		w.Namespace = packageName
   162  		w.WithBackupDir("")
   163  
   164  		for enumName, enum := range enumsToGenerate {
   165  			fqemumname := packageName + "." + enumName
   166  			if seen.Contains(fqemumname) {
   167  				continue
   168  			}
   169  			w.AddEnum(enum)
   170  		}
   171  		str, err := w.Convert(nil)
   172  		if err != nil {
   173  			return nil, err
   174  		}
   175  		thisPackageCode += str
   176  		models[packageName] = thisPackageCode
   177  	}
   178  
   179  	// Sort the package names first to make the output deterministic
   180  	sortedPackageNames := make([]string, 0)
   181  	for packageName := range models {
   182  		sortedPackageNames = append(sortedPackageNames, packageName)
   183  	}
   184  	sort.Strings(sortedPackageNames)
   185  
   186  	var modelsData bytes.Buffer
   187  	for _, packageName := range sortedPackageNames {
   188  		modelData := models[packageName]
   189  		if strings.TrimSpace(modelData) == "" {
   190  			continue
   191  		}
   192  		modelsData.WriteString("export namespace " + packageName + " {\n")
   193  		sc := bufio.NewScanner(strings.NewReader(modelData))
   194  		for sc.Scan() {
   195  			modelsData.WriteString("\t" + sc.Text() + "\n")
   196  		}
   197  		modelsData.WriteString("\n}\n\n")
   198  	}
   199  	return modelsData.Bytes(), nil
   200  }
   201  
   202  func (b *Bindings) WriteModels(modelsDir string) error {
   203  	modelsData, err := b.GenerateModels()
   204  	if err != nil {
   205  		return err
   206  	}
   207  	// Don't write if we don't have anything
   208  	if len(modelsData) == 0 {
   209  		return nil
   210  	}
   211  
   212  	filename := filepath.Join(modelsDir, "models.ts")
   213  	err = os.WriteFile(filename, modelsData, 0o755)
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  func (b *Bindings) AddEnumToGenerateTS(e interface{}) {
   222  	enumType := reflect.TypeOf(e)
   223  
   224  	var packageName string
   225  	var enumName string
   226  	// enums should be represented as array of all possible values
   227  	if hasElements(enumType) {
   228  		enum := enumType.Elem()
   229  		// simple enum represented by struct with Value/TSName fields
   230  		if enum.Kind() == reflect.Struct {
   231  			_, tsNamePresented := enum.FieldByName("TSName")
   232  			enumT, valuePresented := enum.FieldByName("Value")
   233  			if tsNamePresented && valuePresented {
   234  				packageName = getPackageName(enumT.Type.String())
   235  				enumName = enumT.Type.Name()
   236  			} else {
   237  				return
   238  			}
   239  			// otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname
   240  		} else {
   241  			packageName = getPackageName(enumType.Elem().String())
   242  			enumName = enumType.Elem().Name()
   243  		}
   244  		if b.enumsToGenerateTS[packageName] == nil {
   245  			b.enumsToGenerateTS[packageName] = make(map[string]interface{})
   246  		}
   247  		if b.enumsToGenerateTS[packageName][enumName] != nil {
   248  			return
   249  		}
   250  		b.enumsToGenerateTS[packageName][enumName] = e
   251  	}
   252  }
   253  
   254  func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) {
   255  	if b.structsToGenerateTS[packageName] == nil {
   256  		b.structsToGenerateTS[packageName] = make(map[string]interface{})
   257  	}
   258  	if b.structsToGenerateTS[packageName][structName] != nil {
   259  		return
   260  	}
   261  	b.structsToGenerateTS[packageName][structName] = s
   262  
   263  	// Iterate this struct and add any struct field references
   264  	structType := reflect.TypeOf(s)
   265  	if hasElements(structType) {
   266  		structType = structType.Elem()
   267  	}
   268  
   269  	for i := 0; i < structType.NumField(); i++ {
   270  		field := structType.Field(i)
   271  		if field.Anonymous {
   272  			continue
   273  		}
   274  		kind := field.Type.Kind()
   275  		if kind == reflect.Struct {
   276  			if !field.IsExported() {
   277  				continue
   278  			}
   279  			fqname := field.Type.String()
   280  			sNameSplit := strings.Split(fqname, ".")
   281  			if len(sNameSplit) < 2 {
   282  				continue
   283  			}
   284  			sName := sNameSplit[1]
   285  			pName := getPackageName(fqname)
   286  			a := reflect.New(field.Type)
   287  			if b.hasExportedJSONFields(field.Type) {
   288  				s := reflect.Indirect(a).Interface()
   289  				b.AddStructToGenerateTS(pName, sName, s)
   290  			}
   291  		} else if hasElements(field.Type) && field.Type.Elem().Kind() == reflect.Struct {
   292  			if !field.IsExported() {
   293  				continue
   294  			}
   295  			fqname := field.Type.Elem().String()
   296  			sNameSplit := strings.Split(fqname, ".")
   297  			if len(sNameSplit) < 2 {
   298  				continue
   299  			}
   300  			sName := sNameSplit[1]
   301  			pName := getPackageName(fqname)
   302  			typ := field.Type.Elem()
   303  			a := reflect.New(typ)
   304  			if b.hasExportedJSONFields(typ) {
   305  				s := reflect.Indirect(a).Interface()
   306  				b.AddStructToGenerateTS(pName, sName, s)
   307  			}
   308  		}
   309  	}
   310  }
   311  
   312  func (b *Bindings) SetTsPrefix(prefix string) *Bindings {
   313  	b.tsPrefix = prefix
   314  	return b
   315  }
   316  
   317  func (b *Bindings) SetTsSuffix(postfix string) *Bindings {
   318  	b.tsSuffix = postfix
   319  	return b
   320  }
   321  
   322  func (b *Bindings) SetOutputType(outputType string) *Bindings {
   323  	if outputType == "interfaces" {
   324  		b.tsInterface = true
   325  	}
   326  	return b
   327  }
   328  
   329  func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
   330  	var result slicer.StringSlicer
   331  	for packageName, structsToGenerate := range b.structsToGenerateTS {
   332  		for structName := range structsToGenerate {
   333  			result.Add(packageName + "." + structName)
   334  		}
   335  	}
   336  	return &result
   337  }
   338  
   339  func (b *Bindings) getAllEnumNames() *slicer.StringSlicer {
   340  	var result slicer.StringSlicer
   341  	for packageName, enumsToGenerate := range b.enumsToGenerateTS {
   342  		for enumName := range enumsToGenerate {
   343  			result.Add(packageName + "." + enumName)
   344  		}
   345  	}
   346  	return &result
   347  }
   348  
   349  func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool {
   350  	for i := 0; i < typeOf.NumField(); i++ {
   351  		jsonFieldName := ""
   352  		f := typeOf.Field(i)
   353  		jsonTag := f.Tag.Get("json")
   354  		if len(jsonTag) == 0 {
   355  			continue
   356  		}
   357  		jsonTagParts := strings.Split(jsonTag, ",")
   358  		if len(jsonTagParts) > 0 {
   359  			jsonFieldName = jsonTagParts[0]
   360  		}
   361  		for _, t := range jsonTagParts {
   362  			if t == "-" {
   363  				continue
   364  			}
   365  		}
   366  		if jsonFieldName != "" {
   367  			return true
   368  		}
   369  	}
   370  	return false
   371  }