Skip to content

Commit c162cc8

Browse files
committed
feat: add config add/list/get
To make it easier to operate on config files, adding utility commands to interface with the config object.
1 parent 84b2d0b commit c162cc8

4 files changed

Lines changed: 299 additions & 27 deletions

File tree

cmd/aepcli/main.go

Lines changed: 142 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,59 +25,77 @@ func main() {
2525

2626
func aepcli(args []string) error {
2727
var logLevel string
28-
var fileOrAlias string
28+
var fileAliasOrCore string
2929
var additionalArgs []string
30+
var headers []string
31+
var pathPrefix string
32+
var serverURL string
33+
var configFileVar string
3034
var s *service.Service
3135

3236
rootCmd := &cobra.Command{
3337
Use: "aepcli [host or api alias] [resource or --help]",
3438
Args: cobra.MinimumNArgs(1),
3539
Run: func(cmd *cobra.Command, args []string) {
36-
fileOrAlias = args[0]
40+
fileAliasOrCore = args[0]
3741
if len(args) > 1 {
3842
additionalArgs = args[1:]
3943
}
4044
},
4145
}
4246

43-
var rawHeaders []string
44-
var pathPrefix string
45-
var serverURL string
47+
configFile, err := config.DefaultConfigFile()
48+
if err != nil {
49+
return fmt.Errorf("unable to get default config file: %w", err)
50+
}
51+
4652
rootCmd.Flags().SetInterspersed(false) // allow sub parsers to parse subsequent flags after the resource
47-
rootCmd.PersistentFlags().StringArrayVar(&rawHeaders, "header", []string{}, "Specify headers in the format key=value")
53+
rootCmd.PersistentFlags().StringArrayVar(&headers, "header", []string{}, "Specify headers in the format key=value")
4854
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Set the logging level (debug, info, warn, error)")
4955
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.")
5056
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.")
51-
57+
rootCmd.PersistentFlags().StringVar(&configFileVar, "config", "", "Path to config file")
5258
rootCmd.SetArgs(args)
59+
5360
if err := rootCmd.Execute(); err != nil {
5461
return err
5562
}
5663

64+
if configFileVar != "" {
65+
configFile = configFileVar
66+
}
67+
5768
if err := setLogLevel(logLevel); err != nil {
5869
return fmt.Errorf("unable to set log level: %w", err)
5970
}
6071

61-
c, err := config.ReadConfig()
72+
c, err := config.ReadConfigFromFile(configFile)
6273
if err != nil {
6374
return fmt.Errorf("unable to read config: %v", err)
6475
}
6576

66-
if api, ok := c.APIs[fileOrAlias]; ok {
77+
if fileAliasOrCore == "core" {
78+
return handleCoreCommand(additionalArgs, configFile)
79+
}
80+
81+
if api, ok := c.APIs[fileAliasOrCore]; ok {
6782
cd, err := config.ConfigDir()
6883
if err != nil {
69-
fmt.Println(err)
70-
os.Exit(1)
84+
return fmt.Errorf("unable to get config directory: %w", err)
85+
}
86+
if filepath.IsAbs(api.OpenAPIPath) || strings.HasPrefix(api.OpenAPIPath, "http") {
87+
fileAliasOrCore = api.OpenAPIPath
88+
} else {
89+
fileAliasOrCore = filepath.Join(cd, api.OpenAPIPath)
7190
}
72-
fileOrAlias = filepath.Join(cd, api.OpenAPIPath)
7391
if pathPrefix == "" {
7492
pathPrefix = api.PathPrefix
7593
}
76-
rawHeaders = append(rawHeaders, api.Headers...)
94+
headers = append(headers, api.Headers...)
7795
serverURL = api.ServerURL
7896
}
7997

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

89-
headers, err := parseHeaders(rawHeaders)
107+
headersMap, err := parseHeaders(headers)
90108
if err != nil {
91109
return fmt.Errorf("unable to parse headers: %w", err)
92110
}
93111

94-
s = service.NewService(*serviceDefinition, headers)
112+
s = service.NewService(*serviceDefinition, headersMap)
95113

96114
result, err := s.ExecuteCommand(additionalArgs)
97115
if err != nil {
@@ -130,3 +148,111 @@ func setLogLevel(levelAsString string) error {
130148
slog.SetLogLoggerLevel(level)
131149
return nil
132150
}
151+
152+
func handleCoreCommand(additionalArgs []string, configFile string) error {
153+
var openAPIPath string
154+
var overwrite bool
155+
var api config.API
156+
var serverURL string
157+
var headers []string
158+
var pathPrefix string
159+
160+
coreCmd := &cobra.Command{
161+
Use: "core",
162+
Short: "Core API management commands",
163+
}
164+
165+
configCmd := &cobra.Command{
166+
Use: "config",
167+
Short: "Manage core API configurations",
168+
}
169+
170+
addCmd := &cobra.Command{
171+
Use: "add [name]",
172+
Short: "Add a new core API configuration",
173+
Args: cobra.ExactArgs(1),
174+
Run: func(cmd *cobra.Command, args []string) {
175+
api = config.API{
176+
Name: args[0],
177+
OpenAPIPath: openAPIPath,
178+
ServerURL: serverURL,
179+
Headers: headers,
180+
PathPrefix: pathPrefix,
181+
}
182+
if err := config.WriteAPIWithName(configFile, api, overwrite); err != nil {
183+
fmt.Printf("Error writing API config: %v\n", err)
184+
os.Exit(1)
185+
}
186+
fmt.Printf("Core API configuration '%s' added successfully\n", args[0])
187+
},
188+
}
189+
190+
addCmd.Flags().StringVar(&openAPIPath, "openapi-path", "", "Path to OpenAPI specification file")
191+
addCmd.Flags().StringArrayVar(&headers, "header", []string{}, "Headers in format key=value")
192+
addCmd.Flags().StringVar(&serverURL, "server-url", "", "Server URL")
193+
addCmd.Flags().StringVar(&pathPrefix, "path-prefix", "", "Path prefix")
194+
addCmd.Flags().BoolVar(&overwrite, "overwrite", false, "Overwrite existing configuration")
195+
196+
readCmd := &cobra.Command{
197+
Use: "get [name]",
198+
Short: "Get an API configuration",
199+
Args: cobra.ExactArgs(1),
200+
Run: func(cmd *cobra.Command, args []string) {
201+
cfg, err := config.ReadConfigFromFile(configFile)
202+
if err != nil {
203+
fmt.Printf("Error reading config file: %v\n", err)
204+
os.Exit(1)
205+
}
206+
207+
api, exists := cfg.APIs[args[0]]
208+
if !exists {
209+
fmt.Printf("No API configuration found with name '%s'\n", args[0])
210+
os.Exit(1)
211+
}
212+
213+
fmt.Printf("Name: %s\n", api.Name)
214+
fmt.Printf("OpenAPI Path: %s\n", api.OpenAPIPath)
215+
fmt.Printf("Server URL: %s\n", api.ServerURL)
216+
fmt.Printf("Headers: %v\n", api.Headers)
217+
fmt.Printf("Path Prefix: %s\n", api.PathPrefix)
218+
},
219+
}
220+
221+
listCmd := &cobra.Command{
222+
Use: "list",
223+
Short: "List all API configurations",
224+
Args: cobra.NoArgs,
225+
Run: func(cmd *cobra.Command, args []string) {
226+
apis, err := config.ListAPIs(configFile)
227+
if err != nil {
228+
fmt.Printf("Error listing APIs: %v\n", err)
229+
os.Exit(1)
230+
}
231+
232+
if len(apis) == 0 {
233+
fmt.Println("No API configurations found")
234+
return
235+
}
236+
237+
for _, api := range apis {
238+
fmt.Printf("Name: %s\n", api.Name)
239+
fmt.Printf("OpenAPI Path: %s\n", api.OpenAPIPath)
240+
fmt.Printf("Server URL: %s\n", api.ServerURL)
241+
fmt.Printf("Headers: %v\n", api.Headers)
242+
fmt.Printf("Path Prefix: %s\n", api.PathPrefix)
243+
fmt.Println()
244+
}
245+
},
246+
}
247+
248+
configCmd.AddCommand(addCmd)
249+
configCmd.AddCommand(readCmd)
250+
configCmd.AddCommand(listCmd)
251+
coreCmd.AddCommand(configCmd)
252+
253+
coreCmd.SetArgs(additionalArgs)
254+
if err := coreCmd.Execute(); err != nil {
255+
return fmt.Errorf("error executing core command: %v", err)
256+
}
257+
return nil
258+
}

docs/userguide.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,19 @@ headers = ["X-API-TOKEN=123", "X-API-CLIENT=aepcli"]
103103
```
104104

105105
If you would like to use aepcli as your recommend command-line interface for
106-
your API, you can provide a simple script to write the appropriate configuration
107-
to your configuration file.
106+
your API, you can provide a one-liner to add the configuration to your
107+
configuration file:
108+
109+
```bash
110+
aepcli core config add bookstore --openapi-path=$HOME/workspace/aepc/example/bookstore/v1/bookstore_openapi.json
111+
```
112+
113+
You can also list and read all of the configurations you have added:
114+
115+
```bash
116+
aepcli core config list
117+
aepcli core config get bookstore
118+
```
108119

109120
### specifying resource parent ids
110121

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

181+
### core commands
182+
183+
See `aepcli core --help` for commands for aepcli (e.g. config)
184+
170185
## OpenAPI Definitions
171186

172187
### OAS definitions supported

internal/config/config.go

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package config
22

33
import (
4+
"errors"
45
"fmt"
6+
"log/slog"
57
"os"
68
"path/filepath"
79

@@ -20,19 +22,16 @@ type API struct {
2022
PathPrefix string
2123
}
2224

23-
// ReadConfig reads the configuration from the default configuration file.
24-
func ReadConfig() (*Config, error) {
25-
configDir, err := ConfigDir()
26-
if err != nil {
25+
func ReadConfigFromFile(file string) (*Config, error) {
26+
// Check if file exists first
27+
if _, err := os.Stat(file); os.IsNotExist(err) {
28+
slog.Error("Config file does not exist", "file", file)
2729
return nil, err
2830
}
29-
return ReadConfigFromFile(filepath.Join(configDir, "config.toml"))
30-
}
3131

32-
func ReadConfigFromFile(filename string) (*Config, error) {
3332
var c Config
34-
if _, err := toml.DecodeFile(filename, &c); err != nil {
35-
return nil, fmt.Errorf("unable to decode config file at %v: %v", filename, err)
33+
if _, err := toml.DecodeFile(file, &c); err != nil {
34+
return nil, fmt.Errorf("unable to decode config file at %v: %v", file, err)
3635
}
3736
return &c, nil
3837
}
@@ -44,3 +43,72 @@ func ConfigDir() (string, error) {
4443
}
4544
return filepath.Join(homeDir, ".config", "aepcli"), nil
4645
}
46+
47+
// WriteAPIWithName writes a new API configuration to the specified config file.
48+
func WriteAPIWithName(file string, api API, overwrite bool) error {
49+
if api.Name == "" {
50+
return errors.New("api name cannot be empty")
51+
}
52+
53+
// Read existing config
54+
cfg, err := ReadConfigFromFile(file)
55+
if err != nil {
56+
// If file doesn't exist yet, initialize new config
57+
if errors.Is(err, os.ErrNotExist) {
58+
cfg = &Config{
59+
APIs: make(map[string]API),
60+
}
61+
} else {
62+
return fmt.Errorf("failed to read existing config: %w", err)
63+
}
64+
}
65+
66+
// Check if API already exists
67+
if _, exists := cfg.APIs[api.Name]; exists && !overwrite {
68+
return fmt.Errorf("API with name '%s' already exists. Set --overwrite to true to overwrite", api.Name)
69+
}
70+
71+
// Add/update API in config
72+
cfg.APIs[api.Name] = api
73+
74+
// Ensure parent directory exists
75+
parentDir := filepath.Dir(file)
76+
if err := os.MkdirAll(parentDir, 0755); err != nil {
77+
return fmt.Errorf("failed to create parent directory for config file: %w", err)
78+
}
79+
80+
// Open file for writing
81+
f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
82+
if err != nil {
83+
return fmt.Errorf("failed to open config file: %w", err)
84+
}
85+
defer f.Close()
86+
87+
// Encode and write config
88+
if err := toml.NewEncoder(f).Encode(cfg); err != nil {
89+
return fmt.Errorf("failed to write config file: %w", err)
90+
}
91+
return nil
92+
}
93+
94+
func DefaultConfigFile() (string, error) {
95+
dir, err := ConfigDir()
96+
if err != nil {
97+
return "", err
98+
}
99+
return filepath.Join(dir, "config.toml"), nil
100+
}
101+
102+
// ListAPIs returns a slice of all API configurations in the specified config file.
103+
func ListAPIs(file string) ([]API, error) {
104+
cfg, err := ReadConfigFromFile(file)
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to read config file: %w", err)
107+
}
108+
109+
apis := make([]API, 0, len(cfg.APIs))
110+
for _, api := range cfg.APIs {
111+
apis = append(apis, api)
112+
}
113+
return apis, nil
114+
}

0 commit comments

Comments
 (0)