Repository: vanus-labs/vanus (go)

Issue:
[Good First Issues]: implement function "split_from_start" - 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_from_start**

Args

- sourceJsonPath
- position
- targetJsonPath

## Description

The function is used to scan the source JSON path from left to right, split it into an array of two sub-strings from the given numeric position of the character, and assign the array to a target JSON path. The character of the position should be included in the first sub-string (from left to right).

## 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_from_start", "$.data.appinfoA", 5, "$.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" : ["hello", " world!"]
   }
}

```
# How to implement

1. Fork the vanus repository.
2. Create a branch `function-split_from_start`
3. Create `split_from_start.go` under `internal/primitive/transform/action/strings`, and then implement the function.
4. Create `split_from_start_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:
feat: implement function split from start
<!--

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 #445

### Problem Summary
The function is used to scan the source JSON path from left to right, split it into an array of two sub-strings from the given numeric position of the character, and assign the array to a target JSON path. The character of the position should be included in the first sub-string (from left to right).

### What is changed and how does it work?

- added the constructor for that function
- the implementation of the function
- unit tests for that function

### 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/replace_between_delimiters.go b/internal/primitive/transform/action/strings/replace_between_delimiters.go
new file mode 100644
index 0000000..a0ccb69
--- /dev/null
+++ b/internal/primitive/transform/action/strings/replace_between_delimiters.go
@@ -0,0 +1,32 @@
+// 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 (
+	"github.com/linkall-labs/vanus/internal/primitive/transform/action"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/arg"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/function"
+)
+
+// NewReplaceBetweenDelimitersAction ["path","startDelimiter","endDelimiter","newValue"].
+func NewReplaceBetweenDelimitersAction() action.Action {
+	a := &action.SourceTargetSameAction{}
+	a.CommonAction = action.CommonAction{
+		ActionName: "REPLACE_BETWEEN_DELIMITERS",
+		FixedArgs:  []arg.TypeList{arg.EventList, arg.All, arg.All, arg.All},
+		Fn:         function.ReplaceBetweenDelimitersFunction,
+	}
+	return a
+}
diff --git a/internal/primitive/transform/action/strings/split_between_positions.go b/internal/primitive/transform/action/strings/split_between_positions.go
new file mode 100644
index 0000000..a8ea063
--- /dev/null
+++ b/internal/primitive/transform/action/strings/split_between_positions.go
@@ -0,0 +1,90 @@
+// 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/linkall-labs/vanus/internal/primitive/transform/action"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/arg"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/common"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/context"
+)
+
+type splitBetweenPositionsAction struct {
+	action.CommonAction
+}
+
+// NewSplitBetweenPositionsAction["sourceJSONPath", "startPosition", "endPosition", "targetJsonPath"].
+func NewSplitBetweenPositionsAction() action.Action {
+	return &splitBetweenPositionsAction{
+		CommonAction: action.CommonAction{
+			ActionName: "SPLIT_BETWEEN_POSITIONS",
+			FixedArgs:  []arg.TypeList{arg.EventList, arg.All, arg.All, []arg.Type{arg.EventData}},
+		},
+	}
+}
+
+func (a *splitBetweenPositionsAction) 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 *splitBetweenPositionsAction) 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)
+	endPosition, _ := args[2].(int)
+
+	var substrings []string
+
+	switch {
+	case startPosition >= endPosition:
+		// if startPosition is gte endPosition, return an error
+		return fmt.Errorf("start position must be less than the endPosition")
+	case startPosition >= len(sourceJSONPath):
+		// if startPosition is beyond the end of the string
+		substrings = []string{
+			sourceJSONPath,
+			"",
+			"",
+		}
+	case endPosition > len(sourceJSONPath):
+		// if endPosition is beyond the end of the string
+		substrings = []string{
+			sourceJSONPath[:startPosition],
+			sourceJSONPath[startPosition:],
+			"",
+		}
+	default:
+		substrings = []string{
+			sourceJSONPath[:startPosition],
+			sourceJSONPath[startPosition:endPosition],
+			sourceJSONPath[endPosition:],
+		}
+	}
+	return a.TargetArg.SetValue(ceCtx, substrings)
+}
diff --git a/internal/primitive/transform/action/strings/split_from_start.go b/internal/primitive/transform/action/strings/split_from_start.go
new file mode 100644
index 0000000..99c3458
--- /dev/null
+++ b/internal/primitive/transform/action/strings/split_from_start.go
@@ -0,0 +1,44 @@
+// 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 (
+	"github.com/linkall-labs/vanus/internal/primitive/transform/action"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/arg"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/common"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/function"
+)
+
+type splitFromStartAction struct {
+	action.FunctionAction
+}
+
+// NewSplitFromStartAction ["split_from_start", "sourceJsonPath", "position", "targetJsonPath"].
+func NewSplitFromStartAction() action.Action {
+	a := &splitFromStartAction{}
+	a.CommonAction = action.CommonAction{
+		ActionName: "SPLIT_FROM_START",
+		FixedArgs:  []arg.TypeList{arg.EventList, []arg.Type{arg.Constant}, []arg.Type{arg.EventData}},
+		Fn:         function.SplitFromStart,
+	}
+	return a
+}
+
+func (a *splitFromStartAction) Init(args []arg.Arg) error {
+	a.TargetArg = args[2]
+	a.Args = args[:2]
+	a.ArgTypes = []common.Type{common.String, common.Int}
+	return nil
+}
diff --git a/internal/primitive/transform/function/strings_functions.go b/internal/primitive/transform/function/strings_functions.go
index 2846d27..4d3037a 100644
--- a/internal/primitive/transform/function/strings_functions.go
+++ b/internal/primitive/transform/function/strings_functions.go
@@ -120,6 +120,31 @@ var CapitalizeSentence = function{
 	},
 }
 
