]> Cypherpunks repositories - keks.git/commitdiff
Clearer validation messages
authorSergey Matveev <stargrave@stargrave.org>
Wed, 9 Apr 2025 08:15:00 +0000 (11:15 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Wed, 9 Apr 2025 08:44:37 +0000 (11:44 +0300)
go/cmd/schema-validate/main.go
go/schema/check.go

index 1b25b8c6bfdb2e370ab1fc942981b2c1a6b23046809e0848dde5fb5395dd7a18..b5927eeca02872c68e52aa8531d25c35ea8a6a23178066a1fde0026cace35810 100644 (file)
@@ -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)
        }
 }
index 3e12dd57d2d09e823aa34952af40453f36481fd5eeaa9b1dc502dbea5886f592..d2c148acd2c657e607d6c9ce49a0449cff78136a33da3465c6d17e256cea199c 100644 (file)
@@ -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