From f6e465d55627718a81500745165a10bab64baeb6 Mon Sep 17 00:00:00 2001 From: 3timeslazy Date: Mon, 29 Oct 2018 23:57:39 +0300 Subject: [PATCH] Feature/#106 fmt function (#151) --- pkg/stdlib/objects/merge_recursive_test.go | 2 - pkg/stdlib/strings/fmt.go | 88 ++++++++++++ pkg/stdlib/strings/fmt_test.go | 152 +++++++++++++++++++++ pkg/stdlib/strings/lib.go | 1 + 4 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 pkg/stdlib/strings/fmt.go create mode 100644 pkg/stdlib/strings/fmt_test.go diff --git a/pkg/stdlib/objects/merge_recursive_test.go b/pkg/stdlib/objects/merge_recursive_test.go index 0a2c1ff5..835b36d8 100644 --- a/pkg/stdlib/objects/merge_recursive_test.go +++ b/pkg/stdlib/objects/merge_recursive_test.go @@ -2,7 +2,6 @@ package objects_test import ( "context" - "log" "testing" "github.com/MontFerret/ferret/pkg/runtime/values" @@ -131,7 +130,6 @@ func TestMergeRecursive(t *testing.T) { actual, err := objects.MergeRecursive(context.Background(), obj1, obj2) - log.Println(actual) So(err, ShouldBeNil) So(actual.Compare(expected), ShouldEqual, 0) }) diff --git a/pkg/stdlib/strings/fmt.go b/pkg/stdlib/strings/fmt.go new file mode 100644 index 00000000..094aaf1e --- /dev/null +++ b/pkg/stdlib/strings/fmt.go @@ -0,0 +1,88 @@ +package strings + +import ( + "context" + "regexp" + "strconv" + "strings" + + "github.com/MontFerret/ferret/pkg/runtime/core" + "github.com/MontFerret/ferret/pkg/runtime/values" + "github.com/pkg/errors" +) + +// Fmt formats the template using these arguments. +// @params template (String) - template. +// @params args (Any Values) - template arguments. +// @returns (String) - string formed by template using arguments. +func Fmt(_ context.Context, args ...core.Value) (core.Value, error) { + err := core.ValidateArgs(args, 1, core.MaxArgs) + if err != nil { + return values.None, err + } + + err = core.ValidateType(args[0], core.StringType) + if err != nil { + return values.None, err + } + + formatted, err := format(args[0].String(), args[1:]) + if err != nil { + return values.None, err + } + + return values.NewString(formatted), nil +} + +func format(template string, args []core.Value) (string, error) { + rgx, err := regexp.Compile("{[0-9]*}") + if err != nil { + return "", errors.Errorf("failed to build regexp: %v", err) + } + + argsCount := len(args) + emptyBracketsCount := strings.Count(template, "{}") + + if argsCount > emptyBracketsCount && emptyBracketsCount != 0 { + return "", errors.Errorf("there are arguments that have never been used") + } + + var betweenBrackets string + var n int + // index of the last value + // inserted into the template + var lastArgIdx int + + template = rgx.ReplaceAllStringFunc(template, func(s string) string { + if err != nil { + return "" + } + + betweenBrackets = s[1 : len(s)-1] + + if betweenBrackets == "" { + if argsCount <= lastArgIdx { + err = errors.Errorf("not enought arguments") + return "" + } + + lastArgIdx++ + return args[lastArgIdx-1].String() + } + + n, err = strconv.Atoi(betweenBrackets) + if err != nil { + err = errors.Errorf("failed to parse int: %v", err) + return "" + } + + if n >= argsCount { + err = errors.Errorf("invalid reference to argument `%d`", n) + return "" + } + + return args[n].String() + }) + + return template, err +} diff --git a/pkg/stdlib/strings/fmt_test.go b/pkg/stdlib/strings/fmt_test.go new file mode 100644 index 00000000..cf4603ff --- /dev/null +++ b/pkg/stdlib/strings/fmt_test.go @@ -0,0 +1,152 @@ +package strings_test + +import ( + "context" + "testing" + + "github.com/MontFerret/ferret/pkg/runtime/core" + + "github.com/MontFerret/ferret/pkg/runtime/values" + "github.com/MontFerret/ferret/pkg/stdlib/strings" + . "github.com/smartystreets/goconvey/convey" +) + +type testCase struct { + Name string + Expected string + Format string + Args []core.Value + ShouldErr bool +} + +func TestFmt(t *testing.T) { + tcs := []*testCase{ + &testCase{ + Name: `FMT("{}", 1) return "1"`, + Expected: "1", + Format: "{}", + Args: []core.Value{ + values.NewInt(1), + }, + }, + &testCase{ + Name: `FMT("{1} {} {0} {}", 1, 2) return "2 1 1 2"`, + Expected: "2 1 1 2", + Format: "{1} {} {0} {}", + Args: []core.Value{ + values.NewInt(1), + values.NewInt(2), + }, + }, + &testCase{ + Name: `FMT("{1} {} {0} {} {}", 1, 2, 3) return "2 1 1 2 3"`, + Expected: "2 1 1 2 3", + Format: "{1} {} {0} {} {}", + Args: []core.Value{ + values.NewInt(1), + values.NewInt(2), + values.NewInt(3), + }, + }, + &testCase{ + Name: `FMT("{2}{1} {0}", "World!", ",", "Hello") return "Hello, World!"`, + Expected: "Hello, World!", + Format: "{2}{1} {0}", + Args: []core.Value{ + values.NewString("World!"), + values.NewString(","), + values.NewString("Hello"), + }, + }, + &testCase{ + Name: `FMT({}, {key:"value"}) return "{"key":"value"}"`, + Expected: `{"key":"value"}`, + Format: "{}", + Args: []core.Value{ + values.NewObjectWith( + values.NewObjectProperty( + "key", values.NewString("value"), + ), + ), + }, + }, + &testCase{ + Name: `FMT({}, {key:"value"}) return "{"key":"value"}"`, + Expected: `{"key":"value","yek":"eulav"}`, + Format: "{}", + Args: []core.Value{ + values.NewObjectWith( + values.NewObjectProperty( + "key", values.NewString("value"), + ), + values.NewObjectProperty( + "yek", values.NewString("eulav"), + ), + ), + }, + }, + &testCase{ + Name: `FMT("string") return "string"`, + Expected: "string", + Format: "string", + }, + &testCase{ + Name: `FMT("string") return "string"`, + Expected: "string", + Format: "string", + Args: []core.Value{ + values.NewInt(1), + }, + }, + &testCase{ + Name: `FMT("{}") return error`, + Format: "{}", + Args: []core.Value{}, + ShouldErr: true, + }, + &testCase{ + Name: `FMT("{1}", 10) return error`, + Format: "{1}", + Args: []core.Value{ + values.NewInt(10), + }, + ShouldErr: true, + }, + &testCase{ + Name: `FMT("{1} {} {0} {}", 1, 2, 3) return error`, + Format: "{1} {} {0} {}", + Args: []core.Value{ + values.NewInt(1), + values.NewInt(2), + values.NewInt(3), + }, + ShouldErr: true, + }, + } + + for _, tc := range tcs { + tc.Do(t) + } +} + +func (tc *testCase) Do(t *testing.T) { + Convey(tc.Name, t, func() { + var expected core.Value + + expected = values.NewString(tc.Expected) + + args := []core.Value{values.NewString(tc.Format)} + args = append(args, tc.Args...) + + formatted, err := strings.Fmt(context.Background(), args...) + + if tc.ShouldErr { + So(err, ShouldBeError) + expected = values.None + } else { + So(err, ShouldBeNil) + } + + So(formatted, ShouldEqual, expected) + }) +} diff --git a/pkg/stdlib/strings/lib.go b/pkg/stdlib/strings/lib.go index dfe5101d..b43a881b 100644 --- a/pkg/stdlib/strings/lib.go +++ b/pkg/stdlib/strings/lib.go @@ -34,5 +34,6 @@ func NewLib() map[string]core.Function { "FROM_BASE64": FromBase64, "TRIM": Trim, "UPPER": Upper, + "FMT": Fmt, } }