From 63c4da2e413bd34f75824e7ffd5a46bcf21a8399 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Wed, 11 Jun 2025 12:01:15 +0200 Subject: [PATCH 1/2] Autogenerate list of connector capabilities for docs generation --- Justfile | 7 +- docs/other/connector-capabilities.json | 1 + tools/compile-capabilities/main.go | 129 +++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 docs/other/connector-capabilities.json create mode 100644 tools/compile-capabilities/main.go diff --git a/Justfile b/Justfile index a912cd429..a594cd9f7 100644 --- a/Justfile +++ b/Justfile @@ -3,7 +3,7 @@ set dotenv-load default: @just --list -pre-commit: tidy generate lint openapi compile-plugins +pre-commit: tidy generate lint openapi compile-plugins compile-connector-capabilities pc: pre-commit lint: @@ -21,6 +21,11 @@ compile-connector-configs: ./compile-configs --path {{justfile_directory()}}/internal/connectors/plugins/public --output {{justfile_directory()}}/openapi/v3/v3-connectors-config.yaml @rm ./compile-configs +compile-connector-capabilities: + @go build -o compile-capabilities {{justfile_directory()}}/tools/compile-capabilities + ./compile-capabilities --path {{justfile_directory()}}/internal/connectors/plugins/public --output {{justfile_directory()}}/docs/other/connector-capabilities.json + @rm ./compile-capabilities + [group('openapi')] compile-api-yaml: compile-connector-configs @npx openapi-merge-cli --config {{justfile_directory()}}/openapi/openapi-merge.json diff --git a/docs/other/connector-capabilities.json b/docs/other/connector-capabilities.json new file mode 100644 index 000000000..3ac65e97d --- /dev/null +++ b/docs/other/connector-capabilities.json @@ -0,0 +1 @@ +{"adyen":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_CREATE_WEBHOOKS","CAPABILITY_TRANSLATE_WEBHOOKS"],"atlar":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_FETCH_OTHERS"],"bankingcircle":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_CREATE_BANK_ACCOUNT","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT"],"column":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_CREATE_BANK_ACCOUNT","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT","CAPABILITY_CREATE_WEBHOOKS","CAPABILITY_TRANSLATE_WEBHOOKS"],"currencycloud":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT"],"dummypay":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_ALLOW_FORMANCE_ACCOUNT_CREATION","CAPABILITY_ALLOW_FORMANCE_PAYMENT_CREATION","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT"],"generic":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_ALLOW_FORMANCE_ACCOUNT_CREATION","CAPABILITY_ALLOW_FORMANCE_PAYMENT_CREATION"],"increase":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT","CAPABILITY_CREATE_BANK_ACCOUNT","CAPABILITY_TRANSLATE_WEBHOOKS","CAPABILITY_CREATE_WEBHOOKS"],"mangopay":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_FETCH_OTHERS","CAPABILITY_CREATE_BANK_ACCOUNT","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT","CAPABILITY_CREATE_WEBHOOKS","CAPABILITY_TRANSLATE_WEBHOOKS"],"modulr":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT"],"moneycorp":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT"],"qonto":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS"],"stripe":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT"],"wise":["CAPABILITY_FETCH_ACCOUNTS","CAPABILITY_FETCH_BALANCES","CAPABILITY_FETCH_EXTERNAL_ACCOUNTS","CAPABILITY_FETCH_PAYMENTS","CAPABILITY_FETCH_OTHERS","CAPABILITY_CREATE_TRANSFER","CAPABILITY_CREATE_PAYOUT","CAPABILITY_CREATE_WEBHOOKS","CAPABILITY_TRANSLATE_WEBHOOKS"]} \ No newline at end of file diff --git a/tools/compile-capabilities/main.go b/tools/compile-capabilities/main.go new file mode 100644 index 000000000..c46e2d901 --- /dev/null +++ b/tools/compile-capabilities/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "log" + "os" + "path/filepath" + "strings" +) + +var ( + connectorCapabilities map[string][]string + + path = flag.String("path", "./", "Path to the directory") + outputFilename = flag.String("output", "connector-capabilities.json", "Name of the output file to write") +) + +func main() { + flag.Parse() + if *path == "" { + log.Fatal("path flag is required") + } + if *outputFilename == "" { + log.Fatal("output flag is required") + } + + entries, err := os.ReadDir(*path) + if err != nil { + log.Fatal(err) + } + + connectorCapabilities = make(map[string][]string) + for _, e := range entries { + if !e.IsDir() { + continue + } + + capabilities, err := readCapabilities(e.Name()) + if err != nil { + log.Fatal(err) + } + + connectorCapabilities[e.Name()] = capabilities + } + + d, err := json.Marshal(&connectorCapabilities) + if err != nil { + log.Fatalf("error: %v", err) + } + + f, err := os.Create(*outputFilename) + if err != nil { + log.Fatalf("error: %v", err) + } + defer f.Close() + + _, err = f.Write(d) + if err != nil { + log.Fatalf("error: %v", err) + } +} + +func readCapabilities(name string) ([]string, error) { + capabilities := make([]string, 0) + // Verify the opened file is within the intended directory + absPath, err := filepath.Abs(*path) + if err != nil { + return capabilities, fmt.Errorf("failed to resolve directory %s: %w", *path, err) + } + absFile, err := filepath.Abs(filepath.Join(*path, name, "capabilities.go")) + if err != nil { + return capabilities, err + } + if !strings.HasPrefix(absFile, absPath) { + return capabilities, fmt.Errorf("invalid path: %s", name) + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, filepath.Join(*path, name, "capabilities.go"), nil, 0) + if err != nil { + return capabilities, err + } + + var parseErr error + ast.Inspect(node, func(n ast.Node) bool { + if decl, ok := n.(*ast.GenDecl); ok { + for _, spec := range decl.Specs { + if valueSpec, ok := spec.(*ast.ValueSpec); ok { + for i, sliceName := range valueSpec.Names { + if sliceName.Name != "capabilities" { + continue + } + if len(valueSpec.Values) > i { + if compositeLit, ok := valueSpec.Values[i].(*ast.CompositeLit); ok { + for _, elt := range compositeLit.Elts { + str, err := astString(fset, elt) + if err != nil { + parseErr = err + return false + } + capabilities = append(capabilities, strings.TrimPrefix(str, "models.")) + } + } + } + return false + } + } + } + } + return true + }) + return capabilities, parseErr +} + +// Helper function to convert AST node back to string +func astString(fset *token.FileSet, node ast.Node) (string, error) { + var buf bytes.Buffer + if err := printer.Fprint(&buf, fset, node); err != nil { + return "", fmt.Errorf("couldn't covert ast node into string: %w", err) + } + return buf.String(), nil +} From e0f098c8f033b9aa6f2ecdc1ca02e1f6fef480c1 Mon Sep 17 00:00:00 2001 From: Crimson Thompson Date: Thu, 12 Jun 2025 13:04:05 +0200 Subject: [PATCH 2/2] Use registry to fetch capability strings instead of parsing from files --- tools/compile-capabilities/main.go | 78 +++++------------------------- 1 file changed, 11 insertions(+), 67 deletions(-) diff --git a/tools/compile-capabilities/main.go b/tools/compile-capabilities/main.go index c46e2d901..4540231db 100644 --- a/tools/compile-capabilities/main.go +++ b/tools/compile-capabilities/main.go @@ -1,18 +1,15 @@ package main import ( - "bytes" "encoding/json" "flag" "fmt" - "go/ast" - "go/parser" - "go/printer" - "go/token" "log" "os" - "path/filepath" - "strings" + + _ "github.com/formancehq/payments/internal/connectors/plugins/public" + "github.com/formancehq/payments/internal/connectors/plugins/registry" + "github.com/formancehq/payments/internal/models" ) var ( @@ -42,12 +39,12 @@ func main() { continue } - capabilities, err := readCapabilities(e.Name()) + capabilities, err := registry.GetCapabilities(e.Name()) if err != nil { log.Fatal(err) } - connectorCapabilities[e.Name()] = capabilities + connectorCapabilities[e.Name()] = toString(capabilities) } d, err := json.Marshal(&connectorCapabilities) @@ -67,63 +64,10 @@ func main() { } } -func readCapabilities(name string) ([]string, error) { - capabilities := make([]string, 0) - // Verify the opened file is within the intended directory - absPath, err := filepath.Abs(*path) - if err != nil { - return capabilities, fmt.Errorf("failed to resolve directory %s: %w", *path, err) - } - absFile, err := filepath.Abs(filepath.Join(*path, name, "capabilities.go")) - if err != nil { - return capabilities, err - } - if !strings.HasPrefix(absFile, absPath) { - return capabilities, fmt.Errorf("invalid path: %s", name) - } - - fset := token.NewFileSet() - node, err := parser.ParseFile(fset, filepath.Join(*path, name, "capabilities.go"), nil, 0) - if err != nil { - return capabilities, err - } - - var parseErr error - ast.Inspect(node, func(n ast.Node) bool { - if decl, ok := n.(*ast.GenDecl); ok { - for _, spec := range decl.Specs { - if valueSpec, ok := spec.(*ast.ValueSpec); ok { - for i, sliceName := range valueSpec.Names { - if sliceName.Name != "capabilities" { - continue - } - if len(valueSpec.Values) > i { - if compositeLit, ok := valueSpec.Values[i].(*ast.CompositeLit); ok { - for _, elt := range compositeLit.Elts { - str, err := astString(fset, elt) - if err != nil { - parseErr = err - return false - } - capabilities = append(capabilities, strings.TrimPrefix(str, "models.")) - } - } - } - return false - } - } - } - } - return true - }) - return capabilities, parseErr -} - -// Helper function to convert AST node back to string -func astString(fset *token.FileSet, node ast.Node) (string, error) { - var buf bytes.Buffer - if err := printer.Fprint(&buf, fset, node); err != nil { - return "", fmt.Errorf("couldn't covert ast node into string: %w", err) +func toString(list []models.Capability) []string { + result := make([]string, len(list)) + for i, item := range list { + result[i] = fmt.Sprintf("CAPABILITY_%s", item.String()) } - return buf.String(), nil + return result }