mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
Add strings.split_n built-in function (#8915)
### Why the changes in this PR are needed? Policies often need only the first or last few parts of a split string, but `split()` always returns every part. The common workarounds add noise: ```rego [name, email, _, _] := split(user, ";") [name, email] := array.slice(split(user, ";"), 0, 2) ``` ### What are the changes in this PR? Adds `strings.split_n(x, delimiter, n)`: - Positive n: returns the first n parts from the left - Negative n: returns the last abs(n) parts from the right - n=0: returns an empty array - If abs(n) exceeds the number of parts, all parts are returned ### Notes to assist PR review: Semantics follow the design agreed on in the review of #8361. ### Further comments: Fixes #8344 This change was developed with AI assistance. Signed-off-by: wonju lee <wonju@kia.com> Co-authored-by: wonju lee <wonju@kia.com>
This commit is contained in:
@@ -185,6 +185,7 @@
|
||||
"strings.render_template",
|
||||
"strings.replace_n",
|
||||
"strings.reverse",
|
||||
"strings.split_n",
|
||||
"substring",
|
||||
"trim",
|
||||
"trim_left",
|
||||
@@ -24640,6 +24641,36 @@
|
||||
},
|
||||
"wasm": true
|
||||
},
|
||||
"strings.split_n": {
|
||||
"args": [
|
||||
{
|
||||
"description": "string that is split",
|
||||
"name": "x",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "delimiter used for splitting",
|
||||
"name": "delimiter",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"description": "number of parts to return; positive selects from the left, negative from the right, zero returns an empty array",
|
||||
"name": "n",
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"available": [
|
||||
"edge"
|
||||
],
|
||||
"description": "Returns an array of at most `n` parts of `x` split on `delimiter`. If `n` is positive, returns the first `n` parts. If `n` is negative, returns the last `abs(n)` parts. If `n` is zero, returns an empty array. If `abs(n)` exceeds the number of parts, all parts are returned.",
|
||||
"introduced": "edge",
|
||||
"result": {
|
||||
"description": "split parts",
|
||||
"name": "ys",
|
||||
"type": "array[string]"
|
||||
},
|
||||
"wasm": false
|
||||
},
|
||||
"substring": {
|
||||
"args": [
|
||||
{
|
||||
|
||||
@@ -4177,6 +4177,29 @@
|
||||
"type": "function"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "strings.split_n",
|
||||
"decl": {
|
||||
"args": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"dynamic": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": "function"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "substring",
|
||||
"decl": {
|
||||
|
||||
@@ -141,6 +141,7 @@ var DefaultBuiltins = [...]*Builtin{
|
||||
StartsWith,
|
||||
EndsWith,
|
||||
Split,
|
||||
SplitN,
|
||||
Replace,
|
||||
ReplaceN,
|
||||
Trim,
|
||||
@@ -1281,6 +1282,21 @@ var Split = &Builtin{
|
||||
CanSkipBctx: true,
|
||||
}
|
||||
|
||||
var SplitN = &Builtin{
|
||||
Name: "strings.split_n",
|
||||
Description: "Returns an array of at most `n` parts of `x` split on `delimiter`. If `n` is positive, returns the first `n` parts. If `n` is negative, returns the last `abs(n)` parts. If `n` is zero, returns an empty array. If `abs(n)` exceeds the number of parts, all parts are returned.",
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("x", types.S).Description("string that is split"),
|
||||
types.Named("delimiter", types.S).Description("delimiter used for splitting"),
|
||||
types.Named("n", types.N).Description("number of parts to return; positive selects from the left, negative from the right, zero returns an empty array"),
|
||||
),
|
||||
types.Named("ys", types.NewArray(nil, types.S)).Description("split parts"),
|
||||
),
|
||||
Categories: stringsCat,
|
||||
CanSkipBctx: true,
|
||||
}
|
||||
|
||||
var Replace = &Builtin{
|
||||
Name: "replace",
|
||||
Description: "Replace replaces all instances of a sub-string.",
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
---
|
||||
cases:
|
||||
- note: strings/split_n/positive_basic
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", 2)
|
||||
}
|
||||
want_result:
|
||||
- x: ["a", "b"]
|
||||
- note: strings/split_n/positive_one
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", 1)
|
||||
}
|
||||
want_result:
|
||||
- x: ["a"]
|
||||
- note: strings/split_n/positive_exact
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", 4)
|
||||
}
|
||||
want_result:
|
||||
- x: ["a", "b", "c", "d"]
|
||||
- note: strings/split_n/positive_more_than_parts
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", 10)
|
||||
}
|
||||
want_result:
|
||||
- x: ["a", "b", "c", "d"]
|
||||
- note: strings/split_n/zero
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", 0)
|
||||
}
|
||||
want_result:
|
||||
- x: []
|
||||
- note: strings/split_n/negative_basic
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", -2)
|
||||
}
|
||||
want_result:
|
||||
- x: ["c", "d"]
|
||||
- note: strings/split_n/negative_one
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", -1)
|
||||
}
|
||||
want_result:
|
||||
- x: ["d"]
|
||||
- note: strings/split_n/negative_exact
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", -4)
|
||||
}
|
||||
want_result:
|
||||
- x: ["a", "b", "c", "d"]
|
||||
- note: strings/split_n/negative_more_than_parts
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c.d", ".", -10)
|
||||
}
|
||||
want_result:
|
||||
- x: ["a", "b", "c", "d"]
|
||||
- note: strings/split_n/no_delimiter_found_positive
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("abc", ".", 2)
|
||||
}
|
||||
want_result:
|
||||
- x: ["abc"]
|
||||
- note: strings/split_n/no_delimiter_found_negative
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("abc", ".", -2)
|
||||
}
|
||||
want_result:
|
||||
- x: ["abc"]
|
||||
- note: strings/split_n/empty_string
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("", ".", 2)
|
||||
}
|
||||
want_result:
|
||||
- x: [""]
|
||||
- note: strings/split_n/empty_delimiter_positive
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("abc", "", 2)
|
||||
}
|
||||
want_result:
|
||||
- x: ["a", "b"]
|
||||
- note: strings/split_n/empty_delimiter_negative
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("abc", "", -2)
|
||||
}
|
||||
want_result:
|
||||
- x: ["b", "c"]
|
||||
- note: strings/split_n/non_integer_n
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := result if {
|
||||
result := strings.split_n("a.b.c", ".", 1.5)
|
||||
}
|
||||
strict_error: true
|
||||
want_error_code: eval_type_error
|
||||
want_error: "strings.split_n: operand 3 must be integer number but got floating-point number"
|
||||
- note: strings/split_n/usecase_positive
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := [name, email] if {
|
||||
[name, email] := strings.split_n(input.user, ";", 2)
|
||||
}
|
||||
input:
|
||||
user: "alice;alice@example.com;admin;active"
|
||||
want_result:
|
||||
- x: ["alice", "alice@example.com"]
|
||||
- note: strings/split_n/usecase_negative
|
||||
query: data.test.p = x
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
|
||||
p := [name, email] if {
|
||||
[name, email] := strings.split_n(input.user, ";", -2)
|
||||
}
|
||||
input:
|
||||
user: "admin;active;alice;alice@example.com"
|
||||
want_result:
|
||||
- x: ["alice", "alice@example.com"]
|
||||
@@ -557,6 +557,55 @@ func builtinSplit(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e
|
||||
return iter(ast.ArrayTerm(util.SplitMap(text, delim, ast.InternedTerm)...))
|
||||
}
|
||||
|
||||
func builtinSplitN(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
s, err := builtins.StringOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d, err := builtins.StringOperand(operands[1].Value, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := builtins.IntOperand(operands[2].Value, 3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
text, delim := string(s), string(d)
|
||||
|
||||
var result []*ast.Term
|
||||
if n >= 0 {
|
||||
// n+1 may overflow for very large n; a negative limit means no limit.
|
||||
limit := n + 1
|
||||
if limit < 0 {
|
||||
limit = -1
|
||||
}
|
||||
parts := strings.SplitN(text, delim, limit)
|
||||
end := n
|
||||
if end > len(parts) {
|
||||
end = len(parts)
|
||||
}
|
||||
result = make([]*ast.Term, end)
|
||||
for i := range result {
|
||||
result[i] = ast.InternedTerm(parts[i])
|
||||
}
|
||||
} else {
|
||||
parts := strings.Split(text, delim)
|
||||
start := len(parts) + n
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
result = make([]*ast.Term, len(parts)-start)
|
||||
for i, p := range parts[start:] {
|
||||
result[i] = ast.InternedTerm(p)
|
||||
}
|
||||
}
|
||||
|
||||
return iter(ast.ArrayTerm(result...))
|
||||
}
|
||||
|
||||
func builtinReplace(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
s, err := builtins.StringOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
@@ -831,6 +880,7 @@ func init() {
|
||||
RegisterBuiltinFunc(ast.Upper.Name, builtinUpper)
|
||||
RegisterBuiltinFunc(ast.Lower.Name, builtinLower)
|
||||
RegisterBuiltinFunc(ast.Split.Name, builtinSplit)
|
||||
RegisterBuiltinFunc(ast.SplitN.Name, builtinSplitN)
|
||||
RegisterBuiltinFunc(ast.Replace.Name, builtinReplace)
|
||||
RegisterBuiltinFunc(ast.ReplaceN.Name, builtinReplaceN)
|
||||
RegisterBuiltinFunc(ast.Trim.Name, builtinTrim)
|
||||
|
||||
Reference in New Issue
Block a user