Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for interface generation and enums #3047

Merged
merged 14 commits into from
Nov 25, 2023
Merged
11 changes: 8 additions & 3 deletions v2/cmd/wails/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,15 @@ func generateModule(f *flags.GenerateModule) error {
return err
}

if projectConfig.Bindings.TsGeneration.OutputType == "" {
projectConfig.Bindings.TsGeneration.OutputType = "classes"
}

_, err = bindings.GenerateBindings(bindings.Options{
Tags: buildTags,
TsPrefix: projectConfig.Bindings.TsGeneration.Prefix,
TsSuffix: projectConfig.Bindings.TsGeneration.Suffix,
Tags: buildTags,
TsPrefix: projectConfig.Bindings.TsGeneration.Prefix,
TsSuffix: projectConfig.Bindings.TsGeneration.Suffix,
TsOutputType: projectConfig.Bindings.TsGeneration.OutputType,
})
if err != nil {
return err
Expand Down
12 changes: 11 additions & 1 deletion v2/internal/app/app_bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func (a *App) Run() error {

var tsPrefixFlag *string
var tsPostfixFlag *string
var tsOutputTypeFlag *string

tsPrefix := os.Getenv("tsprefix")
if tsPrefix == "" {
Expand All @@ -42,18 +43,27 @@ func (a *App) Run() error {
tsPostfixFlag = bindingFlags.String("tssuffix", "", "Suffix for generated typescript entities")
}

tsOutputType := os.Getenv("tsoutputtype")
if tsOutputType == "" {
tsOutputTypeFlag = bindingFlags.String("tsoutputtype", "", "Output type for generated typescript entities (classes|interfaces)")
}

_ = bindingFlags.Parse(os.Args[1:])
if tsPrefixFlag != nil {
tsPrefix = *tsPrefixFlag
}
if tsPostfixFlag != nil {
tsSuffix = *tsPostfixFlag
}
if tsOutputTypeFlag != nil {
tsOutputType = *tsOutputTypeFlag
}

appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated())
appBindings := binding.NewBindings(a.logger, a.options.Bind, bindingExemptions, IsObfuscated(), a.options.EnumBind)

appBindings.SetTsPrefix(tsPrefix)
appBindings.SetTsSuffix(tsSuffix)
appBindings.SetOutputType(tsOutputType)

err := generateBindings(appBindings)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion v2/internal/app/app_dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
appoptions.OnDomReady,
appoptions.OnBeforeClose,
}
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false)
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, false, appoptions.EnumBind)

eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler)
Expand Down
2 changes: 1 addition & 1 deletion v2/internal/app/app_production.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func CreateApp(appoptions *options.App) (*App, error) {
appoptions.OnDomReady,
appoptions.OnBeforeClose,
}
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated())
appBindings := binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions, IsObfuscated(), appoptions.EnumBind)
eventHandler := runtime.NewEvents(myLogger)
ctx = context.WithValue(ctx, "events", eventHandler)
// Attach logger to context
Expand Down
107 changes: 106 additions & 1 deletion v2/internal/binding/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,20 @@ type Bindings struct {
exemptions slicer.StringSlicer

structsToGenerateTS map[string]map[string]interface{}
enumsToGenerateTS map[string]map[string]interface{}
tsPrefix string
tsSuffix string
tsInterface bool
obfuscate bool
}

// NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool) *Bindings {
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}, obfuscate bool, enumsToBind []interface{}) *Bindings {
result := &Bindings{
db: newDB(),
logger: logger.CustomLogger("Bindings"),
structsToGenerateTS: make(map[string]map[string]interface{}),
enumsToGenerateTS: make(map[string]map[string]interface{}),
obfuscate: obfuscate,
}

Expand All @@ -47,6 +50,10 @@ func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exem
result.exemptions.Add(name)
}

for _, enum := range enumsToBind {
result.AddEnumToGenerateTS(enum)
}

