From 3255956ef06239ba41ad093354ffafe14dbcd980 Mon Sep 17 00:00:00 2001 From: Yusuke Tsutsumi Date: Sat, 16 Nov 2024 11:29:52 -0800 Subject: [PATCH] feat: adding aepcli core openapi convert command exposing aep-lib's conversion functionality as a command-line will help with consumption of these aep-adjacent OpenAPI specifications in clients not written in GoLang. --- cmd/aepcli/core.go | 186 +++++++++++++++++++++++++++++++++++++++++++++ cmd/aepcli/main.go | 108 -------------------------- docs/userguide.md | 10 +++ go.mod | 2 +- go.sum | 4 + 5 files changed, 201 insertions(+), 109 deletions(-) create mode 100644 cmd/aepcli/core.go diff --git a/cmd/aepcli/core.go b/cmd/aepcli/core.go new file mode 100644 index 0000000..4e4d892 --- /dev/null +++ b/cmd/aepcli/core.go @@ -0,0 +1,186 @@ +package main + +import ( + "fmt" + "log/slog" + "os" + + "github.com/aep-dev/aep-lib-go/pkg/api" + "github.com/aep-dev/aep-lib-go/pkg/openapi" + "github.com/aep-dev/aepcli/internal/config" + "github.com/spf13/cobra" +) + +func handleCoreCommand(additionalArgs []string, configFile string) error { + coreCmd := &cobra.Command{ + Use: "core", + Args: cobra.MinimumNArgs(1), + Short: "Core API management commands", + } + + coreCmd.AddCommand(openAPICommand()) + coreCmd.AddCommand(configCmd(configFile)) + + coreCmd.SetArgs(additionalArgs) + if err := coreCmd.Execute(); err != nil { + return fmt.Errorf("error executing core command: %v", err) + } + return nil +} + +func openAPICommand() *cobra.Command { + c := &cobra.Command{ + Use: "openapi", + Short: "OpenAPI commands", + } + var inputPath string + var outputPath string + var pathPrefix string + + convertCmd := &cobra.Command{ + Use: "convert", + Short: "Best effort conversion of OpenAPI specification to an AEP API", + Run: func(cmd *cobra.Command, args []string) { + if inputPath == "" { + fmt.Println("Input path is required") + os.Exit(1) + } + slog.Debug("Converting OpenAPI spec", "inputPath", inputPath, "outputPath", outputPath, "pathPrefix", pathPrefix) + + originalOAS, err := openapi.FetchOpenAPI(inputPath) + if err != nil { + fmt.Printf("Error fetching OpenAPI spec: %v\n", err) + os.Exit(1) + } + + api, err := api.GetAPI(originalOAS, "", pathPrefix) + if err != nil { + fmt.Printf("Error converting to AEP API: %v\n", err) + os.Exit(1) + } + + finalOAS, err := api.ConvertToOpenAPIBytes() + if err != nil { + fmt.Printf("Error converting to OpenAPI: %v\n", err) + os.Exit(1) + } + + if outputPath == "" { + fmt.Println(string(finalOAS)) + } else { + err = os.WriteFile(outputPath, []byte(finalOAS), 0644) + if err != nil { + fmt.Printf("Error writing output file: %v\n", err) + os.Exit(1) + } + } + + }, + } + + convertCmd.Flags().StringVarP(&inputPath, "input", "i", "", "Input OpenAPI specification file path") + convertCmd.Flags().StringVarP(&outputPath, "output", "o", "", "Output file path. If unset, print to stdout") + convertCmd.Flags().StringVar(&pathPrefix, "path-prefix", "", "Path prefix to strip from paths when evaluating resource hierarchy") + convertCmd.MarkFlagRequired("input") + + c.AddCommand(convertCmd) + return c +} + +func configCmd(configFile string) *cobra.Command { + var openAPIPath string + var overwrite bool + var api config.API + var serverURL string + var headers []string + var pathPrefix string + + configCmd := &cobra.Command{ + Use: "config", + Short: "Manage core API configurations", + } + + addCmd := &cobra.Command{ + Use: "add [name]", + Short: "Add a new core API configuration", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + api = config.API{ + Name: args[0], + OpenAPIPath: openAPIPath, + ServerURL: serverURL, + Headers: headers, + PathPrefix: pathPrefix, + } + if err := config.WriteAPIWithName(configFile, api, overwrite); err != nil { + fmt.Printf("Error writing API config: %v\n", err) + os.Exit(1) + } + fmt.Printf("Core API configuration '%s' added successfully\n", args[0]) + }, + } + + addCmd.Flags().StringVar(&openAPIPath, "openapi-path", "", "Path to OpenAPI specification file") + addCmd.Flags().StringArrayVar(&headers, "header", []string{}, "Headers in format key=value") + addCmd.Flags().StringVar(&serverURL, "server-url", "", "Server URL") + addCmd.Flags().StringVar(&pathPrefix, "path-prefix", "", "Path prefix") + addCmd.Flags().BoolVar(&overwrite, "overwrite", false, "Overwrite existing configuration") + + readCmd := &cobra.Command{ + Use: "get [name]", + Short: "Get an API configuration", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.ReadConfigFromFile(configFile) + if err != nil { + fmt.Printf("Error reading config file: %v\n", err) + os.Exit(1) + } + + api, exists := cfg.APIs[args[0]] + if !exists { + fmt.Printf("No API configuration found with name '%s'\n", args[0]) + os.Exit(1) + } + + fmt.Printf("Name: %s\n", api.Name) + fmt.Printf("OpenAPI Path: %s\n", api.OpenAPIPath) + fmt.Printf("Server URL: %s\n", api.ServerURL) + fmt.Printf("Headers: %v\n", api.Headers) + fmt.Printf("Path Prefix: %s\n", api.PathPrefix) + }, + } + + listCmd := &cobra.Command{ + Use: "list", + Short: "List all API configurations", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + apis, err := config.ListAPIs(configFile) + if err != nil { + fmt.Printf("Error listing APIs: %v\n", err) + os.Exit(1) + } + + if len(apis) == 0 { + fmt.Println("No API configurations found") + return + } + + for _, api := range apis { + fmt.Printf("Name: %s\n", api.Name) + fmt.Printf("OpenAPI Path: %s\n", api.OpenAPIPath) + fmt.Printf("Server URL: %s\n", api.ServerURL) + fmt.Printf("Headers: %v\n", api.Headers) + fmt.Printf("Path Prefix: %s\n", api.PathPrefix) + fmt.Println() + } + }, + } + + configCmd.AddCommand(addCmd) + configCmd.AddCommand(readCmd) + configCmd.AddCommand(listCmd) + + return configCmd +} diff --git a/cmd/aepcli/main.go b/cmd/aepcli/main.go index bcae701..c8b9b67 100644 --- a/cmd/aepcli/main.go +++ b/cmd/aepcli/main.go @@ -152,111 +152,3 @@ func setLogLevel(levelAsString string) error { slog.SetLogLoggerLevel(level) return nil } - -func handleCoreCommand(additionalArgs []string, configFile string) error { - var openAPIPath string - var overwrite bool - var api config.API - var serverURL string - var headers []string - var pathPrefix string - - coreCmd := &cobra.Command{ - Use: "core", - Short: "Core API management commands", - } - - configCmd := &cobra.Command{ - Use: "config", - Short: "Manage core API configurations", - } - - addCmd := &cobra.Command{ - Use: "add [name]", - Short: "Add a new core API configuration", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - api = config.API{ - Name: args[0], - OpenAPIPath: openAPIPath, - ServerURL: serverURL, - Headers: headers, - PathPrefix: pathPrefix, - } - if err := config.WriteAPIWithName(configFile, api, overwrite); err != nil { - fmt.Printf("Error writing API config: %v\n", err) - os.Exit(1) - } - fmt.Printf("Core API configuration '%s' added successfully\n", args[0]) - }, - } - - addCmd.Flags().StringVar(&openAPIPath, "openapi-path", "", "Path to OpenAPI specification file") - addCmd.Flags().StringArrayVar(&headers, "header", []string{}, "Headers in format key=value") - addCmd.Flags().StringVar(&serverURL, "server-url", "", "Server URL") - addCmd.Flags().StringVar(&pathPrefix, "path-prefix", "", "Path prefix") - addCmd.Flags().BoolVar(&overwrite, "overwrite", false, "Overwrite existing configuration") - - readCmd := &cobra.Command{ - Use: "get [name]", - Short: "Get an API configuration", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - cfg, err := config.ReadConfigFromFile(configFile) - if err != nil { - fmt.Printf("Error reading config file: %v\n", err) - os.Exit(1) - } - - api, exists := cfg.APIs[args[0]] - if !exists { - fmt.Printf("No API configuration found with name '%s'\n", args[0]) - os.Exit(1) - } - - fmt.Printf("Name: %s\n", api.Name) - fmt.Printf("OpenAPI Path: %s\n", api.OpenAPIPath) - fmt.Printf("Server URL: %s\n", api.ServerURL) - fmt.Printf("Headers: %v\n", api.Headers) - fmt.Printf("Path Prefix: %s\n", api.PathPrefix) - }, - } - - listCmd := &cobra.Command{ - Use: "list", - Short: "List all API configurations", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - apis, err := config.ListAPIs(configFile) - if err != nil { - fmt.Printf("Error listing APIs: %v\n", err) - os.Exit(1) - } - - if len(apis) == 0 { - fmt.Println("No API configurations found") - return - } - - for _, api := range apis { - fmt.Printf("Name: %s\n", api.Name) - fmt.Printf("OpenAPI Path: %s\n", api.OpenAPIPath) - fmt.Printf("Server URL: %s\n", api.ServerURL) - fmt.Printf("Headers: %v\n", api.Headers) - fmt.Printf("Path Prefix: %s\n", api.PathPrefix) - fmt.Println() - } - }, - } - - configCmd.AddCommand(addCmd) - configCmd.AddCommand(readCmd) - configCmd.AddCommand(listCmd) - coreCmd.AddCommand(configCmd) - - coreCmd.SetArgs(additionalArgs) - if err := coreCmd.Execute(); err != nil { - return fmt.Errorf("error executing core command: %v", err) - } - return nil -} diff --git a/docs/userguide.md b/docs/userguide.md index 195cf00..3069479 100644 --- a/docs/userguide.md +++ b/docs/userguide.md @@ -195,6 +195,16 @@ Request: GET http://localhost:8081/publishers/standard-house/books/foo See `aepcli core --help` for commands for aepcli (e.g. config) +#### convert + +Parse an existing OpenAPI definition as much as possible and convert it to an AEP-compliant OpenAPI definition. + +This is helpful as a tool to do OpenAPI conversion when it is difficult to update the definition (e.g. the generation logic is in a third-party codebase). It's availability as a CLI also simplifies the process of integrating into CD processes to publish the spec publicly. + +```bash +aepcli core openapi convert --input-path=openapis/bookstore.json --output-path=openapis/bookstore.aep.json --path-prefix=/bookstore +``` + ## OpenAPI Definitions ### OAS definitions supported diff --git a/go.mod b/go.mod index f0cb214..9b5d259 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.22.3 require ( github.com/BurntSushi/toml v1.4.0 // indirect - github.com/aep-dev/aep-lib-go v0.0.0-20241109204312-789b93c37d17 // indirect + github.com/aep-dev/aep-lib-go v0.0.0-20241116210104-baaf5068c543 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 0301672..a263c44 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/aep-dev/aep-lib-go v0.0.0-20241109204312-789b93c37d17 h1:I6JqfEQyyMZ7jMTFv2OrLBOzqCa8dFiu/FSsEx0Bty0= github.com/aep-dev/aep-lib-go v0.0.0-20241109204312-789b93c37d17/go.mod h1:M+h1D6T2uIUPelmaEsJbjR6JhqKsTlPX3lxp25zQQsk= +github.com/aep-dev/aep-lib-go v0.0.0-20241116073126-52817379fc1d h1:juPQ462ve7LE4S7u++5ZyVY4F/VueXXLa6QIr6LpVFc= +github.com/aep-dev/aep-lib-go v0.0.0-20241116073126-52817379fc1d/go.mod h1:M+h1D6T2uIUPelmaEsJbjR6JhqKsTlPX3lxp25zQQsk= +github.com/aep-dev/aep-lib-go v0.0.0-20241116210104-baaf5068c543 h1:47/49jTwjRRZWLiiaWvXNWf+6p7IqLrkWwzPsGS2scI= +github.com/aep-dev/aep-lib-go v0.0.0-20241116210104-baaf5068c543/go.mod h1:M+h1D6T2uIUPelmaEsJbjR6JhqKsTlPX3lxp25zQQsk= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=