From 5e8d67a5fbd7325efbd753aec4f270ef4a54d7fe Mon Sep 17 00:00:00 2001 From: Anton Medvedev Date: Tue, 3 May 2022 00:26:44 +0200 Subject: [PATCH] Add default reducer --- pkg/reducer/reduce.go | 35 +++++++- pkg/reducer/split.go | 178 ++++++++++++++++++++++++++++++++++++++ pkg/reducer/split_test.go | 137 +++++++++++++++++++++++++++++ 3 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 pkg/reducer/split.go create mode 100644 pkg/reducer/split_test.go diff --git a/pkg/reducer/reduce.go b/pkg/reducer/reduce.go index 665dd62..ac12775 100644 --- a/pkg/reducer/reduce.go +++ b/pkg/reducer/reduce.go @@ -6,8 +6,10 @@ import ( "encoding/json" "fmt" "os/exec" + "strconv" "strings" + . "github.com/antonmedv/fx/pkg/dict" . "github.com/antonmedv/fx/pkg/json" . "github.com/antonmedv/fx/pkg/theme" ) @@ -26,7 +28,38 @@ func GenerateCode(lang string, args []string) string { } func Reduce(object interface{}, lang string, args []string, theme Theme) int { - if len(args) == 0 { + path, ok := split(args) + if ok { + for _, get := range path { + switch get := get.(type) { + case string: + switch o := object.(type) { + case *Dict: + object = o.Values[get] + case string: + if get == "length" { + object = Number(strconv.Itoa(len([]rune(o)))) + } else { + object = nil + } + case Array: + if get == "length" { + object = Number(strconv.Itoa(len(o))) + } else { + object = nil + } + default: + object = nil + } + case int: + switch o := object.(type) { + case Array: + object = o[get] + default: + object = nil + } + } + } echo(object, theme) return 0 } diff --git a/pkg/reducer/split.go b/pkg/reducer/split.go new file mode 100644 index 0000000..c1436b5 --- /dev/null +++ b/pkg/reducer/split.go @@ -0,0 +1,178 @@ +package reducer + +import ( + "strconv" + "unicode" +) + +type state int + +const ( + start state = iota + unknown + propOrIndex + prop + index + indexEnd + number + doubleQuote + doubleQuoteEscape + singleQuote + singleQuoteEscape +) + +func split(args []string) ([]interface{}, bool) { + path := make([]interface{}, 0) + for _, arg := range args { + s := "" + state := start + for _, ch := range arg { + switch state { + + case start: + switch { + case ch == 'x': + state = unknown + case ch == '.': + state = propOrIndex + default: + return path, false + } + + case unknown: + switch { + case ch == '.': + state = prop + s = "" + case ch == '[': + state = index + s = "" + default: + return path, false + } + + case propOrIndex: + switch { + case isProp(ch): + state = prop + s = string(ch) + case ch == '[': + state = index + default: + return path, false + } + + case prop: + switch { + case isProp(ch): + s += string(ch) + case ch == '.': + state = prop + path = append(path, s) + s = "" + case ch == '[': + state = index + path = append(path, s) + s = "" + default: + return path, false + } + + case index: + switch { + case unicode.IsDigit(ch): + state = number + s = string(ch) + case ch == '"': + state = doubleQuote + s = "" + case ch == '\'': + state = singleQuote + s = "" + default: + return path, false + } + + case indexEnd: + switch { + case ch == ']': + state = unknown + default: + return path, false + } + + case number: + switch { + case unicode.IsDigit(ch): + s += string(ch) + case ch == ']': + state = unknown + n, err := strconv.Atoi(s) + if err != nil { + return path, false + } + path = append(path, n) + s = "" + default: + return path, false + } + + case doubleQuote: + switch ch { + case '"': + state = indexEnd + path = append(path, s) + s = "" + case '\\': + state = doubleQuoteEscape + default: + s += string(ch) + } + + case doubleQuoteEscape: + switch ch { + case '"': + state = doubleQuote + s += string(ch) + default: + return path, false + } + + case singleQuote: + switch ch { + case '\'': + state = indexEnd + path = append(path, s) + s = "" + case '\\': + state = singleQuoteEscape + s += string(ch) + default: + s += string(ch) + } + + case singleQuoteEscape: + switch ch { + case '\'': + state = singleQuote + s += string(ch) + default: + return path, false + } + } + } + if len(s) > 0 { + if state == prop { + path = append(path, s) + } else { + return path, false + } + + } + } + return path, true +} + +func isProp(ch rune) bool { + return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$' +} diff --git a/pkg/reducer/split_test.go b/pkg/reducer/split_test.go new file mode 100644 index 0000000..1b9efb7 --- /dev/null +++ b/pkg/reducer/split_test.go @@ -0,0 +1,137 @@ +package reducer + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_split(t *testing.T) { + tests := []struct { + args []string + want []interface{} + }{ + { + args: []string{}, + want: []interface{}{}, + }, + { + args: []string{"."}, + want: []interface{}{}, + }, + { + args: []string{"x"}, + want: []interface{}{}, + }, + { + args: []string{".foo"}, + want: []interface{}{"foo"}, + }, + { + args: []string{"x.foo"}, + want: []interface{}{"foo"}, + }, + { + args: []string{"x[42]"}, + want: []interface{}{42}, + }, + { + args: []string{".[42]"}, + want: []interface{}{42}, + }, + { + args: []string{".42"}, + want: []interface{}{"42"}, + }, + { + args: []string{".физ"}, + want: []interface{}{"физ"}, + }, + { + args: []string{".foo.bar"}, + want: []interface{}{"foo", "bar"}, + }, + { + args: []string{".foo", ".bar"}, + want: []interface{}{"foo", "bar"}, + }, + { + args: []string{".foo[42]"}, + want: []interface{}{"foo", 42}, + }, + { + args: []string{".foo[42].bar"}, + want: []interface{}{"foo", 42, "bar"}, + }, + { + args: []string{".foo[1][2]"}, + want: []interface{}{"foo", 1, 2}, + }, + { + args: []string{".foo[\"bar\"]"}, + want: []interface{}{"foo", "bar"}, + }, + { + args: []string{".foo[\"bar\\\"\"]"}, + want: []interface{}{"foo", "bar\""}, + }, + { + args: []string{".foo['bar']['baz\\'']"}, + want: []interface{}{"foo", "bar", "baz\\'"}, + }, + } + for _, tt := range tests { + t.Run(strings.Join(tt.args, " "), func(t *testing.T) { + path, ok := split(tt.args) + require.Equal(t, tt.want, path) + require.True(t, ok) + }) + } +} + +func Test_split_negative(t *testing.T) { + tests := []struct { + args []string + }{ + { + args: []string{"./"}, + }, + { + args: []string{"x/"}, + }, + { + args: []string{"1+1"}, + }, + { + args: []string{"x[42"}, + }, + { + args: []string{".i % 2"}, + }, + { + args: []string{"x[for x]"}, + }, + { + args: []string{"x['y'."}, + }, + { + args: []string{"x[0?"}, + }, + { + args: []string{"x[\"\\u"}, + }, + { + args: []string{"x['\\n"}, + }, + { + args: []string{"x[9999999999999999999999999999999999999]"}, + }, + } + for _, tt := range tests { + t.Run(strings.Join(tt.args, " "), func(t *testing.T) { + path, ok := split(tt.args) + require.False(t, ok, path) + }) + } +}