diff --git a/cmd/validate.go b/cmd/validate.go index 6b91984..9c440de 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -28,6 +28,7 @@ func (vc *validateCommand) Help() string { } func (vc *validateCommand) Run(args []string) int { + exitCode := 0 basePath := getWd() vc.parseFlags() @@ -42,13 +43,15 @@ func (vc *validateCommand) Run(args []string) int { if err := schema.ValidateInterface(conf); err != nil { vc.logValidationError(err, conf) - log.Fatal(err) - } else { - fmt.Println("ok") + exitCode = 1 } } - return 0 + if exitCode == 0 { + fmt.Println("ok") + } + + return exitCode } func (vc *validateCommand) Synopsis() string { @@ -100,15 +103,22 @@ func (vc *validateCommand) loadSchema(basePath string) *jsonschema.Schema { func (vc *validateCommand) logValidationError(err error, conf any) { ptrPaths := santhosh.GetPtrPaths(err.(*jsonschema.ValidationError)) for _, path := range ptrPaths { - value, err := json.Marshal(santhosh.GetValueAtPath(conf, path)) + value, err := santhosh.GetValueAtPath(conf, path) + if err != nil { + log.Fatal(err) + } + + mvalue, err := json.Marshal(value) if err != nil { log.Fatal(err) } - log.Printf( + fmt.Printf( "path '%s' contains an invalid configuration value: %+v\n", santhosh.JoinPtrPath(path), - string(value), + string(mvalue), ) } + + fmt.Println(err) } diff --git a/internal/arch/file/rule_test.go b/internal/arch/file/rule_test.go index 23c210e..71f48d3 100644 --- a/internal/arch/file/rule_test.go +++ b/internal/arch/file/rule_test.go @@ -56,11 +56,11 @@ func Test_It_Checks_All_Conditions(t *testing.T) { Because("I want to test all expressions together") if !cmp.Equal(vs, tC.wantViolations, cmp.AllowUnexported(rule.Violation{}), cmpopts.EquateEmpty()) { - t.Errorf("Expected %v, got %v", tC.wantViolations, vs) + t.Errorf("expected %v, got %v", tC.wantViolations, vs) } if errs != nil { - t.Errorf("Expected errs to be nil, got: %+v", errs) + t.Errorf("expected errs to be nil, got: %+v", errs) } }) } @@ -73,6 +73,6 @@ func Test_It_Adds_ErrRuleBuilderLocked_Only_Once(t *testing.T) { rb.AddError(file.ErrRuleBuilderLocked) if errs := rb.GetErrors(); len(errs) != 1 { - t.Errorf("Expected 1 error, got %d", len(errs)) + t.Errorf("expected 1 error, got %d", len(errs)) } } diff --git a/internal/schema/santhosh/errors.go b/internal/schema/santhosh/errors.go index f12ba5b..6f16e26 100644 --- a/internal/schema/santhosh/errors.go +++ b/internal/schema/santhosh/errors.go @@ -1,6 +1,7 @@ package santhosh import ( + "errors" "strconv" "strings" @@ -8,6 +9,8 @@ import ( "golang.org/x/exp/slices" ) +var ErrObjTypeAssertion = errors.New("obj type assertion failed") + func JoinPtrPath(path []any) string { strpath := "#" @@ -23,17 +26,27 @@ func JoinPtrPath(path []any) string { return strpath } -func GetValueAtPath(obj any, path []any) any { +func GetValueAtPath(obj any, path []any) (any, error) { for _, key := range path { switch v := key.(type) { case int: - obj = obj.([]any)[v] + tobj, ok := obj.([]any) + if !ok { + return nil, ErrObjTypeAssertion + } + + obj = tobj[v] case string: - obj = obj.(map[string]any)[v] + tobj, ok := obj.(map[string]any) + if !ok { + return nil, ErrObjTypeAssertion + } + + obj = tobj[v] } } - return obj + return obj, nil } func GetPtrPaths(err error) [][]any { @@ -45,13 +58,13 @@ func GetPtrPaths(err error) [][]any { } func extractPtrs(err *jsonschema.ValidationError) []string { - var ptrs []string + ptrs := []string{err.InstancePtr} for _, cause := range err.Causes { if len(cause.Causes) > 0 { ptrs = append(ptrs, extractPtrs(cause)...) } else { - ptrs = append(ptrs, err.InstancePtr) + ptrs = append(ptrs, cause.InstancePtr) } } diff --git a/internal/schema/santhosh/errors_test.go b/internal/schema/santhosh/errors_test.go new file mode 100644 index 0000000..14e1dd8 --- /dev/null +++ b/internal/schema/santhosh/errors_test.go @@ -0,0 +1,158 @@ +package santhosh_test + +import ( + "goarkitect/internal/schema/santhosh" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/santhosh-tekuri/jsonschema" +) + +func Test_JoinPtrPath(t *testing.T) { + testCases := []struct { + desc string + path []any + want string + }{ + { + desc: "empty path", + path: []any{}, + want: "#", + }, + { + desc: "strings-only path", + path: []any{"foo", "bar"}, + want: "#/foo/bar", + }, + { + desc: "ints-only path", + path: []any{0, 1, 2, 3}, + want: "#/0/1/2/3", + }, + { + desc: "strings and ints path", + path: []any{"foo", 3, "bar", 5}, + want: "#/foo/3/bar/5", + }, + { + desc: "no-strings and no-ints path", + path: []any{0.0, 'a', true}, + want: "#", + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + got := santhosh.JoinPtrPath(tC.path) + if got != tC.want { + t.Errorf("got %v, want %v", got, tC.want) + } + }) + } +} + +func Test_GetValueAtPath(t *testing.T) { + testCases := []struct { + desc string + obj any + path []any + want any + wantErr error + }{ + { + desc: "1-level obj", + obj: map[string]any{"foo": "bar"}, + path: []any{"foo"}, + want: "bar", + }, + { + desc: "2-levels obj", + obj: map[string]any{ + "foo": map[string]any{ + "bar": 123, + }, + }, + path: []any{"foo", "bar"}, + want: 123, + }, + { + desc: "2-levels obj, mixed data types", + obj: []any{ + map[string]any{"foo": []any{"foo", 123}}, + }, + path: []any{0, "foo", 1}, + want: 123, + }, + { + desc: "2-levels obj, mixed data types", + obj: map[string]any{ + "foo": []any{ + map[string]any{"bar": 123}, + }, + }, + path: []any{"foo", 0, "bar"}, + want: 123, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + got, err := santhosh.GetValueAtPath(tC.obj, tC.path) + if got != tC.want { + t.Errorf("got %v, want %v", got, tC.want) + } + if err != tC.wantErr { + t.Errorf("got error %v, want %v", err, tC.wantErr) + } + }) + } +} + +func Test_GetPtrPaths(t *testing.T) { + testCases := []struct { + desc string + err *jsonschema.ValidationError + want [][]any + }{ + { + desc: "get the value pointed by instance ptr", + err: &jsonschema.ValidationError{ + Message: "missing properties: \"filePath\"", + InstancePtr: "#/rules/0/matcher", + SchemaURL: "/home/user/api/config_schema.json", + SchemaPtr: "#/definitions/fileMatcherOne/required", + }, + want: [][]any{ + {"rules", 0, "matcher"}, + }, + }, + { + desc: "get the value pointed by instance ptr and its cause", + err: &jsonschema.ValidationError{ + Message: "missing properties: \"filePath\"", + InstancePtr: "#/rules/0/matcher", + SchemaURL: "/home/user/api/config_schema.json", + SchemaPtr: "#/definitions/fileMatcherOne/required", + Causes: []*jsonschema.ValidationError{ + { + Message: "wrong property: \"filePaths\"", + InstancePtr: "#/rules/0/matcher/filePaths", + SchemaURL: "/home/user/api/config_schema.json", + SchemaPtr: "#/definitions/fileMatcherOne/required", + }, + }, + }, + want: [][]any{ + {"rules", 0, "matcher"}, + {"rules", 0, "matcher", "filePaths"}, + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + got := santhosh.GetPtrPaths(tC.err) + if !cmp.Equal(got, tC.want, cmpopts.EquateEmpty()) { + t.Errorf("expected %v, got %v", tC.want, got) + } + }) + } +}