diff --git a/abi/generator.go b/abi/generator.go index a00b07f2..c9c5ce48 100644 --- a/abi/generator.go +++ b/abi/generator.go @@ -3,150 +3,33 @@ package main import ( - "fmt" - "go/format" "io/fs" "os" "path/filepath" - "sort" "strings" "github.com/tonkeeper/tongo/abi/parser" ) -const HEADER = `package abi -// Code autogenerated. DO NOT EDIT. - -import ( -%v -) - -` -const SCHEMAS_PATH = "schemas/" - -func mergeMethods(methods []parser.GetMethod) ([]parser.GetMethod, error) { - methodsMap := map[string]parser.GetMethod{} - var golangNamedMethods []parser.GetMethod - for _, method := range methods { - current, ok := methodsMap[method.Name] - if !ok { - methodsMap[method.Name] = method - continue - } - if len(current.Input.StackValues) > 0 || len(method.Input.StackValues) > 0 { - return nil, fmt.Errorf("method '%s' has a version with input params, it has to be defined with golang_name to avoid collision", method.Name) - } - current.Output = append(current.Output, method.Output...) - methodsMap[method.Name] = current - } - var results []parser.GetMethod - for _, method := range methodsMap { - results = append(results, method) - } - results = append(results, golangNamedMethods...) - sort.Slice(results, func(i, j int) bool { - return results[i].Name < results[j].Name - }) - return results, nil -} +const schemasPath = "schemas/" func main() { - var abi parser.ABI - var methods []parser.GetMethod - filepath.Walk(SCHEMAS_PATH, func(path string, info fs.FileInfo, err error) error { - if !strings.HasSuffix(info.Name(), ".xml") { - return nil - } - scheme, err := os.ReadFile(path) - if err != nil { - panic(err) + var xmlFiles []string + filepath.Walk(schemasPath, func(path string, info fs.FileInfo, err error) error { + if strings.HasSuffix(info.Name(), ".xml") { + xmlFiles = append(xmlFiles, path) } - a, err := parser.ParseABI(scheme) - if err != nil { - panic(err) - } - methods = append(methods, a.Methods...) - abi.ExtOut = append(abi.ExtOut, a.ExtOut...) - abi.ExtIn = append(abi.ExtIn, a.ExtIn...) - abi.Internals = append(abi.Internals, a.Internals...) - abi.JettonPayloads = append(abi.JettonPayloads, a.JettonPayloads...) - abi.NFTPayloads = append(abi.NFTPayloads, a.NFTPayloads...) - abi.Types = append(abi.Types, a.Types...) - abi.Interfaces = append(abi.Interfaces, a.Interfaces...) return nil }) - methods, err := mergeMethods(methods) - if err != nil { - panic(err) - } - abi.Methods = methods - - gen, err := parser.NewGenerator(nil, abi) + files, err := parser.Generate(xmlFiles, parser.GenerateOptions{}) if err != nil { panic(err) } - types := gen.CollectedTypes() - msgDecoder := gen.GenerateMsgDecoder() - - getMethods, simpleMethods, err := gen.GetMethods() - if err != nil { - panic(err) - } - invocationOrder, err := gen.RenderInvocationOrderList(simpleMethods) - if err != nil { - panic(err) - } - messagesMD, err := gen.RenderMessagesMD() - if err != nil { - panic(err) - } - - jettons, err := gen.RenderJetton() - if err != nil { - panic(err) - } - - nfts, err := gen.RenderNFT() - if err != nil { - panic(err) - } - - contractErrors, err := gen.RenderContractErrors() - if err != nil { - panic(err) - } - - for _, f := range [][]string{ - {types, "types.go", `"github.com/tonkeeper/tongo/tlb"`, `"fmt"`, `"encoding/json"`}, - {msgDecoder, "messages_generated.go", `"github.com/tonkeeper/tongo/tlb"`}, - {getMethods, "get_methods.go", `"context"`, `"fmt"`, `"github.com/tonkeeper/tongo/ton"`, `"github.com/tonkeeper/tongo/boc"`, `"github.com/tonkeeper/tongo/tlb"`}, - {invocationOrder, "interfaces.go", `"github.com/tonkeeper/tongo/ton"`}, - {jettons, "jetton_msg_types.go", `"github.com/tonkeeper/tongo/boc"`, `"github.com/tonkeeper/tongo/tlb"`}, - {nfts, "nfts_msg_types.go", `"github.com/tonkeeper/tongo/boc"`, `"github.com/tonkeeper/tongo/tlb"`}, - {contractErrors, "contracts_errors.go"}, - } { - file, err := os.Create(f[1]) - if err != nil { - panic(err) - } - code := []byte(fmt.Sprintf(HEADER, strings.Join(f[2:], "\n")) + f[0]) - formatedCode, err := format.Source(code) - if err != nil { - formatedCode = code - //panic(err) - } - _, err = file.Write(formatedCode) - if err != nil { - panic(err) - } - err = file.Close() - if err != nil { + for filename, content := range files { + if err := os.WriteFile(filename, content, 0644); err != nil { panic(err) } } - if err := os.WriteFile("messages.md", []byte(messagesMD), 0644); err != nil { - panic(err) - } } diff --git a/abi/parser/generate.go b/abi/parser/generate.go new file mode 100644 index 00000000..dd7e41d6 --- /dev/null +++ b/abi/parser/generate.go @@ -0,0 +1,174 @@ +package parser + +import ( + "fmt" + "go/format" + "os" + "sort" + "strings" +) + +// GenerateOptions configures code generation. +type GenerateOptions struct { + // PackageName is the package name for generated code. Defaults to "abi". + PackageName string +} + +// GeneratedFiles maps filenames to their formatted Go code content. +type GeneratedFiles map[string][]byte + +const defaultPackageName = "abi" + +const headerTemplate = `package %s +// Code autogenerated. DO NOT EDIT. + +import ( +%v +) + +` + +// Generate parses XML ABI files and generates Go code. +// Returns a map of filename to formatted Go source code. +func Generate(xmlFiles []string, opts GenerateOptions) (GeneratedFiles, error) { + if opts.PackageName == "" { + opts.PackageName = defaultPackageName + } + + abi, err := parseAndMergeABIs(xmlFiles) + if err != nil { + return nil, fmt.Errorf("Generate() failed: %v", err) + } + + gen, err := NewGenerator(nil, abi) + if err != nil { + return nil, fmt.Errorf("Generate() failed: %v", err) + } + + types := gen.CollectedTypes() + msgDecoder := gen.GenerateMsgDecoder() + + getMethods, simpleMethods, err := gen.GetMethods() + if err != nil { + return nil, fmt.Errorf("Generate() failed: %v", err) + } + + invocationOrder, err := gen.RenderInvocationOrderList(simpleMethods) + if err != nil { + return nil, fmt.Errorf("Generate() failed: %v", err) + } + + jettons, err := gen.RenderJetton() + if err != nil { + return nil, fmt.Errorf("Generate() failed: %v", err) + } + + nfts, err := gen.RenderNFT() + if err != nil { + return nil, fmt.Errorf("Generate() failed: %v", err) + } + + contractErrors, err := gen.RenderContractErrors() + if err != nil { + return nil, fmt.Errorf("Generate() failed: %v", err) + } + + messagesMD, err := gen.RenderMessagesMD() + if err != nil { + return nil, fmt.Errorf("Generate() failed: %v", err) + } + + files := []struct { + content string + filename string + imports []string + }{ + {types, "types.go", []string{`"github.com/tonkeeper/tongo/tlb"`, `"fmt"`, `"encoding/json"`}}, + {msgDecoder, "messages_generated.go", []string{`"github.com/tonkeeper/tongo/tlb"`}}, + {getMethods, "get_methods.go", []string{`"context"`, `"fmt"`, `"github.com/tonkeeper/tongo/ton"`, `"github.com/tonkeeper/tongo/boc"`, `"github.com/tonkeeper/tongo/tlb"`}}, + {invocationOrder, "interfaces.go", []string{`"github.com/tonkeeper/tongo/ton"`}}, + {jettons, "jetton_msg_types.go", []string{`"github.com/tonkeeper/tongo/boc"`, `"github.com/tonkeeper/tongo/tlb"`}}, + {nfts, "nfts_msg_types.go", []string{`"github.com/tonkeeper/tongo/boc"`, `"github.com/tonkeeper/tongo/tlb"`}}, + {contractErrors, "contracts_errors.go", nil}, + } + + result := make(GeneratedFiles) + for _, f := range files { + header := fmt.Sprintf(headerTemplate, opts.PackageName, strings.Join(f.imports, "\n")) + code := []byte(header + f.content) + + formatted, err := format.Source(code) + if err != nil { + formatted = code // keep unformatted if formatting fails + } + result[f.filename] = formatted + } + + result["messages.md"] = []byte(messagesMD) + + return result, nil +} + +func parseAndMergeABIs(xmlFiles []string) (ABI, error) { + var abi ABI + var methods []GetMethod + + for _, path := range xmlFiles { + data, err := os.ReadFile(path) + if err != nil { + return ABI{}, fmt.Errorf("parseAndMergeABIs() failed reading %s: %v", path, err) + } + + a, err := ParseABI(data) + if err != nil { + return ABI{}, fmt.Errorf("parseAndMergeABIs() failed parsing %s: %v", path, err) + } + + methods = append(methods, a.Methods...) + abi.ExtOut = append(abi.ExtOut, a.ExtOut...) + abi.ExtIn = append(abi.ExtIn, a.ExtIn...) + abi.Internals = append(abi.Internals, a.Internals...) + abi.JettonPayloads = append(abi.JettonPayloads, a.JettonPayloads...) + abi.NFTPayloads = append(abi.NFTPayloads, a.NFTPayloads...) + abi.Types = append(abi.Types, a.Types...) + abi.Interfaces = append(abi.Interfaces, a.Interfaces...) + } + + merged, err := mergeMethods(methods) + if err != nil { + return ABI{}, fmt.Errorf("parseAndMergeABIs() failed: %v", err) + } + abi.Methods = merged + + return abi, nil +} + +func mergeMethods(methods []GetMethod) ([]GetMethod, error) { + methodsMap := map[string]GetMethod{} + var golangNamedMethods []GetMethod + + for _, method := range methods { + current, ok := methodsMap[method.Name] + if !ok { + methodsMap[method.Name] = method + continue + } + if len(current.Input.StackValues) > 0 || len(method.Input.StackValues) > 0 { + return nil, fmt.Errorf("method '%s' has a version with input params, it has to be defined with golang_name to avoid collision", method.Name) + } + current.Output = append(current.Output, method.Output...) + methodsMap[method.Name] = current + } + + var results []GetMethod + for _, method := range methodsMap { + results = append(results, method) + } + results = append(results, golangNamedMethods...) + + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) + + return results, nil +}