// Add the structs to bind
for _, ptr := range structPointersToBind {
err := result.Add(ptr)
Expand Down Expand Up @@ -88,16 +95,21 @@ func (b *Bindings) ToJSON() (string, error) {
func (b *Bindings) GenerateModels() ([]byte, error) {
models := map[string]string{}
var seen slicer.StringSlicer
var seenEnumsPackages slicer.StringSlicer
allStructNames := b.getAllStructNames()
allStructNames.Sort()
allEnumNames := b.getAllEnumNames()
allEnumNames.Sort()
for packageName, structsToGenerate := range b.structsToGenerateTS {
thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")
w.KnownStructs = allStructNames
w.KnownEnums = allEnumNames
// sort the structs
var structNames []string
for structName := range structsToGenerate {
Expand All @@ -112,6 +124,20 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
structInterface := structsToGenerate[structName]
w.Add(structInterface)
}

// if we have enums for this package, add them as well
var enums, enumsExist = b.enumsToGenerateTS[packageName]
if enumsExist {
for enumName, enum := range enums {
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
seenEnumsPackages.Add(packageName)
}

str, err := w.Convert(nil)
if err != nil {
return nil, err
Expand All @@ -121,6 +147,35 @@ func (b *Bindings) GenerateModels() ([]byte, error) {
models[packageName] = thisPackageCode
}

// Add outstanding enums to the models that were not in packages with structs
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
if seenEnumsPackages.Contains(packageName) {
continue
}

thisPackageCode := ""
w := typescriptify.New()
w.WithPrefix(b.tsPrefix)
w.WithSuffix(b.tsSuffix)
w.WithInterface(b.tsInterface)
w.Namespace = packageName
w.WithBackupDir("")

for enumName, enum := range enumsToGenerate {
fqemumname := packageName + "." + enumName
if seen.Contains(fqemumname) {
continue
}
w.AddEnum(enum)
}
str, err := w.Convert(nil)
if err != nil {
return nil, err
}
thisPackageCode += str
models[packageName] = thisPackageCode
}

// Sort the package names first to make the output deterministic
sortedPackageNames := make([]string, 0)
for packageName := range models {
Expand Down Expand Up @@ -163,6 +218,39 @@ func (b *Bindings) WriteModels(modelsDir string) error {
return nil
}

func (b *Bindings) AddEnumToGenerateTS(e interface{}) {
enumType := reflect.TypeOf(e)

var packageName string
var enumName string
// enums should be represented as array of all possible values
if hasElements(enumType) {
enum := enumType.Elem()
// simple enum represented by struct with Value/TSName fields
if enum.Kind() == reflect.Struct {
_, tsNamePresented := enum.FieldByName("TSName")
enumT, valuePresented := enum.FieldByName("Value")
if tsNamePresented && valuePresented {
packageName = getPackageName(enumT.Type.String())
enumName = enumT.Type.Name()
} else {
return
}
// otherwise expecting implementation with TSName() https://github.com/tkrajina/typescriptify-golang-structs#enums-with-tsname
} else {
packageName = getPackageName(enumType.Elem().String())
enumName = enumType.Elem().Name()
}
if b.enumsToGenerateTS[packageName] == nil {
b.enumsToGenerateTS[packageName] = make(map[string]interface{})
}
if b.enumsToGenerateTS[packageName][enumName] != nil {
return
}
b.enumsToGenerateTS[packageName][enumName] = e
}
}

func (b *Bindings) AddStructToGenerateTS(packageName string, structName string, s interface{}) {
if b.structsToGenerateTS[packageName] == nil {
b.structsToGenerateTS[packageName] = make(map[string]interface{})
Expand Down Expand Up @@ -231,6 +319,13 @@ func (b *Bindings) SetTsSuffix(postfix string) *Bindings {
return b
}

func (b *Bindings) SetOutputType(outputType string) *Bindings {
if outputType == "interfaces" {
b.tsInterface = true
}
return b
}

func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, structsToGenerate := range b.structsToGenerateTS {
Expand All @@ -241,6 +336,16 @@ func (b *Bindings) getAllStructNames() *slicer.StringSlicer {
return &result
}

func (b *Bindings) getAllEnumNames() *slicer.StringSlicer {
var result slicer.StringSlicer
for packageName, enumsToGenerate := range b.enumsToGenerateTS {
for enumName := range enumsToGenerate {
result.Add(packageName + "." + enumName)
}
}
return &result
}

func (b *Bindings) hasExportedJSONFields(typeOf reflect.Type) bool {
for i := 0; i < typeOf.NumField(); i++ {
jsonFieldName := ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestConflictingPackageName(t *testing.T) {

// setup
testLogger := &logger.Logger{}
b := binding.NewBindings(testLogger, []interface{}{&HandlerTest{}}, []interface{}{}, false)
b := binding.NewBindings(testLogger, []interface{}{&HandlerTest{}}, []interface{}{}, false, []interface{}{})

// then
err := b.GenerateGoBindings(generationDir)
Expand Down
50 changes: 50 additions & 0 deletions v2/internal/binding/binding_test/binding_importedenum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package binding_test

import "github.com/wailsapp/wails/v2/internal/binding/binding_test/binding_test_import"

type ImportedEnumStruct struct {
EnumValue binding_test_import.ImportedEnum `json:"EnumValue"`
}

func (s ImportedEnumStruct) Get() ImportedEnumStruct {
return s
}

var ImportedEnumTest = BindingTest{
name: "ImportedEnum",
structs: []interface{}{
&ImportedEnumStruct{},
},
enums: []interface{}{
binding_test_import.AllImportedEnumValues,
},
exemptions: nil,
shouldError: false,
want: `export namespace binding_test {

export class ImportedEnumStruct {
EnumValue: binding_test_import.ImportedEnum;

static createFrom(source: any = {}) {
return new ImportedEnumStruct(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.EnumValue = source["EnumValue"];
}
}

}

export namespace binding_test_import {

export enum ImportedEnum {
Value1 = "value1",
Value2 = "value2",
Value3 = "value3",
}

}
`,
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestPromises(t *testing.T) {

// setup
testLogger := &logger.Logger{}
b := binding.NewBindings(testLogger, []interface{}{&PromisesTest{}}, []interface{}{}, false)
b := binding.NewBindings(testLogger, []interface{}{&PromisesTest{}}, []interface{}{}, false, []interface{}{})

// then
err := b.GenerateGoBindings(generationDir)
Expand Down
14 changes: 11 additions & 3 deletions v2/internal/binding/binding_test/binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import (
type BindingTest struct {
name string
structs []interface{}
enums []interface{}
exemptions []interface{}
want string
shouldError bool
TsGenerationOptionsTest
}

type TsGenerationOptionsTest struct {
TsPrefix string
TsSuffix string
TsPrefix string
TsSuffix string
TsOutputType string
}

func TestBindings_GenerateModels(t *testing.T) {
Expand All @@ -31,12 +33,17 @@ func TestBindings_GenerateModels(t *testing.T) {
ImportedStructTest,
ImportedSliceTest,
ImportedMapTest,
ImportedEnumTest,
NestedFieldTest,
NonStringMapKeyTest,
SingleFieldTest,
MultistructTest,
EmptyStructTest,
GeneratedJsEntityTest,
GeneratedJsEntityWithIntEnumTest,
GeneratedJsEntityWithStringEnumTest,
GeneratedJsEntityWithEnumTsName,
GeneratedJsEntityWithNestedStructInterfacesTest,
AnonymousSubStructTest,
AnonymousSubStructMultiLevelTest,
GeneratedJsEntityWithNestedStructTest,
Expand All @@ -46,13 +53,14 @@ func TestBindings_GenerateModels(t *testing.T) {
testLogger := &logger.Logger{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := binding.NewBindings(testLogger, tt.structs, tt.exemptions, false)
b := binding.NewBindings(testLogger, tt.structs, tt.exemptions, false, tt.enums)
for _, s := range tt.structs {
err := b.Add(s)
require.NoError(t, err)
}
b.SetTsPrefix(tt.TsPrefix)
b.SetTsSuffix(tt.TsSuffix)
b.SetOutputType(tt.TsOutputType)
got, err := b.GenerateModels()
if (err != nil) != tt.shouldError {
t.Errorf("GenerateModels() error = %v, shouldError %v", err, tt.shouldError)
Expand Down
Loading
Loading