Repository: vanus-labs/vanus (go)

Issue:
[Good First Issues]: implement function "split_with_intervals" - 7 points
# Motivation
Vanus Functions are a series of pre-built functions worked within Vanus transformer. By choosing a function, users can specify the way that events will be changed.

It will be a good start if you decide to contribute to Vanus codes.

# Details

Function name: **split_with_intervals**

Args

- sourceJsonPath
- startPosition - The character position where the split is to begin.
- splitInterval - The number of characters to skip before the next split.
- targetJsonPath

## Description

The function is used to split the source JSON path in a way that all characters before the starting position (not including the character in the starting position) will be the first sub-string, then from the character in the starting position (include itself), every fixed number of characters will be split into a substring.
For example, if the original string is "abcdefg", the startPosition is 1, and splitInterval is 2, then the string will be split into ["a", "bc", "de", "fg"].

## Example

```
{
    "specversion" : "1.0",
    "type" : "com.example.someevent",
    "source" : "/mycontext",
    "subject": null,
    "id" : "C234-1234-1234",
    "time" : "2018-04-05T17:31:00Z",
    "comexampleextension1" : "value",
    "comexampleothervalue" : 5,
    "datacontenttype" : "application/json",
    "data": {
        "appinfoA" : "hello world!"
       

    }
}
```

After the function.

```
"pipeline":[
    {"command":["split_with_intervals", "$.data.appinfoA", 2, 2, "$.data. appinfoB"]}
]
```

The transformed event should look like this:

```
{
    "specversion" : "1.0",
    "type" : "com.example.someevent",
    "source" : "/mycontext",
    "subject": null,
    "id" : "C234-1234-1234",
    "time" : "2018-04-05T17:31:00Z",
    "comexampleextension1" : "value",
    "comexampleothervalue" : 5,
    "datacontenttype" : "application/json",
    "data" : {
        "appinfoA" : "hello world!",
         "appinfoB" : ["ll", "o ", "wo", "rl", "d!"]
   }
}

```
# How to implement

1. Fork the vanus repository.
2. Create a branch `split_with_intervals`
3. Create `split_with_intervals.go` under `internal/primitive/transform/action/strings`, and then implement the function.
4. Create `split_with_intervals_test.go` under `internal/primitive/transform/action/strings` for unit test.
5. Register your function in `internal/primitive/transform/runtime/init.go`.
6. Submit your PR

# Contribution points

Each issue counts specific points based on its difficulty. Contributors earn the corresponding points if they solve that issue. Contribution points are used to describe the contributions contributors have made.  They can also be used to get rewards when there are community events.

This issue counts **7 points**.

# How to claim to solve the issue

If you want to implement this function, please leave a comment in this issue like:

> I'd like to implement this function, please assign this issue to me.

Vanus community will assign the issue to you on time.

Pull request description:
feat: add funtion split_with_intervals
<!--

Thank you for contributing to Vanus!

PR Title Format: 

feat/fix/refactor/docs/...: what's changed

Note: see https://github.com/linkall-labs/vanus/blob/main/CONTRIBUTING.md to know details about title format
-->

### What problem does this PR solve?
<!--

Please create an issue first to describe the problem

There MUST be one line starting with "Issue Number:  ".

-->

Issue Number: close #444

### Problem Summary

- This function is used to split the source JSON path in a way that all characters before the starting position (not including the character in the starting position) will be the first sub-string, then from the character in the starting position (include itself), every fixed number of characters will be split into a substring.

### What is changed and how does it work?

- implemented function SplitWithIntervals - internal/primitive/transform/action/strings/split_with_intervals.go
- create file with test - internal/primitive/transform/action/strings/split_with_intervals_test.go
- register function - NewSplitWithIntervals in vanus/internal/primitive/transform/runtime/init.go

### Check List

<!-- At least one of them must be included. -->

#### Tests

- [x] Unit test
- [ ] Integration test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No code


