Skip to content

Commit

Permalink
Add support for interface generation and enums (#3047)
Browse files Browse the repository at this point in the history
* Add support to output ts models as interfaces

* Add support to generate enums from golang

* cleanup logs

* add missing documentation

* fix package names for enum. Fix processing enums that are in separate packages

* revert golang 1.21

* Fix spelling

* Add support for simplified version of Enum for typescriptify

* update docs

* removed unused logs

* Add tests. Fix imported enums types in models

---------

Co-authored-by: Lea Anthony <[email protected]>
  • Loading branch information
APshenkin and leaanthony authored Nov 25, 2023
1 parent 929dfb4 commit b9de31e
Show file tree
Hide file tree
Showing 24 changed files with 717 additions and 44 deletions.
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

0 comments on commit b9de31e

Please sign in to comment.