Skip to content

Commit

Permalink
feat: add config add/list/get
Browse files Browse the repository at this point in the history
To make it easier to operate on config files, adding utility commands
to interface with the config object.
  • Loading branch information
toumorokoshi committed Nov 2, 2024
1 parent 84b2d0b commit c162cc8
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 27 deletions.
158 changes: 142 additions & 16 deletions cmd/aepcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,59 +25,77 @@ func main() {

func aepcli(args []string) error {
var logLevel string
var fileOrAlias string
var fileAliasOrCore string
var additionalArgs []string
var headers []string
var pathPrefix string
var serverURL string
var configFileVar string
var s *service.Service

rootCmd := &cobra.Command{
Use: "aepcli [host or api alias] [resource or --help]",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fileOrAlias = args[0]
fileAliasOrCore = args[0]
if len(args) > 1 {
additionalArgs = args[1:]
}
},
}

var rawHeaders []string
var pathPrefix string
var serverURL string
configFile, err := config.DefaultConfigFile()
if err != nil {
return fmt.Errorf("unable to get default config file: %w", err)
}

rootCmd.Flags().SetInterspersed(false) // allow sub parsers to parse subsequent flags after the resource
rootCmd.PersistentFlags().StringArrayVar(&rawHeaders, "header", []string{}, "Specify headers in the format key=value")
rootCmd.PersistentFlags().StringArrayVar(&headers, "header", []string{}, "Specify headers in the format key=value")
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Set the logging level (debug, info, warn, error)")
rootCmd.PersistentFlags().StringVar(&pathPrefix, "path-prefix", "", "Specify a path prefix that is prepended to all paths in the openapi schema. This will strip them when evaluating the resource hierarchy paths.")
rootCmd.PersistentFlags().StringVar(&serverURL, "server-url", "", "Specify a URL to use for the server. If not specified, the first server URL in the OpenAPI definition will be used.")

rootCmd.PersistentFlags().StringVar(&configFileVar, "config", "", "Path to config file")
rootCmd.SetArgs(args)

if err := rootCmd.Execute(); err != nil {
return err
}

if configFileVar != "" {
configFile = configFileVar
}

if err := setLogLevel(logLevel); err != nil {
return fmt.Errorf("unable to set log level: %w", err)
}

c, err := config.ReadConfig()
c, err := config.ReadConfigFromFile(configFile)
if err != nil {
return fmt.Errorf("unable to read config: %v", err)
}

if api, ok := c.APIs[fileOrAlias]; ok {
if fileAliasOrCore == "core" {
return handleCoreCommand(additionalArgs, configFile)
}

if api, ok := c.APIs[fileAliasOrCore]; ok {
cd, err := config.ConfigDir()
if err != nil {
fmt.Println(err)
os.Exit(1)
return fmt.Errorf("unable to get config directory: %w", err)
}
if filepath.IsAbs(api.OpenAPIPath) || strings.HasPrefix(api.OpenAPIPath, "http") {
fileAliasOrCore = api.OpenAPIPath
} else {
fileAliasOrCore = filepath.Join(cd, api.OpenAPIPath)
}
fileOrAlias = filepath.Join(cd, api.OpenAPIPath)
if pathPrefix == "" {
pathPrefix = api.PathPrefix
}
rawHeaders = append(rawHeaders, api.Headers...)
headers = append(headers, api.Headers...)
serverURL = api.ServerURL
}

openapi, err := openapi.FetchOpenAPI(fileOrAlias)
openapi, err := openapi.FetchOpenAPI(fileAliasOrCore)
if err != nil {
return fmt.Errorf("unable to fetch openapi: %w", err)
}
Expand All @@ -86,12 +104,12 @@ func aepcli(args []string) error {
return fmt.Errorf("unable to get service definition: %w", err)
}

headers, err := parseHeaders(rawHeaders)
headersMap, err := parseHeaders(headers)
if err != nil {
return fmt.Errorf("unable to parse headers: %w", err)
}

s = service.NewService(*serviceDefinition, headers)
s = service.NewService(*serviceDefinition, headersMap)

result, err := s.ExecuteCommand(additionalArgs)
if err != nil {
Expand Down Expand Up @@ -130,3 +148,111 @@ 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
}
19 changes: 17 additions & 2 deletions docs/userguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,19 @@ headers = ["X-API-TOKEN=123", "X-API-CLIENT=aepcli"]
```

If you would like to use aepcli as your recommend command-line interface for
your API, you can provide a simple script to write the appropriate configuration
to your configuration file.
your API, you can provide a one-liner to add the configuration to your
configuration file:

```bash
aepcli core config add bookstore --openapi-path=$HOME/workspace/aepc/example/bookstore/v1/bookstore_openapi.json
```

You can also list and read all of the configurations you have added:

```bash
aepcli core config list
aepcli core config get bookstore
```

### specifying resource parent ids

Expand Down Expand Up @@ -167,6 +178,10 @@ lists are specified as a comma-separated list:
aepcli bookstore book-edition create --book "peter-pan" --publisher "consistent-house" --tags "fantasy,childrens"
```

### core commands

See `aepcli core --help` for commands for aepcli (e.g. config)

## OpenAPI Definitions

### OAS definitions supported
Expand Down
86 changes: 77 additions & 9 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package config

import (
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"

Expand All @@ -20,19 +22,16 @@ type API struct {
PathPrefix string
}

// ReadConfig reads the configuration from the default configuration file.
func ReadConfig() (*Config, error) {
configDir, err := ConfigDir()
if err != nil {
func ReadConfigFromFile(file string) (*Config, error) {
// Check if file exists first
if _, err := os.Stat(file); os.IsNotExist(err) {
slog.Error("Config file does not exist", "file", file)
return nil, err
}
return ReadConfigFromFile(filepath.Join(configDir, "config.toml"))
}

func ReadConfigFromFile(filename string) (*Config, error) {
var c Config
if _, err := toml.DecodeFile(filename, &c); err != nil {
return nil, fmt.Errorf("unable to decode config file at %v: %v", filename, err)
if _, err := toml.DecodeFile(file, &c); err != nil {
return nil, fmt.Errorf("unable to decode config file at %v: %v", file, err)
}
return &c, nil
}
Expand All @@ -44,3 +43,72 @@ func ConfigDir() (string, error) {
}
return filepath.Join(homeDir, ".config", "aepcli"), nil
}

// WriteAPIWithName writes a new API configuration to the specified config file.
func WriteAPIWithName(file string, api API, overwrite bool) error {
if api.Name == "" {
return errors.New("api name cannot be empty")
}

// Read existing config
cfg, err := ReadConfigFromFile(file)
if err != nil {
// If file doesn't exist yet, initialize new config
if errors.Is(err, os.ErrNotExist) {
cfg = &Config{
APIs: make(map[string]API),
}
} else {
return fmt.Errorf("failed to read existing config: %w", err)
}
}

// Check if API already exists
if _, exists := cfg.APIs[api.Name]; exists && !overwrite {
return fmt.Errorf("API with name '%s' already exists. Set --overwrite to true to overwrite", api.Name)
}

// Add/update API in config
cfg.APIs[api.Name] = api

// Ensure parent directory exists
parentDir := filepath.Dir(file)
if err := os.MkdirAll(parentDir, 0755); err != nil {
return fmt.Errorf("failed to create parent directory for config file: %w", err)
}

// Open file for writing
f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open config file: %w", err)
}
defer f.Close()

// Encode and write config
if err := toml.NewEncoder(f).Encode(cfg); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}

func DefaultConfigFile() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "config.toml"), nil
}

// ListAPIs returns a slice of all API configurations in the specified config file.
func ListAPIs(file string) ([]API, error) {
cfg, err := ReadConfigFromFile(file)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}

apis := make([]API, 0, len(cfg.APIs))
for _, api := range cfg.APIs {
apis = append(apis, api)
}
return apis, nil
}
Loading

0 comments on commit c162cc8

Please sign in to comment.