diff --git a/MODULE.bazel b/MODULE.bazel index 424ab3c..c11a0d0 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -19,6 +19,7 @@ use_repo( go_deps, "com_github_jedib0t_go_pretty_v6", "com_github_mark3labs_mcp_go", + "com_github_ohler55_ojg", "com_github_savioxavier_termlink", "com_github_spf13_cobra", "com_github_spf13_pflag", diff --git a/go.mod b/go.mod index aa5b87c..c4be76e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/eat-pray-ai/yutu go 1.24.3 require ( - github.com/mark3labs/mcp-go v0.32.0 + github.com/mark3labs/mcp-go v0.32.1-0.20250628092910-1eddde7bd69b github.com/savioxavier/termlink v1.4.3 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 @@ -40,6 +40,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jedib0t/go-pretty/v6 v6.6.7 + github.com/ohler55/ojg v1.26.8 github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/go.sum b/go.sum index 778959d..1046098 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,6 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -42,10 +40,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mark3labs/mcp-go v0.32.0 h1:fgwmbfL2gbd67obg57OfV2Dnrhs1HtSdlY/i5fn7MU8= -github.com/mark3labs/mcp-go v0.32.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= +github.com/mark3labs/mcp-go v0.32.1-0.20250628092910-1eddde7bd69b h1:nxi80AOhzZBM8ZiP9PAP26MJOT6WbEpkut8fmcOFPuo= +github.com/mark3labs/mcp-go v0.32.1-0.20250628092910-1eddde7bd69b/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/ohler55/ojg v1.26.8 h1:njM65m+ej8sLHiFZIhJK9UkwOmDPsUikjGbTgcwu8CU= +github.com/ohler55/ojg v1.26.8/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -58,8 +58,6 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= -github.com/savioxavier/termlink v1.4.2 h1:PRlvcStluuSKA87KCIqzODknYeQ3XEcgJP6DvAAVl1c= -github.com/savioxavier/termlink v1.4.2/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= github.com/savioxavier/termlink v1.4.3 h1:Gh6vrG7jSn21cRiYdQqFXYcdXfM+Fg14aG487JTfKpA= github.com/savioxavier/termlink v1.4.3/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -82,26 +80,16 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -118,8 +106,6 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs= -google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo= google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= diff --git a/pkg/utils/BUILD.bazel b/pkg/utils/BUILD.bazel index 0b436aa..688821d 100644 --- a/pkg/utils/BUILD.bazel +++ b/pkg/utils/BUILD.bazel @@ -6,6 +6,7 @@ go_library( importpath = "github.com/eat-pray-ai/yutu/pkg/utils", visibility = ["//visibility:public"], deps = [ + "@com_github_ohler55_ojg//jp", "@com_github_spf13_pflag//:pflag", "@in_gopkg_yaml_v3//:yaml_v3", ], diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 60309a1..148827a 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,9 +5,9 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/ohler55/ojg/jp" "github.com/spf13/pflag" "io" - "os" "os/exec" "path/filepath" "runtime" @@ -15,17 +15,25 @@ import ( "gopkg.in/yaml.v3" ) -func PrintJSON(data interface{}, writer io.Writer) { - if writer == nil { // todo: remove fallback to stdout - writer = os.Stdout +func PrintJSON(data interface{}, jpath string, writer io.Writer) { + j, err := jp.ParseString(jpath) + if err != nil && jpath != "" { + _, _ = fmt.Fprintln(writer, "Invalid JSONPath:", jpath) + return + } else if jpath != "" { + data = j.Get(data) } marshalled, _ := json.MarshalIndent(data, "", " ") _, _ = fmt.Fprintln(writer, string(marshalled)) } -func PrintYAML(data interface{}, writer io.Writer) { - if writer == nil { // todo: remove fallback to stdout - writer = os.Stdout +func PrintYAML(data interface{}, jpath string, writer io.Writer) { + j, err := jp.ParseString(jpath) + if err != nil && jpath != "" { + _, _ = fmt.Fprintln(writer, "Invalid JSONPath:", jpath) + return + } else if jpath != "" { + data = j.Get(data) } marshalled, _ := yaml.Marshal(data) _, _ = fmt.Fprintln(writer, string(marshalled)) diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 87e02e7..8375703 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -2,7 +2,6 @@ package utils import ( "bytes" - "fmt" "github.com/spf13/cobra" "github.com/spf13/pflag" "reflect" @@ -115,24 +114,76 @@ func TestIsJson(t *testing.T) { func TestPrintJSON(t *testing.T) { type args struct { - data interface{} + data interface{} + jpath string } tests := []struct { name string args args wantWriter string }{ + { + name: "empty jsonpath", + args: args{data: map[string]string{"key": "value"}, jpath: ""}, + wantWriter: "{\n \"key\": \"value\"\n}\n", + }, { name: "simple json", - args: args{data: map[string]string{"key": "value"}}, - wantWriter: fmt.Sprint("{\n \"key\": \"value\"\n}\n"), + args: args{data: map[string]string{"key": "value"}, jpath: "$"}, + wantWriter: "[\n {\n \"key\": \"value\"\n }\n]\n", + }, + { + name: "invalid jsonpath", + args: args{ + data: map[string]string{"key": "value"}, jpath: "//", + }, + wantWriter: "Invalid JSONPath: //\n", + }, + { + name: "extract specific field", + args: args{ + data: map[string]interface{}{ + "key": "value", + "another": "field", + }, jpath: "$.key", + }, + wantWriter: "[\n \"value\"\n]\n", + }, + { + name: "nested jsonpath", + args: args{ + data: map[string]interface{}{ + "item1": map[string]string{"key1": "value1"}, + "item2": map[string]string{"key2": "value2"}, + "count": 2, + }, + jpath: "$.*.key1", + }, + wantWriter: "[\n \"value1\"\n]\n", + }, + { + name: "array jsonpath", + args: args{ + data: []map[string]string{ + {"key1": "value1"}, + {"key2": "value2"}, + {"key1": "value3"}, + }, + jpath: "$[*].key1", + }, + wantWriter: "[\n \"value1\",\n \"value3\"\n]\n", + }, + { + name: "nil data", + args: args{data: nil, jpath: "$"}, + wantWriter: "[\n null\n]\n", }, } for _, tt := range tests { t.Run( tt.name, func(t *testing.T) { writer := &bytes.Buffer{} - PrintJSON(tt.args.data, writer) + PrintJSON(tt.args.data, tt.args.jpath, writer) if gotWriter := writer.String(); gotWriter != tt.wantWriter { t.Errorf("PrintJSON() = %v, want %v", gotWriter, tt.wantWriter) } @@ -143,7 +194,8 @@ func TestPrintJSON(t *testing.T) { func TestPrintYAML(t *testing.T) { type args struct { - data interface{} + data interface{} + jpath string } tests := []struct { name string @@ -151,16 +203,67 @@ func TestPrintYAML(t *testing.T) { wantWriter string }{ { - name: "simple yaml", - args: args{data: map[string]string{"key": "value"}}, + name: "empty jsonpath", + args: args{data: map[string]string{"key": "value"}, jpath: ""}, wantWriter: "key: value\n\n", }, + { + name: "simple yaml", + args: args{data: map[string]string{"key": "value"}, jpath: "$"}, + wantWriter: "- key: value\n\n", + }, + { + name: "invalid jsonpath", + args: args{ + data: map[string]string{"key": "value"}, jpath: "//", + }, + wantWriter: "Invalid JSONPath: //\n", + }, + { + name: "extract specific field", + args: args{ + data: map[string]interface{}{ + "key": "value", + "another": "field", + }, jpath: "$.key", + }, + wantWriter: "- value\n\n", + }, + { + name: "nested jsonpath", + args: args{ + data: map[string]interface{}{ + "item1": map[string]string{"key1": "value1"}, + "item2": map[string]string{"key2": "value2"}, + "count": 2, + }, + jpath: "$.*.key1", + }, + wantWriter: "- value1\n\n", + }, + { + name: "array jsonpath", + args: args{ + data: []map[string]string{ + {"key1": "value1"}, + {"key2": "value2"}, + {"key1": "value3"}, + }, + jpath: "$[*].key1", + }, + wantWriter: "- value1\n- value3\n\n", + }, + { + name: "nil data", + args: args{data: nil, jpath: "$"}, + wantWriter: "- null\n\n", + }, } for _, tt := range tests { t.Run( tt.name, func(t *testing.T) { writer := &bytes.Buffer{} - PrintYAML(tt.args.data, writer) + PrintYAML(tt.args.data, tt.args.jpath, writer) if gotWriter := writer.String(); gotWriter != tt.wantWriter { t.Errorf("PrintYAML() = %v, want %v", gotWriter, tt.wantWriter) }