From 8c5191350c6f43361b14150a55d31191607e9a5a66f5ced34ac5665c94f28540 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 9 Apr 2025 11:15:00 +0300 Subject: [PATCH] Clearer validation messages --- go/cmd/schema-validate/main.go | 13 +- go/schema/check.go | 396 ++++++++++++++++++++++++++------- 2 files changed, 324 insertions(+), 85 deletions(-) diff --git a/go/cmd/schema-validate/main.go b/go/cmd/schema-validate/main.go index 1b25b8c..b5927ee 100644 --- a/go/cmd/schema-validate/main.go +++ b/go/cmd/schema-validate/main.go @@ -16,6 +16,7 @@ package main import ( + "errors" "flag" "fmt" "log" @@ -27,7 +28,7 @@ import ( func main() { flag.Parse() - log.SetFlags(log.Lshortfile) + log.SetFlags(0) if flag.NArg() != 3 { fmt.Fprintf(os.Stderr, "Usage: schema-validate SCHEMA.keks SCHEMA-NAME DATA.keks\n") os.Exit(1) @@ -61,7 +62,15 @@ func main() { } err = schema.Check(flag.Arg(1), schemas, data) + var bad bool +CheckErr: if err != nil { - log.Fatal(err) + bad = true + log.Print(err) + err = errors.Unwrap(err) + goto CheckErr + } + if bad { + os.Exit(1) } } diff --git a/go/schema/check.go b/go/schema/check.go index 3e12dd5..d2c148a 100644 --- a/go/schema/check.go +++ b/go/schema/check.go @@ -17,9 +17,10 @@ package schema import ( "bytes" - "errors" "fmt" "slices" + "strconv" + "strings" "time" "go.cypherpunks.su/tai64n/v4" @@ -41,41 +42,101 @@ const ( CmdTimePrec = "TP" CmdType = "T" CmdUTC = "UTC" - - Magic = "schema" + Magic = "schema" ) +type BaseErr struct { + Data any + w error + SchemaName string + CmdName string + Taken string + Msg string + CmdIdx int +} + +type SchemaErr struct{ BaseErr } + +type DataErr struct{ BaseErr } + +func (err *BaseErr) Error() string { + cols := []string{ + fmt.Sprintf("schema:%s", err.SchemaName), + fmt.Sprintf("cmd:%d:%s", err.CmdIdx, err.CmdName), + } + if err.Taken != "" { + cols = append(cols, fmt.Sprintf("taken:%s", err.Taken)) + } + if err.Data != nil { + cols = append(cols, fmt.Sprintf("data:%T:%+v", err.Data, err.Data)) + } + cols = append(cols, err.Msg) + return strings.Join(cols, " ") +} + +func (err *BaseErr) Unwrap() error { + return err.w +} + func Check(schemaName string, schemas map[string][][]any, data any) error { - acts := schemas[schemaName] - if acts == nil { - return errors.New(schemaName + ": no schema") + cmds := schemas[schemaName] + if cmds == nil { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, Msg: "no such schema", + }} } + var taken string var single bool // TAKEn, not EACH var vs []any - for i, act := range acts { - if len(act) == 0 { - return fmt.Errorf("%s: %d: empty command", schemaName, i) + for cmdIdx, cmd := range cmds { + if len(cmd) == 0 { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + Msg: "empty command", + }} } - cmd, ok := act[0].(string) + cmdName, ok := cmd[0].(string) if !ok { - return fmt.Errorf("%s: %d: non command: %+v", schemaName, i, act[0]) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + Msg: "non str command", + }} } - switch cmd { + switch cmdName { case CmdExists: if vs == nil { - return fmt.Errorf("%s: %d: %s", schemaName, i, cmd) + return &DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + }} } case CmdNotExists: if vs != nil { - return fmt.Errorf("%s: %d: %s", schemaName, i, cmd) + return &DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + }} } case CmdTake: + taken = "" single = true - if len(act) != 2 { - return fmt.Errorf("%s: %d: %s: wrong number of args", schemaName, i, cmd) + if len(cmd) != 2 { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "wrong number of args", + }} } - switch k := act[1].(type) { + switch k := cmd[1].(type) { case string: + taken = k if k == "." { vs = []any{data} } else { @@ -88,10 +149,18 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { vs = []any{v} } case uint64: + taken = strconv.FormatUint(k, 10) l := data.([]any) - vs = []any{l[k]} + if int(k) < len(l) { + vs = []any{l[k]} + } default: - return fmt.Errorf("%s: %d: %s: bad target", schemaName, i, cmd) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "bad target", + }} } case CmdEach: single = false @@ -108,18 +177,36 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { case []any: vs = v default: - return fmt.Errorf("%s: %d: %s: non-iterable", schemaName, i, cmd) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Msg: "non iterable", + }} } case CmdEq: if vs == nil { continue } - if len(act) != 2 { - return fmt.Errorf("%s: %d: %s: wrong number of args", schemaName, i, cmd) + if len(cmd) != 2 { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "wrong number of args", + }} } - expect, ok := act[1].([]byte) + var expect []byte + expect, ok = cmd[1].([]byte) if !ok { - return fmt.Errorf("%s: %d: %s: bad bin: %+v", schemaName, i, cmd, act[1]) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "bad bin", + Data: cmd[1], + }} } for _, v := range vs { var eq bool @@ -142,25 +229,50 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { eq = bytes.Equal(got, expect) default: if !ok { - return fmt.Errorf("%s: %d: %s: non-comparable: %T", schemaName, i, cmd, v) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Msg: "non comparable", + Data: v, + }} } } if !eq { - return fmt.Errorf("%s: %d: %s: !=", schemaName, i, cmd) + return &DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Data: v, + }} } } case CmdType: if vs == nil { continue } - expected := make([]types.Type, 0, len(act)-1) - if len(act) < 2 { - return fmt.Errorf("%s: %d: %s: wrong number of args", schemaName, i, cmd) + expected := make([]types.Type, 0, len(cmd)-1) + if len(cmd) < 2 { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "wrong number of args", + }} } - for _, tAny := range act[1:] { - t, ok := tAny.(string) + for _, tAny := range cmd[1:] { + var t string + t, ok = tAny.(string) if !ok { - return fmt.Errorf("%s: %d: %s: non-str: %+v", schemaName, i, cmd, tAny) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "non str", + Data: tAny, + }} } switch t { case "NIL": @@ -186,11 +298,17 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { case "STR": expected = append(expected, types.Str) default: - return fmt.Errorf("%s: %d: %s: unknown type: %s", schemaName, i, cmd, t) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "unknown type", + Data: t, + }} } } var typ types.Type - for n, v := range vs { + for _, v := range vs { switch v.(type) { case nil: typ = types.NIL @@ -225,27 +343,51 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { case keks.Raw: typ = types.Raw default: - return fmt.Errorf("%s: %d: %s: unsupported type: %T", schemaName, i, cmd, v) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Msg: "unsupported type", + Data: v, + }} } if !slices.Contains(expected, typ) { - return fmt.Errorf("%s: %d: %d: %s: %T", schemaName, i, n, cmd, v) + return &DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Data: v, + }} } } case CmdGT, CmdLT: if vs == nil { continue } - if len(act) != 2 { - return fmt.Errorf("%s: %d: %s: wrong number of args", schemaName, i, cmd) + if len(cmd) != 2 { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "wrong number of args", + }} } var expect int64 - switch v := act[1].(type) { + switch v := cmd[1].(type) { case uint64: expect = int64(v) case int64: expect = v default: - return fmt.Errorf("%s: %d: %s: unsupported type: %T", schemaName, i, cmd, v) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "unsupported type", + Data: v, + }} } for _, v := range vs { var got int64 @@ -264,107 +406,171 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { case int64: got = v default: - return fmt.Errorf("%s: %d: %s: non len-able: %T", schemaName, i, cmd, v) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Msg: "non len-able", + Data: v, + }} } } - switch cmd { + ok = false + switch cmdName { case CmdGT: - if got <= expect { - return fmt.Errorf("%s: %d: %d <= %d", schemaName, i, got, expect) + if got > expect { + ok = true } case CmdLT: - if got >= expect { - return fmt.Errorf("%s: %d: %d >= %d", schemaName, i, got, expect) + if got < expect { + ok = true } } + if !ok { + return &DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Data: got, + }} + } } case CmdSchema: if vs == nil { continue } - if len(act) != 2 { - return fmt.Errorf("%s: %d: %s: wrong number of args", schemaName, i, cmd) + if len(cmd) != 2 { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "wrong number of args", + }} } - name, ok := act[1].(string) + name, ok := cmd[1].(string) if !ok { - return fmt.Errorf("%s: %d: %s: bad name: %+v", schemaName, i, cmd, act[1]) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "non str", + Data: name, + }} } - for n, v := range vs { + for _, v := range vs { if err := Check(name, schemas, v); err != nil { - return fmt.Errorf("%s: %d: %d: %s: %w", schemaName, i, n, cmd, err) + return &DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Data: v, + w: err, + }} } } case CmdTimePrec: if vs == nil { continue } - if len(act) != 2 { - return fmt.Errorf("%s: %d: %s: wrong number of args", schemaName, i, cmd) + if len(cmd) != 2 { + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "wrong number of args", + }} } - prec, ok := act[1].(uint64) + prec, ok := cmd[1].(uint64) if !ok { - return fmt.Errorf("%s: %d: %s: bad prec: %+v", schemaName, i, cmd, act[1]) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "bad precision value", + Data: cmd[1], + }} } for _, v := range vs { - switch v := v.(type) { + switch v.(type) { case *tai64n.TAI64: case *tai64n.TAI64N: case *tai64n.TAI64NA: case time.Time: default: - return fmt.Errorf("%s: %d: %s: unsupported data type: %T", - schemaName, i, cmd, v) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Msg: "unsupported data type", + Data: v, + }} } + err := DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Data: v, + }} switch prec { - case 0: // s + case 0: + err.Msg = "s" switch v := v.(type) { case *tai64n.TAI64: case time.Time: if v.Nanosecond() != 0 { - return fmt.Errorf("%s: %d: %s: >s", schemaName, i, cmd) + return &err } default: - return fmt.Errorf("%s: %d: %s: >s", schemaName, i, cmd) + return &err } - case 3: // ms + case 3: + err.Msg = "ms" switch v := v.(type) { case *tai64n.TAI64: case *tai64n.TAI64N: d := be.Get(v[8:]) if d%1000000 != 0 { - return fmt.Errorf("%s: %d: %s: >ms", schemaName, i, cmd) + return &err } case time.Time: if v.Nanosecond()%1000000 != 0 { - return fmt.Errorf("%s: %d: %s: >ms", schemaName, i, cmd) + return &err } default: - return fmt.Errorf("%s: %d: %s: >ms", schemaName, i, cmd) + return &err } - case 6: // µs + case 6: + err.Msg = "µs" switch v := v.(type) { case *tai64n.TAI64: case *tai64n.TAI64N: d := be.Get(v[8:]) if d%1000 != 0 { - return fmt.Errorf("%s: %d: %s: >µs", schemaName, i, cmd) + return &err } case time.Time: if v.Nanosecond()%1000 != 0 { - return fmt.Errorf("%s: %d: %s: >µs", schemaName, i, cmd) + return &err } default: - return fmt.Errorf("%s: %d: %s: >µs", schemaName, i, cmd) + return &err } - case 9: // ns + case 9: + err.Msg = "ns" switch v.(type) { case *tai64n.TAI64: case *tai64n.TAI64N: case time.Time: default: - return fmt.Errorf("%s: %d: %s: >ns", schemaName, i, cmd) + return &err } - case 12: // ps + case 12: + err.Msg = "ps" switch v := v.(type) { case time.Time: case *tai64n.TAI64: @@ -372,10 +578,11 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { case *tai64n.TAI64NA: d := be.Get(v[12:]) if d%1000000 != 0 { - return fmt.Errorf("%s: %d: %s: >ps", schemaName, i, cmd) + return &err } } - case 15: // fs + case 15: + err.Msg = "fs" switch v := v.(type) { case time.Time: case *tai64n.TAI64: @@ -383,13 +590,19 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { case *tai64n.TAI64NA: d := be.Get(v[12:]) if d%1000 != 0 { - return fmt.Errorf("%s: %d: %s: >fs", schemaName, i, cmd) + return &err } } - case 18: // as + case 18: + err.Msg = "as" default: - return fmt.Errorf("%s: %d: %s: unknown value: %d", - schemaName, i, cmd, prec) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "unknown value", + Data: v, + }} } } case CmdUTC: @@ -410,15 +623,32 @@ func Check(schemaName string, schemas map[string][][]any, data any) error { case time.Time: continue default: - return fmt.Errorf("%s: %d: %s: unsupported data type: %T", - schemaName, i, cmd, v) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Msg: "unsupported data type", + Data: v, + }} } if isLeap { - return fmt.Errorf("%s: %d: %s: is leap", schemaName, i, cmd) + return &DataErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Taken: taken, + Data: v, + }} } } default: - return fmt.Errorf("%s: %d: %s: unknown command", schemaName, i, cmd) + return &SchemaErr{BaseErr: BaseErr{ + SchemaName: schemaName, + CmdIdx: cmdIdx, + CmdName: cmdName, + Msg: "unknown command", + }} } } return nil -- 2.48.1