Patch:
diff --git a/internal/primitive/transform/action/strings/split_with_intervals.go b/internal/primitive/transform/action/strings/split_with_intervals.go
new file mode 100644
index 0000000..7a3855d
--- /dev/null
+++ b/internal/primitive/transform/action/strings/split_with_intervals.go
@@ -0,0 +1,80 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package strings
+
+import (
+	"fmt"
+
+	"github.com/vanus-labs/vanus/internal/primitive/transform/action"
+	"github.com/vanus-labs/vanus/internal/primitive/transform/arg"
+	"github.com/vanus-labs/vanus/internal/primitive/transform/common"
+	"github.com/vanus-labs/vanus/internal/primitive/transform/context"
+)
+
+type splitWithIntervalsAction struct {
+	action.CommonAction
+}
+
+// NewSplitWithIntervalsAction["sourceJSONPath", "startPosition", "splitInterval", "targetJsonPath"].
+func NewSplitWithIntervalsAction() action.Action {
+	return &splitWithIntervalsAction{
+		CommonAction: action.CommonAction{
+			ActionName: "SPLIT_WITH_INTERVALS",
+			FixedArgs:  []arg.TypeList{arg.EventList, arg.All, arg.All, []arg.Type{arg.EventData}},
+		},
+	}
+}
+
+func (a *splitWithIntervalsAction) Init(args []arg.Arg) error {
+	a.TargetArg = args[3]
+	a.Args = args[:3]
+	a.ArgTypes = []common.Type{common.String, common.Int, common.Int}
+	return nil
+}
+
+func (a *splitWithIntervalsAction) Execute(ceCtx *context.EventContext) error {
+	args, err := a.RunArgs(ceCtx)
+	if err != nil {
+		return err
+	}
+
+	v, _ := a.TargetArg.Evaluate(ceCtx)
+	if v != nil {
+		return fmt.Errorf("key %s exists", a.TargetArg.Original())
+	}
+
+	sourceJSONPath, _ := args[0].(string)
+	startPosition, _ := args[1].(int)
+	splitInterval, _ := args[2].(int)
+
+	// split string
+	var substrings []string
+	if startPosition > len(sourceJSONPath) {
+		// if startPosition is beyond the end of the string, return an error
+		return a.TargetArg.SetValue(ceCtx, []string{sourceJSONPath})
+	}
+
+	// split the string according to the specified interval
+	substrings = []string{sourceJSONPath[:startPosition]}
+	for i := startPosition; i < len(sourceJSONPath); i += splitInterval {
+		end := i + splitInterval
+		if end > len(sourceJSONPath) {
+			end = len(sourceJSONPath)
+		}
+		substrings = append(substrings, sourceJSONPath[i:end])
+	}
+
+	return a.TargetArg.SetValue(ceCtx, substrings)
+}
diff --git a/internal/primitive/transform/runtime/init.go b/internal/primitive/transform/runtime/init.go
index c6f7cbb..80c106a 100644
--- a/internal/primitive/transform/runtime/init.go
+++ b/internal/primitive/transform/runtime/init.go
@@ -53,6 +53,7 @@ func init() {
 		strings.NewReplaceBetweenPositionsAction,
 		strings.NewCapitalizeSentenceAction,
 		strings.NewCheckCustomValuesAction,
+		strings.NewSplitWithIntervalsAction,
 		strings.NewExtractBetweenDelimitersAction,
 		strings.NewExtractMissingAction,
 		strings.NewReplaceBetweenDelimitersAction,


Test patch:
diff --git a/internal/primitive/transform/action/strings/split_with_intervals_test.go b/internal/primitive/transform/action/strings/split_with_intervals_test.go
new file mode 100644
index 0000000..b22be05
--- /dev/null
+++ b/internal/primitive/transform/action/strings/split_with_intervals_test.go
@@ -0,0 +1,99 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package strings_test
+
+import (
+	"testing"
+
+	cetest "github.com/cloudevents/sdk-go/v2/test"
+	. "github.com/smartystreets/goconvey/convey"
+	"github.com/vanus-labs/vanus/internal/primitive/transform/action/strings"
+	"github.com/vanus-labs/vanus/internal/primitive/transform/context"
+	"github.com/vanus-labs/vanus/internal/primitive/transform/runtime"
+)
+
+func TestSplitWithIntervalsAction(t *testing.T) {
+	funcName := strings.NewSplitWithIntervalsAction().Name()
+
+	Convey("test Positive testcase", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.data.appinfoA", 2, 2, "$.data.appinfoB"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{
+			"appinfoA": "hello world!",
+		}
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		res, ok := data["appinfoB"]
+		So(ok, ShouldBeTrue)
+		So(res, ShouldResemble, []string{"he", "ll", "o ", "wo", "rl", "d!"})
+	})
+
+	Convey("test when start position is greater than the length of the string", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.data.appinfoA", 100, 2, "$.data.appinfoB"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{
+			"appinfoA": "hello world!",
+		}
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		res, ok := data["appinfoB"]
+		So(ok, ShouldBeTrue)
+		So(res, ShouldResemble, []string{"hello world!"})
+	})
+
+	Convey("test when the string can't be split exactly into equal intervals", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.data.appinfoA", 1, 3, "$.data.appinfoB"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{
+			"appinfoA": "hello world!",
+		}
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		res, ok := data["appinfoB"]
+		So(ok, ShouldBeTrue)
+		So(res, ShouldResemble, []string{"h", "ell", "o w", "orl", "d!"})
+	})
+
+	Convey("test when target key exists", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.data.appinfoA", 2, 2, "$.data.appinfoB"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{
+			"appinfoA": "hello world!",
+			"appinfoB": "",
+		}
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err.Error(), ShouldEqual, "key $.data.appinfoB exists")
+	})
+}