+var ReplaceBetweenDelimitersFunction = function{
+	name:      "REPLACE_BETWEEN_DELIMITERS",
+	fixedArgs: []common.Type{common.String, common.String, common.String, common.String},
+	fn: func(args []interface{}) (interface{}, error) {
+		value, _ := args[0].(string)
+		startDelimiter, _ := args[1].(string)
+		endDelimiter, _ := args[2].(string)
+		replaceValue, _ := args[3].(string)
+		if startDelimiter == "" || endDelimiter == "" {
+			return nil, fmt.Errorf("start or end delemiter is empty")
+		}
+		startIndex := strings.Index(value, startDelimiter)
+		if startIndex < 0 {
+			return nil, fmt.Errorf("start delemiter is not exist")
+		}
+		index := startIndex + len(startDelimiter)
+		endIndex := strings.Index(value[index:], endDelimiter)
+		if endIndex < 0 {
+			return nil, fmt.Errorf("end delemiter is not exist")
+		}
+		return value[:startIndex] + replaceValue + value[index+endIndex+len(endDelimiter):], nil
+
+	},
+}
+
 var CapitalizeWord = function{
 	name:      "CAPITALIZE_WORD",
 	fixedArgs: []common.Type{common.String},
@@ -140,3 +165,23 @@ var CapitalizeWord = function{
 		return string(rs), nil
 	},
 }
