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 }