diff --git a/cmd/lint.go b/cmd/lint.go index 19618a01b..3b44b2b7d 100644 --- a/cmd/lint.go +++ b/cmd/lint.go @@ -111,72 +111,84 @@ func LintCmd() *cobra.Command { // printLintErrors prints the recipe errors func printLintErrors(errs []error, rcp recipe.Recipe) { var ( - notFoundError plugins.NotFoundError - invalidConfigError plugins.InvalidConfigError + notFoundErr plugins.NotFoundError + invalidCfgErr plugins.InvalidConfigError ) for _, err := range errs { - if errors.As(err, ¬FoundError) { - printPluginErrors(rcp, notFoundError) + if errors.As(err, ¬FoundErr) { + printPluginErrors(rcp, notFoundErr) continue } - if errors.As(err, &invalidConfigError) { - printConfigErrors(rcp, invalidConfigError) + if errors.As(err, &invalidCfgErr) { + printConfigErrors(rcp, invalidCfgErr) continue } - fmt.Printf("recipe error: %s\n", err.Error()) + fmt.Printf("%s: recipe error: %s\n", rcp.Name, err.Error()) } } // printPluginErrors print the plugin's type error -func printPluginErrors(rcp recipe.Recipe, notFoundError plugins.NotFoundError) { - if notFoundError.Type == plugins.PluginTypeExtractor { - printPluginError(rcp, rcp.Source, notFoundError) - } else if notFoundError.Type == plugins.PluginTypeProcessor { - plugin, exists := findPluginByName(rcp.Processors, notFoundError.Name) +func printPluginErrors(rcp recipe.Recipe, err plugins.NotFoundError) { + switch err.Type { + case plugins.PluginTypeExtractor: + printPluginError(rcp, rcp.Source, err) + + case plugins.PluginTypeProcessor: + plugin, exists := findPluginByName(rcp.Processors, err.Name) if exists { - printPluginError(rcp, plugin, notFoundError) + printPluginError(rcp, plugin, err) } - } else if notFoundError.Type == plugins.PluginTypeSink { - plugin, exists := findPluginByName(rcp.Sinks, notFoundError.Name) + + case plugins.PluginTypeSink: + plugin, exists := findPluginByName(rcp.Sinks, err.Name) if exists { - printPluginError(rcp, plugin, notFoundError) + printPluginError(rcp, plugin, err) } } } // printPluginError prints the plugin type error -func printPluginError(rcp recipe.Recipe, plugin recipe.PluginRecipe, notFoundError plugins.NotFoundError) { +func printPluginError(rcp recipe.Recipe, plugin recipe.PluginRecipe, err plugins.NotFoundError) { line := plugin.Node.Name.Line - fmt.Printf("%s: invalid %s on line: %d\n", rcp.Name, notFoundError.Type, line) + fmt.Printf("%s: invalid '%s' %s on line: %d\n", rcp.Name, err.Name, err.Type, line) } // printConfigErrors print the plugin's config error -func printConfigErrors(rcp recipe.Recipe, invalidConfigError plugins.InvalidConfigError) { - if invalidConfigError.Type == plugins.PluginTypeExtractor { - printConfigError(rcp, rcp.Source.Node, invalidConfigError) - } else if invalidConfigError.Type == plugins.PluginTypeProcessor { - plugin, exists := findPluginByName(rcp.Processors, invalidConfigError.PluginName) +func printConfigErrors(rcp recipe.Recipe, err plugins.InvalidConfigError) { + switch err.Type { + case plugins.PluginTypeExtractor: + printConfigError(rcp, rcp.Source.Node, err) + + case plugins.PluginTypeProcessor: + plugin, exists := findPluginByName(rcp.Processors, err.PluginName) if exists { - printConfigError(rcp, plugin.Node, invalidConfigError) + printConfigError(rcp, plugin.Node, err) } - } else if invalidConfigError.Type == plugins.PluginTypeSink { - plugin, exists := findPluginByName(rcp.Sinks, invalidConfigError.PluginName) + + case plugins.PluginTypeSink: + plugin, exists := findPluginByName(rcp.Sinks, err.PluginName) if exists { - printConfigError(rcp, plugin.Node, invalidConfigError) + printConfigError(rcp, plugin.Node, err) } } } // printConfigError prints the config error in plugin by searching key -func printConfigError(rcp recipe.Recipe, pluginNode recipe.PluginNode, invalidConfigError plugins.InvalidConfigError) { - for _, configError := range invalidConfigError.Errors { - cfg, ok := pluginNode.Config[configError.Key] +func printConfigError(rcp recipe.Recipe, pluginNode recipe.PluginNode, err plugins.InvalidConfigError) { + for _, cfgErr := range err.Errors { + cfg, ok := pluginNode.Config[cfgErr.Key] if ok { line := cfg.Line - fmt.Printf("%s: invalid %s %s config on line: %d\n", rcp.Name, invalidConfigError.PluginName, invalidConfigError.Type, line) + fmt.Printf( + "%s: invalid %s %s config on line: %d: %s\n", + rcp.Name, err.PluginName, err.Type, line, cfgErr.Message, + ) } else { - fmt.Printf("%s: invalid %s %s config: %s\n", rcp.Name, invalidConfigError.PluginName, invalidConfigError.Type, configError.Message) + fmt.Printf( + "%s: invalid %s %s config: %s\n", + rcp.Name, err.PluginName, err.Type, cfgErr.Message, + ) } } } diff --git a/cmd/run.go b/cmd/run.go index 759d7fdc5..23f40e763 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -111,7 +111,7 @@ func RunCmd() *cobra.Command { lg.Debug("recipe details", "recipe", run.Recipe) var row []string if run.Error != nil { - lg.Error(run.Error.Error(), "recipe") + lg.Error(run.Error.Error(), "recipe", run.Recipe.Name) failures++ row = append(row, cs.FailureIcon(), run.Recipe.Name, cs.Grey(run.Recipe.Source.Name), cs.Greyf("%v ms", strconv.Itoa(run.DurationInMs)), cs.Greyf(strconv.Itoa(run.RecordCount))) } else { diff --git a/plugins/base_plugin_test.go b/plugins/base_plugin_test.go index 7cc7cdd04..9b1c6f6f8 100644 --- a/plugins/base_plugin_test.go +++ b/plugins/base_plugin_test.go @@ -1,3 +1,6 @@ +//go:build plugins +// +build plugins + package plugins_test import ( @@ -49,7 +52,8 @@ func TestBasePluginValidate(t *testing.T) { t.Run("should return InvalidConfigError if config is invalid", func(t *testing.T) { invalidConfig := struct { - FieldA string `validate:"required"` + FieldA string `mapstructure:"field_a" validate:"required"` + FieldB string `mapstructure:"field_b" validate:"url"` }{} basePlugin := plugins.NewBasePlugin(plugins.Info{}, &invalidConfig) @@ -58,7 +62,12 @@ func TestBasePluginValidate(t *testing.T) { RawConfig: map[string]interface{}{}, }) - assert.ErrorAs(t, err, &plugins.InvalidConfigError{}) + assert.Equal(t, err, plugins.InvalidConfigError{ + Errors: []plugins.ConfigError{ + {Key: "field_a", Message: "validation for field 'field_a' failed on the 'required' tag"}, + {Key: "field_b", Message: "validation for field 'field_b' failed on the 'url' tag"}, + }, + }) }) t.Run("should return no error if config is valid", func(t *testing.T) { diff --git a/plugins/errors.go b/plugins/errors.go index 93d9ac260..7aeed75d5 100644 --- a/plugins/errors.go +++ b/plugins/errors.go @@ -3,11 +3,10 @@ package plugins import ( "errors" "fmt" + "strings" ) -var ( - ErrEmptyURNScope = errors.New("urn scope is required to generate unique urn") -) +var ErrEmptyURNScope = errors.New("urn scope is required to generate unique urn") // ConfigError contains fields to check error type ConfigError struct { @@ -23,7 +22,20 @@ type InvalidConfigError struct { } func (err InvalidConfigError) Error() string { - return fmt.Sprintf("invalid %s config", err.Type) + ss := make([]string, 0, len(err.Errors)) + for _, e := range err.Errors { + ss = append(ss, e.Message) + } + + var details string + if len(ss) != 0 { + details = ":\n\t * " + strings.Join(ss, "\n\t * ") + } + + if err.Type == "" { + return "invalid config" + details + } + return fmt.Sprintf("invalid %s config", err.Type) + details } func (err InvalidConfigError) HasError() bool { diff --git a/plugins/errors_test.go b/plugins/errors_test.go new file mode 100644 index 000000000..662016fd0 --- /dev/null +++ b/plugins/errors_test.go @@ -0,0 +1,60 @@ +//go:build plugins +// +build plugins + +package plugins + +import ( + "testing" + + "github.com/MakeNowJust/heredoc" + "github.com/stretchr/testify/assert" +) + +func TestInvalidConfigError(t *testing.T) { + cases := []struct { + name string + err InvalidConfigError + expected string + }{ + { + name: "WithType", + err: InvalidConfigError{ + Type: "extractor", + PluginName: "caramlstore", + Errors: []ConfigError{ + {Key: "engine", Message: "validation for field 'engine' failed on the 'oneof' tag"}, + {Key: "script", Message: "validation for field 'script' failed on the 'required' tag"}, + }, + }, + expected: heredoc.Doc(` + invalid extractor config: + * validation for field 'engine' failed on the 'oneof' tag + * validation for field 'script' failed on the 'required' tag`), + }, + { + name: "WithoutType", + err: InvalidConfigError{ + PluginName: "caramlstore", + Errors: []ConfigError{ + {Key: "engine", Message: "validation for field 'engine' failed on the 'oneof' tag"}, + }, + }, + expected: heredoc.Doc(` + invalid config: + * validation for field 'engine' failed on the 'oneof' tag`), + }, + { + name: "WithoutErrors", + err: InvalidConfigError{ + Type: "extractor", + PluginName: "caramlstore", + }, + expected: "invalid extractor config", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.err.Error()) + }) + } +} diff --git a/plugins/util.go b/plugins/util.go index eee9856b7..a706c18d1 100644 --- a/plugins/util.go +++ b/plugins/util.go @@ -53,10 +53,9 @@ func buildConfig(configMap map[string]interface{}, c interface{}) (err error) { if errors.As(err, &validationErr) { var configErrors []ConfigError for _, fieldErr := range validationErr { - key := strings.TrimPrefix(fieldErr.Namespace(), "Config.") configErrors = append(configErrors, ConfigError{ - Key: key, - Message: fieldErr.Error(), + Key: fieldErr.Field(), + Message: fmt.Sprintf("validation for field '%s' failed on the '%s' tag", fieldErr.Field(), fieldErr.Tag()), }) } return InvalidConfigError{