+
+var SplitFromStart = function{
+	name:      "SPLIT_FROM_START",
+	fixedArgs: []common.Type{common.String, common.Int},
+	fn: func(args []interface{}) (interface{}, error) {
+		value, _ := args[0].(string)
+		splitPosition, _ := args[1].(int)
+
+		if splitPosition <= 0 {
+			return nil, fmt.Errorf("split position must be more than zero")
+		}
+
+		if splitPosition >= len(value) {
+			return []string{value, ""}, nil
+		}
+
+		result := []string{value[:splitPosition], value[splitPosition:]}
+		return result, nil
+	},
+}
diff --git a/internal/primitive/transform/runtime/init.go b/internal/primitive/transform/runtime/init.go
index d9d775f..4907163 100644
--- a/internal/primitive/transform/runtime/init.go
+++ b/internal/primitive/transform/runtime/init.go
@@ -53,8 +53,11 @@ func init() {
 		strings.NewReplaceBetweenPositionsAction,
 		strings.NewCapitalizeSentenceAction,
 		strings.NewCheckCustomValuesAction,
+		strings.NewReplaceBetweenDelimitersAction,
 		strings.NewCapitalizeWordAction,
 		strings.NewSplitWithDelimiterAction,
+		strings.NewSplitBetweenPositionsAction,
+		strings.NewSplitFromStartAction,
 		// condition
 		condition.NewConditionIfAction,
 		// array


Test patch:
diff --git a/internal/primitive/transform/action/strings/replace_between_delimiters_test.go b/internal/primitive/transform/action/strings/replace_between_delimiters_test.go
new file mode 100644
index 0000000..da4230d
--- /dev/null
+++ b/internal/primitive/transform/action/strings/replace_between_delimiters_test.go
@@ -0,0 +1,94 @@
+// 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/linkall-labs/vanus/internal/primitive/transform/action/strings"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/context"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/runtime"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestReplaceBetweenDelimitersAction(t *testing.T) {
+	funcName := strings.NewReplaceBetweenDelimitersAction().Name()
+
+	Convey("test startDelimiter and endDelimiter present in string testcase", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", "&&", "&&", "Vanus"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		e.SetExtension("test", "Hello, &&World&&!")
+		ceCtx := &context.EventContext{
+			Event: &e,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		So(e.Extensions()["test"], ShouldEqual, "Hello, Vanus!")
+	})
+
+	Convey("test startDelimiter and endDelimiter present in string testcase", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", "^^", "^^", "lots of"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		e.SetExtension("test", "Vanus has ^^many^^ beginner friendly open issues!")
+		ceCtx := &context.EventContext{
+			Event: &e,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		So(e.Extensions()["test"], ShouldEqual, "Vanus has lots of beginner friendly open issues!")
+	})
+
+	Convey("test startDelimiter and endDelimiter not present in string testcase", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", "**", "**", "fun"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		e.SetExtension("test", "Contributing to Vanus Opensource project is %%an eye opener%%!")
+		ceCtx := &context.EventContext{
+			Event: &e,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldNotBeNil)
+		So(e.Extensions()["test"], ShouldEqual, "Contributing to Vanus Opensource project is %%an eye opener%%!")
+	})
+
+	Convey("test endDelimiter before startDelimiter in string testcase", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", "&&", "!!", "love"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		e.SetExtension("test", "I !!like&& opensource contributions")
+		ceCtx := &context.EventContext{
+			Event: &e,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldNotBeNil)
+		So(e.Extensions()["test"], ShouldEqual, "I !!like&& opensource contributions")
+	})
+
+	Convey("test Only endDelimiter present in string testcase", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", "&&", "**", "supported"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		e.SetExtension("test", "FOSS is !!powered** by open communities")
+		ceCtx := &context.EventContext{
+			Event: &e,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldNotBeNil)
+		So(e.Extensions()["test"], ShouldEqual, "FOSS is !!powered** by open communities")
+	})
+}
diff --git a/internal/primitive/transform/action/strings/split_between_positions_test.go b/internal/primitive/transform/action/strings/split_between_positions_test.go
new file mode 100644
index 0000000..faa739c
--- /dev/null
+++ b/internal/primitive/transform/action/strings/split_between_positions_test.go
@@ -0,0 +1,114 @@
+// 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/linkall-labs/vanus/internal/primitive/transform/action/strings"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/context"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/runtime"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestSplitBetweenPositionsAction(t *testing.T) {
+	funcName := strings.NewSplitBetweenPositionsAction().Name()
+
+	Convey("test Positive testcase", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.data.appinfoA", 2, 4, "$.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 world!"})
+	})
+
+	Convey("test when start position is greater than the end position", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.data.appinfoA", 7, 6, "$.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.Error(), ShouldEqual, "start position must be less than the endPosition")
+	})
+
+	Convey("test when start position is greater than the length of the string", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.data.appinfoA", 100, 200, "$.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 end position is greater than the length of the string", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.data.appinfoA", 4, 200, "$.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{"hell", "o world!", ""})
+	})
+
+	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")
+	})
+}
diff --git a/internal/primitive/transform/action/strings/split_from_start_test.go b/internal/primitive/transform/action/strings/split_from_start_test.go
new file mode 100644
index 0000000..2047925
--- /dev/null
+++ b/internal/primitive/transform/action/strings/split_from_start_test.go
@@ -0,0 +1,112 @@
+// 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/linkall-labs/vanus/internal/primitive/transform/action/strings"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/context"
+	"github.com/linkall-labs/vanus/internal/primitive/transform/runtime"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestSplitFromStartAction(t *testing.T) {
+	funcName := strings.NewSplitFromStartAction().Name()
+
+	Convey("test split from start: Positive testcase, common", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", 5, "$.data.target"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{}
+		e.SetExtension("test", "Hello, World!")
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		res, ok := data["target"]
+		So(ok, ShouldBeTrue)
+		So(res, ShouldResemble, []string{"Hello", ", World!"})
+	})
+
+	Convey("test split from start: Positive testcase, length 1", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", 1, "$.data.target"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{}
+		e.SetExtension("test", "H")
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		res, ok := data["target"]
+		So(ok, ShouldBeTrue)
+		So(res, ShouldResemble, []string{"H", ""})
+	})
+
+	Convey("test split from start: Positive testcase, length 0", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", 1, "$.data.target"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{}
+		e.SetExtension("test", "")
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		res, ok := data["target"]
+		So(ok, ShouldBeTrue)
+		So(res, ShouldResemble, []string{"", ""})
+	})
+
+	Convey("test split from start: Positive testcase, split position above value length", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", 10, "$.data.target"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{}
+		e.SetExtension("test", "hello")
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldBeNil)
+		res, ok := data["target"]
+		So(ok, ShouldBeTrue)
+		So(res, ShouldResemble, []string{"hello", ""})
+	})
+
+	Convey("test split from start: Negative testcase, split position less than one", t, func() {
+		a, err := runtime.NewAction([]interface{}{funcName, "$.test", 0, "$.data.target"})
+		So(err, ShouldBeNil)
+		e := cetest.MinEvent()
+		data := map[string]interface{}{}
+		e.SetExtension("test", "split position must be more than zero")
+		ceCtx := &context.EventContext{
+			Event: &e,
+			Data:  data,
+		}
+		err = a.Execute(ceCtx)
+		So(err, ShouldNotBeNil)
+		So(e.Extensions()["test"], ShouldEqual, "split position must be more than zero")
+	})
+}
diff --git a/internal/primitive/transform/runtime/action_bench_test.go b/internal/primitive/transform/runtime/action_bench_test.go
index ae8c9f5..143f6c1 100644
--- a/internal/primitive/transform/runtime/action_bench_test.go
+++ b/internal/primitive/transform/runtime/action_bench_test.go
@@ -81,4 +81,5 @@ func BenchmarkAction(b *testing.B) {
 	b.Run("check_custom_values", actionBenchmark([]interface{}{"check_custom_values", "$.data.str", "value", "$.data.target", "true", "false"}))
 	b.Run("split_with_delimiter", actionBenchmark([]interface{}{"split_with_delimiter", "$.data.str", "a", "$.data.target"}))
 	b.Run("unfold_array", actionBenchmark([]interface{}{"unfold_array", "$.data.str", "$.data.target"}))
+	b.Run("replace_between_delimiter", actionBenchmark([]interface{}{"replace_between_delimiter", "$.test", "&&", "&&", "Vanus"}))
 }