diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..4f2163c --- /dev/null +++ b/cmd.go @@ -0,0 +1 @@ +package cmd // import "hookt.dev/cmd" diff --git a/pkg/proto/pattern.go b/pkg/proto/pattern.go index d925548..5080711 100644 --- a/pkg/proto/pattern.go +++ b/pkg/proto/pattern.go @@ -2,10 +2,13 @@ package proto import ( "context" + "fmt" "log/slog" + "text/template" "hookt.dev/cmd/pkg/errors" "hookt.dev/cmd/pkg/proto/wire" + "hookt.dev/cmd/pkg/trace" "github.com/google/go-cmp/cmp" "github.com/itchyny/gojq" @@ -41,34 +44,62 @@ func (p Pattern) Match(ctx context.Context, obj any) (bool, error) { } func (p *P) Pattern(ctx context.Context, obj wire.Object) (Pattern, error) { - pt := make(Pattern) + var ( + pt = make(Pattern) + tr = trace.ContextPattern(ctx) + err error + ) for k, raw := range obj { - q, err := gojq.Parse(k) - if err != nil { - return nil, errors.New("failed to parse jq %q: %w", k, err) + q, e := gojq.Parse(k) + tr.ParseKey(k, q, e) + if e != nil { + err = errors.Join( + err, + errors.New("failed to parse jq %q: %w", k, e), + ) + continue } - var v any + var want any - if err := yaml.Unmarshal(raw, &v); err != nil { - return nil, errors.New("failed to parse value for jq %q: %w", k, err) + e = yaml.Unmarshal(raw, &want) + tr.UnmarshalValue(k, raw, want, e) + if e != nil { + err = errors.Join( + err, + errors.New("failed to parse value for jq %q: %w", k, e), + ) + continue } slog.Debug("building pattern", "key", k, - "pattern", v, + "pattern", want, ) - switch v := v.(type) { + switch want := want.(type) { case bool: - pt[q] = func(_ context.Context, x any) (bool, error) { return v || x == nil, nil } + pt[q] = func(_ context.Context, got any) (bool, error) { return want || got == nil, nil } case string: - pt[q] = p.t.Match(ctx, v) + tv := tr.TemplateValue + tr.TemplateValue = func(_, v string, t *template.Template, e error) { tv(k, v, t, e) } + pt[q] = p.t.Match(trace.WithPattern(ctx, tr), want) default: - pt[q] = func(_ context.Context, x any) (bool, error) { return cmp.Equal(x, v), nil } + pt[q] = func(_ context.Context, got any) (bool, error) { + ok := cmpEqual(want, got) + tr.EqualMatch(k, want, got, ok) + return ok, nil + } } } - return pt, nil + return pt, err +} + +func cmpEqual(want, got any) bool { + if fmt.Sprint(want) == fmt.Sprint(got) { + return true + } + return cmp.Equal(want, got) } diff --git a/pkg/proto/pattern_test.go b/pkg/proto/pattern_test.go index e62c7ea..82f59e1 100644 --- a/pkg/proto/pattern_test.go +++ b/pkg/proto/pattern_test.go @@ -2,9 +2,11 @@ package proto_test import ( "context" + "strconv" "testing" "hookt.dev/cmd/pkg/proto/wire" + "hookt.dev/cmd/pkg/trace" ) func TestPattern(t *testing.T) { @@ -74,7 +76,7 @@ func TestPattern(t *testing.T) { }, 5: { wire.Object{ - ".foo.bar": []byte(`"${{ setvar "bar" . }}"`), + ".foo.bar": []byte(strconv.Quote(`${{ setvar "bar" . }}`)), }, map[string]any{ "foo": map[string]any{ @@ -85,7 +87,7 @@ func TestPattern(t *testing.T) { }, 6: { wire.Object{ - ".foo.bar": []byte(`"${{ var "bar" }}"`), + ".foo.bar": []byte(strconv.Quote(`${{ var "bar" }}`)), }, map[string]any{ "foo": map[string]any{ @@ -136,6 +138,7 @@ func TestPattern(t *testing.T) { p := newP() ctx := context.Background() + ctx = trace.WithPattern(ctx, trace.LogPattern()) for _, cas := range cases { t.Run("", func(t *testing.T) { @@ -146,7 +149,7 @@ func TestPattern(t *testing.T) { ok, err := pt.Match(ctx, cas.obj) if err != nil { - t.Fatal(err) + t.Fatalf("match: Match()=%+v", err) } if ok != cas.ok { diff --git a/pkg/proto/proto.go b/pkg/proto/proto.go index b05d1ea..54cd65b 100644 --- a/pkg/proto/proto.go +++ b/pkg/proto/proto.go @@ -61,7 +61,7 @@ func (p *P) Parse(ctx context.Context, q []byte) (*Workflow, error) { var ( w Workflow - tr = trace.ContextJobTrace(ctx) + tr = trace.ContextJob(ctx) ) w.Jobs = make([]Job, len(raw.Jobs)) diff --git a/pkg/proto/proto_test.go b/pkg/proto/proto_test.go index 3c24a0d..218ae87 100644 --- a/pkg/proto/proto_test.go +++ b/pkg/proto/proto_test.go @@ -3,14 +3,29 @@ package proto_test import ( "context" "io/ioutil" + "log/slog" + "os" "testing" + "time" "hookt.dev/cmd/pkg/plugin/builtin" "hookt.dev/cmd/pkg/proto" - "github.com/davecgh/go-spew/spew" + "github.com/lmittmann/tint" ) +func init() { + slog.SetDefault( + slog.New( + tint.NewHandler(os.Stderr, &tint.Options{ + AddSource: true, + Level: slog.LevelDebug, + TimeFormat: time.Kitchen, + }), + ), + ) +} + func newP() *proto.P { p := builtin.Plugins() q := make([]proto.Interface, len(p)) @@ -32,7 +47,9 @@ func TestParse(t *testing.T) { t.Fatal(err) } - spew.Dump(w) + _ = w + + // spew.Dump(w) } func file(t *testing.T, path string) []byte { diff --git a/pkg/proto/template.go b/pkg/proto/template.go index d49bddc..02b727e 100644 --- a/pkg/proto/template.go +++ b/pkg/proto/template.go @@ -10,9 +10,9 @@ import ( "hookt.dev/cmd/pkg/async" "hookt.dev/cmd/pkg/errors" + "hookt.dev/cmd/pkg/trace" "github.com/Masterminds/sprig" - "github.com/google/go-cmp/cmp" "sigs.k8s.io/yaml" ) @@ -64,28 +64,39 @@ func (t *T) funcs() template.FuncMap { } func (t *T) Match(ctx context.Context, data string) func(context.Context, any) (bool, error) { + tr := trace.ContextPattern(ctx) tmpl, err := t.Parse("", data) + tr.TemplateValue("", data, tmpl, err) if err != nil { return func(context.Context, any) (bool, error) { return false, err } } - return func(_ context.Context, x any) (bool, error) { - var buf bytes.Buffer - - if err := tmpl.Execute(&buf, x); err != nil { + return func(ctx context.Context, got any) (bool, error) { + var ( + buf bytes.Buffer + tr = trace.ContextPattern(ctx) + ) + + err := tmpl.Execute(&buf, got) + tr.ExecuteMatch("", buf.Bytes(), err) + if err != nil { return false, errors.New("failed to evaluate %q: %w", data, err) } - var v any + var want any - if err := yaml.Unmarshal(buf.Bytes(), &v); err != nil { + err = yaml.Unmarshal(buf.Bytes(), &want) + tr.UnmarshalMatch("", buf.Bytes(), want, err) + if err != nil { return false, errors.New("failed to parse result: %w", err) } - switch v := v.(type) { + switch want := want.(type) { case bool: - return v, nil + return want, nil default: - return cmp.Equal(x, v), nil + ok := cmpEqual(want, got) + tr.EqualMatch("", want, got, ok) + return ok, nil } } } diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go index 8653849..f900eea 100644 --- a/pkg/trace/trace.go +++ b/pkg/trace/trace.go @@ -2,28 +2,158 @@ package trace import ( "context" + "fmt" + "log/slog" + "text/template" "hookt.dev/cmd/pkg/proto/wire" + + "github.com/itchyny/gojq" + "github.com/lmittmann/tint" +) + +var ( + nopJob = JobTrace{ + WireJob: func(int, *wire.Job) {}, + WirePlugin: func(int, *wire.Plugin, any) {}, + WireStep: func(int, *wire.Step, any) {}, + RunStep: func() {}, + MatchStep: func() {}, + TapMessage: func() {}, + } + nopPattern = PatternTrace{ + ParseKey: func(string, *gojq.Query, error) {}, + UnmarshalValue: func(string, []byte, any, error) {}, + TemplateValue: func(string, string, *template.Template, error) {}, + ExecuteMatch: func(string, []byte, error) {}, + UnmarshalMatch: func(string, []byte, any, error) {}, + EqualMatch: func(string, any, any, bool) {}, + } ) -var nop = &JobTrace{ - WireJob: func(int, *wire.Job) {}, - WirePlugin: func(int, *wire.Plugin, any) {}, - WireStep: func(int, *wire.Step, any) {}, - RunStep: func() {}, - MatchStep: func() {}, - TapMessage: func() {}, +func LogJob() JobTrace { + return JobTrace{ + WireJob: func(int, *wire.Job) {}, + WirePlugin: func(int, *wire.Plugin, any) {}, + WireStep: func(int, *wire.Step, any) {}, + RunStep: func() {}, + MatchStep: func() {}, + TapMessage: func() {}, + } +} + +func LogPattern() PatternTrace { + return PatternTrace{ + ParseKey: func(key string, q *gojq.Query, err error) { + if err != nil { + slog.Error("trace: ParseKey", + "key", key, + tint.Err(err), + ) + } else { + slog.Info("trace: ParseKey", + "key", key, + ) + } + }, + UnmarshalValue: func(key string, p []byte, v any, err error) { + if err != nil { + slog.Error("trace: UnmarshalValue", + "key", key, + "raw", string(p), + "value", v, + tint.Err(err), + ) + } else { + slog.Info("trace: UnmarshalValue", + "key", key, + "raw", string(p), + "value", v, + ) + } + }, + TemplateValue: func(key string, value string, t *template.Template, err error) { + if err != nil { + slog.Error("trace: TemplateValue", + "key", key, + "value", value, + tint.Err(err), + ) + } else { + slog.Info("trace: TemplateValue", + "key", key, + "value", value, + ) + } + }, + ExecuteMatch: func(key string, p []byte, err error) { + if err != nil { + slog.Error("trace: ExecuteMatch", + "key", key, + "raw", string(p), + tint.Err(err), + ) + } else { + slog.Info("trace: ExecuteMatch", + "key", key, + "raw", string(p), + ) + } + }, + UnmarshalMatch: func(key string, p []byte, v any, err error) { + if err != nil { + slog.Error("trace: UnmarshalMatch", + "key", key, + "raw", string(p), + "value", v, + tint.Err(err), + ) + } else { + slog.Info("trace: UnmarshalMatch", + "key", key, + "raw", string(p), + "value", v, + ) + } + }, + EqualMatch: func(key string, want any, got any, ok bool) { + if !ok { + slog.Error("trace: EqualMatch", + "key", key, + "want", fmt.Sprintf("%+[1]v (%[1]T)", want), + "got", fmt.Sprintf("%+[1]v (%[1]T)", got), + ) + } else { + slog.Info("trace: EqualMatch", + "key", key, + "want", fmt.Sprintf("%+[1]v (%[1]T)", want), + "got", fmt.Sprintf("%+[1]v (%[1]T)", got), + ) + } + }, + } } -func WithJobTrace(ctx context.Context, trace *JobTrace) context.Context { - return with(ctx, trace) +func WithJob(ctx context.Context, trace JobTrace) context.Context { + return with(ctx, &trace) } -func ContextJobTrace(ctx context.Context) *JobTrace { +func ContextJob(ctx context.Context) JobTrace { if trace := from[JobTrace](ctx); trace != nil { - return trace + return *trace + } + return nopJob +} + +func WithPattern(ctx context.Context, trace PatternTrace) context.Context { + return with(ctx, &trace) +} + +func ContextPattern(ctx context.Context) PatternTrace { + if trace := from[PatternTrace](ctx); trace != nil { + return *trace } - return nop + return nopPattern } type JobTrace struct { @@ -36,4 +166,13 @@ type JobTrace struct { TapMessage func() } +type PatternTrace struct { + ParseKey func(string, *gojq.Query, error) + UnmarshalValue func(string, []byte, any, error) + TemplateValue func(string, string, *template.Template, error) + ExecuteMatch func(string, []byte, error) + UnmarshalMatch func(string, []byte, any, error) + EqualMatch func(string, any, any, bool) +} + type EventInfo struct{}