From 42853cc161eafe3c435cbb3762529eaf2e9ccdb2 Mon Sep 17 00:00:00 2001 From: nvima Date: Sat, 22 Apr 2023 22:49:18 +0200 Subject: [PATCH 1/2] Refactor CLI tool: Improved error handling, added status code check This commit includes a major refactor of the CLI tool. It introduces better error handling by utilizing custom TplError structs with error messages and exit codes. This allows for more informative error messages and proper exit codes for various error scenarios. Additionally, an optional `statuscode` attribute has been added to the YAML configuration, allowing users to specify the expected status code for API responses. If provided, the tool will check the API response status code against the expected value before parsing the output, and return an error message if they do not match. --- cmd/testdata/config.yaml | 1 + cmd/tpl.go | 88 ++++++++++++++++++++++++++-------------- util/errors.go | 66 ++++++++++++++++++++++++++++++ util/handleError.go | 10 +++-- util/replaceStdIn.go | 16 ++++---- 5 files changed, 140 insertions(+), 41 deletions(-) create mode 100644 util/errors.go diff --git a/cmd/testdata/config.yaml b/cmd/testdata/config.yaml index c4e25c6..9e73832 100644 --- a/cmd/testdata/config.yaml +++ b/cmd/testdata/config.yaml @@ -9,6 +9,7 @@ gitdiff: - role: "user" content: "Hello" output: "choices[0].message.content" + statuscode: 200 env: - "TEST_API_KEY" - "TEST_SERVER_URL" diff --git a/cmd/tpl.go b/cmd/tpl.go index 5a18a62..778529e 100644 --- a/cmd/tpl.go +++ b/cmd/tpl.go @@ -16,51 +16,61 @@ import ( ) type FunctionConfig struct { - Header []string `mapstructure:"header"` - Data map[string]interface{} `mapstructure:"data"` - Env []string `mapstructure:"env"` - Url string `mapstructure:"url"` - Output string `mapstructure:"output"` + Header []string `mapstructure:"header"` + Data map[string]interface{} `mapstructure:"data"` + Env []string `mapstructure:"env"` + Url string `mapstructure:"url"` + Output string `mapstructure:"output"` + StatusCode int `mapstructure:"statuscode"` } type AppConfig struct { Functions map[string]FunctionConfig } +//TODO Better error handling for testing func tplCommand(cmd *cobra.Command, args []string) { + fc := initFunctionConfig(cmd, args) + output := fc.handleFunc(cmd) + fmt.Fprintf(cmd.OutOrStdout(), output) +} + +func initFunctionConfig(cmd *cobra.Command, args []string) FunctionConfig { + fc := FunctionConfig{} config := viper.AllSettings() + if len(config) == 0 { - panic("No config found") + util.HandleError(cmd, util.NO_FUNC_NAME_ERR.Err(), util.NO_CONFIG_FILE_ERR) } if len(args) == 0 { - panic("No function name provided") + util.HandleError(cmd, util.NO_FUNC_NAME_ERR.Err(), util.NO_FUNC_NAME_ERR) } var appConfig AppConfig err := mapstructure.Decode(config, &appConfig.Functions) if err != nil { - panic("Failed to decode config: " + err.Error()) + util.HandleError(cmd, err, util.INVALID_CONFIG_ERR) } fc, ok := appConfig.Functions[args[0]] if !ok { - panic("No config found for function: " + args[0]) + util.HandleError(cmd, util.NO_FUNC_FOUND_ERR.Err(), util.NO_FUNC_FOUND_ERR) } - - fmt.Fprintf(cmd.OutOrStdout(), fc.handleFunc()) -} -func (fc *FunctionConfig) handleFunc() string { - jsonData := fc.getJSONData() + return fc +} - req, err := http.NewRequest("POST", fc.replaceEnvVariables(fc.Url), bytes.NewBuffer(jsonData)) +func (fc *FunctionConfig) makeHttpCall(jsonData []byte, cmd *cobra.Command) ([]byte, error) { + url := fc.replaceEnvVariables(fc.Url) + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { - panic("Failed to create request: " + err.Error()) + return nil, err } for _, header := range fc.Header { header = fc.replaceEnvVariables(header) + headerParts := strings.SplitN(header, ":", 2) if len(headerParts) == 2 { req.Header.Set(strings.TrimSpace(headerParts[0]), strings.TrimSpace(headerParts[1])) @@ -70,42 +80,59 @@ func (fc *FunctionConfig) handleFunc() string { client := &http.Client{} resp, err := client.Do(req) if err != nil { - panic("Failed to send request: " + err.Error()) + return nil, err } - // Read the response body + body, err := ioutil.ReadAll(resp.Body) if err != nil { - panic("Failed to read response body: " + err.Error()) + return nil, err } defer resp.Body.Close() - // Check if the request was successful - if resp.StatusCode != http.StatusOK { - panic("Request failed, Status: " + resp.Status + ", Body: " + string(body)) + // Check if the request was successful when a status code is provided + if fc.StatusCode != 0 && resp.StatusCode != fc.StatusCode { + err := fmt.Errorf("Request failed with status code %d, Body: %s", resp.StatusCode, string(body)) + util.HandleError(cmd, err, util.INVALID_RESP_CODE) + } + return body, nil +} + +func (fc *FunctionConfig) handleFunc(cmd *cobra.Command) string { + jsonData, err := fc.getJSONData() + if err != nil { + util.HandleError(cmd, err, util.FAILED_TO_GET_DATA) + } + + body, err := fc.makeHttpCall(jsonData, cmd) + if err != nil { + util.HandleError(cmd, err, util.FAILED_TO_MAKE_HTTP_CALL) } - // Parse the JSON response responseData, err := util.ParseJSONResponse(body) if err != nil { - panic(err) + util.HandleError(cmd, err, util.FAILED_TO_PARSE_JSON) } - // Extract the desired output from the JSON response - return util.GetOutputField(responseData, fc.Output) + output, err := util.GetOutputField(responseData, fc.Output) + if err != nil { + util.HandleError(cmd, err, util.FAILED_TO_PARSE_OUTPUT_FIELD) + } + + return output } -func (fc *FunctionConfig) getJSONData() []byte { +func (fc *FunctionConfig) getJSONData() ([]byte, error) { jsonData, err := json.Marshal(fc.Data) if err != nil { - panic(err) + return nil, err } jsonData, err = util.ReplaceStdIn(jsonData) if err != nil { - panic(err) + return nil, err } - return jsonData + return jsonData, nil } func (fc *FunctionConfig) replaceEnvVariables(value string) string { @@ -114,5 +141,6 @@ func (fc *FunctionConfig) replaceEnvVariables(value string) string { placeholder := fmt.Sprintf("${%s}", envVar) value = strings.Replace(value, placeholder, envValue, -1) } + return value } diff --git a/util/errors.go b/util/errors.go new file mode 100644 index 0000000..b5d64f4 --- /dev/null +++ b/util/errors.go @@ -0,0 +1,66 @@ +package util + +import ( + "fmt" +) + +type TplError struct { + msg string + err error + exitCode int +} + +func (te TplError) Err() error { + return te.err +} + +func (te TplError) Msg() string { + return te.msg +} + +func (te TplError) ExitCode() int { + return te.exitCode +} + +var NO_CONFIG_FILE_ERR = TplError{ + msg: "No config file found", + err: fmt.Errorf("No config file found"), + exitCode: 1, +} + +var NO_FUNC_NAME_ERR = TplError{ + msg: "No function name provided", + err: fmt.Errorf("No function name provided"), + exitCode: 2, +} +var NO_FUNC_FOUND_ERR = TplError{ + msg: "Function not found in config", + err: fmt.Errorf("Function not found in config"), + exitCode: 3, +} + +var INVALID_CONFIG_ERR = TplError{ + msg: "Invalid config file", + exitCode: 4, +} + +var INVALID_RESP_CODE = TplError{ + msg: "Invalid response code", + exitCode: 5, +} +var FAILED_TO_GET_DATA = TplError{ + msg: "Failed to get data", + exitCode: 6, +} +var FAILED_TO_MAKE_HTTP_CALL = TplError{ + msg: "Failed to make http call", + exitCode: 7, +} +var FAILED_TO_PARSE_JSON = TplError{ + msg: "Failed to parse JSON", + exitCode: 8, +} +var FAILED_TO_PARSE_OUTPUT_FIELD = TplError{ + msg: "Failed to parse output field", + exitCode: 9, +} diff --git a/util/handleError.go b/util/handleError.go index fd47efd..86ea0ca 100644 --- a/util/handleError.go +++ b/util/handleError.go @@ -3,10 +3,12 @@ package util import ( "fmt" "os" + + "github.com/spf13/cobra" ) -func HandleError(err error, msg string) { - // fmt.Println(err) - fmt.Println(msg) - os.Exit(1) +func HandleError(cmd *cobra.Command, err error, tplError TplError) { + // fmt.Println(err) + fmt.Fprintf(cmd.OutOrStdout(), tplError.msg) + os.Exit(tplError.exitCode) } diff --git a/util/replaceStdIn.go b/util/replaceStdIn.go index c09b55a..0fa731e 100644 --- a/util/replaceStdIn.go +++ b/util/replaceStdIn.go @@ -54,7 +54,7 @@ func ParseJSONResponse(jsonData []byte) (map[string]interface{}, error) { return data, nil } -func GetOutputField(data interface{}, fieldPath string) string { +func GetOutputField(data interface{}, fieldPath string) (string, error) { keys := strings.Split(fieldPath, ".") var result interface{} = data @@ -67,14 +67,14 @@ func GetOutputField(data interface{}, fieldPath string) string { index := key[strings.Index(key, "[")+1 : strings.Index(key, "]")] m, ok := result.(map[string]interface{})[innerKey].([]interface{}) if !ok { - panic("invalid output path") + return "", fmt.Errorf("invalid output path") } intVar, _ := strconv.Atoi(index) result = m[intVar] } else { m, ok := result.(map[string]interface{})[key] if !ok { - panic("invalid output path") + return "", fmt.Errorf("invalid output path") } result = m } @@ -83,12 +83,14 @@ func GetOutputField(data interface{}, fieldPath string) string { if _, ok := result.(map[string]interface{}); ok { jsonResult, err := json.Marshal(result) if err != nil { - panic(err) + return "", err } - return string(jsonResult) + return string(jsonResult), nil } + if _, ok := result.(string); ok { - return result.(string) + return result.(string), nil } - panic("invalid output path") + + return "", fmt.Errorf("invalid output path") } From d248312a7cbf9c230294a4a76c988384a5ba4a73 Mon Sep 17 00:00:00 2001 From: nvima Date: Sat, 22 Apr 2023 22:52:00 +0200 Subject: [PATCH 2/2] refactor: remove duplicate error strings --- util/errors.go | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/util/errors.go b/util/errors.go index b5d64f4..fd44b91 100644 --- a/util/errors.go +++ b/util/errors.go @@ -11,7 +11,7 @@ type TplError struct { } func (te TplError) Err() error { - return te.err + return fmt.Errorf(te.msg) } func (te TplError) Msg() string { @@ -24,43 +24,40 @@ func (te TplError) ExitCode() int { var NO_CONFIG_FILE_ERR = TplError{ msg: "No config file found", - err: fmt.Errorf("No config file found"), exitCode: 1, } var NO_FUNC_NAME_ERR = TplError{ msg: "No function name provided", - err: fmt.Errorf("No function name provided"), exitCode: 2, } var NO_FUNC_FOUND_ERR = TplError{ - msg: "Function not found in config", - err: fmt.Errorf("Function not found in config"), - exitCode: 3, + msg: "Function not found in config", + exitCode: 3, } var INVALID_CONFIG_ERR = TplError{ - msg: "Invalid config file", - exitCode: 4, + msg: "Invalid config file", + exitCode: 4, } var INVALID_RESP_CODE = TplError{ - msg: "Invalid response code", - exitCode: 5, + msg: "Invalid response code", + exitCode: 5, } var FAILED_TO_GET_DATA = TplError{ - msg: "Failed to get data", - exitCode: 6, + msg: "Failed to get data", + exitCode: 6, } var FAILED_TO_MAKE_HTTP_CALL = TplError{ - msg: "Failed to make http call", - exitCode: 7, + msg: "Failed to make http call", + exitCode: 7, } var FAILED_TO_PARSE_JSON = TplError{ - msg: "Failed to parse JSON", - exitCode: 8, + msg: "Failed to parse JSON", + exitCode: 8, } var FAILED_TO_PARSE_OUTPUT_FIELD = TplError{ - msg: "Failed to parse output field", - exitCode: 9, + msg: "Failed to parse output field", + exitCode: 9, }