Skip to content

Commit 8ab8639

Browse files
committed
Add split function to rules engine
1 parent d752381 commit 8ab8639

File tree

8 files changed

+1367
-0
lines changed

8 files changed

+1367
-0
lines changed

docs/source-2.0/additional-specs/rules-engine/standard-library.rst

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,109 @@ The following table shows valid and invalid values for an input to the
376376
- ``true``
377377

378378

379+
.. _rules-engine-standard-library-split:
380+
381+
``split`` function
382+
==================
383+
384+
Summary
385+
Divides a string into an array of substrings based on a delimiter.
386+
Argument types
387+
* value: ``string``
388+
* delimiter: ``string``
389+
* limit: ``int``
390+
Return type
391+
``array<string>``
392+
Since
393+
1.1
394+
395+
The ``split`` function divides a string into an array of substrings based on a non-empty delimiter.
396+
The behavior is controlled by the limit parameter:
397+
398+
* ``limit = 0``: Split all occurrences (unlimited)
399+
* ``limit = 1``: No split performed (returns original string as single element array)
400+
* ``limit > 1``: Split into at most 'limit' parts (performs limit-1 splits)
401+
402+
.. important::
403+
The delimiter must not be null or empty. The limit must not be negative.
404+
For empty input strings, the function returns an array containing a single empty string.
405+
406+
The following example uses ``split`` to divide a bucket name into parts using ``--`` as the delimiter:
407+
408+
.. code-block:: json
409+
410+
{
411+
"fn": "split",
412+
"argv": [
413+
{"ref": "bucketName"},
414+
"--",
415+
0
416+
]
417+
}
418+
419+
420+
.. _rules-engine-standard-library-split-examples:
421+
422+
--------
423+
Examples
424+
--------
425+
426+
The following table shows various inputs and their corresponding outputs for the ``split`` function:
427+
428+
.. list-table::
429+
:header-rows: 1
430+
:widths: 25 15 10 50
431+
432+
* - Input
433+
- Delimiter
434+
- Limit
435+
- Output
436+
* - ``"a--b--c"``
437+
- ``"--"``
438+
- ``0``
439+
- ``["a", "b", "c"]``
440+
* - ``"a--b--c"``
441+
- ``"--"``
442+
- ``2``
443+
- ``["a", "b--c"]``
444+
* - ``"a--b--c"``
445+
- ``"--"``
446+
- ``1``
447+
- ``["a--b--c"]``
448+
* - ``""``
449+
- ``"--"``
450+
- ``0``
451+
- ``[""]``
452+
* - ``"--"``
453+
- ``"--"``
454+
- ``0``
455+
- ``["", ""]``
456+
* - ``"----"``
457+
- ``"--"``
458+
- ``0``
459+
- ``["", "", ""]``
460+
* - ``"--b--"``
461+
- ``"--"``
462+
- ``0``
463+
- ``["", "b", ""]``
464+
* - ``"--x-s3--azid--suffix"``
465+
- ``"--"``
466+
- ``0``
467+
- ``["", "x-s3", "azid", "suffix"]``
468+
* - ``"--x-s3--azid--suffix"``
469+
- ``"--"``
470+
- ``2``
471+
- ``["", "x-s3--azid--suffix"]``
472+
* - ``"abc"``
473+
- ``"x"``
474+
- ``0``
475+
- ``["abc"]``
476+
* - ``"mybucket"``
477+
- ``"--"``
478+
- ``1``
479+
- ``["mybucket"]``
480+
481+
379482
.. _rules-engine-standard-library-stringEquals:
380483

381484
``stringEquals`` function

smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/language/CoreExtension.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.IsValidHostLabel;
1414
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.Not;
1515
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.ParseUrl;
16+
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.Split;
1617
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.StringEquals;
1718
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.Substring;
1819
import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.UriEncode;
@@ -39,6 +40,7 @@ public List<FunctionDefinition> getLibraryFunctions() {
3940
Not.getDefinition(),
4041
Coalesce.getDefinition(),
4142
ParseUrl.getDefinition(),
43+
Split.getDefinition(),
4244
StringEquals.getDefinition(),
4345
Substring.getDefinition(),
4446
UriEncode.getDefinition());
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.rulesengine.language.syntax.expressions.functions;
6+
7+
import java.util.ArrayList;
8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.List;
11+
import java.util.Objects;
12+
import software.amazon.smithy.rulesengine.language.RulesVersion;
13+
import software.amazon.smithy.rulesengine.language.evaluation.type.Type;
14+
import software.amazon.smithy.rulesengine.language.evaluation.value.Value;
15+
import software.amazon.smithy.rulesengine.language.syntax.ToExpression;
16+
import software.amazon.smithy.rulesengine.language.syntax.expressions.ExpressionVisitor;
17+
import software.amazon.smithy.utils.SmithyUnstableApi;
18+
19+
/**
20+
* Splits a string by a delimiter into parts.
21+
*
22+
* <p>The split function divides a string into an array of substrings based on a non-empty delimiter.
23+
* The behavior is controlled by the limit parameter:
24+
* <ul>
25+
* <li>limit = 0: Split all occurrences (unlimited)</li>
26+
* <li>limit = 1: No split performed (returns original string as single element)</li>
27+
* <li>limit > 1: Split into at most 'limit' parts (performs limit-1 splits)</li>
28+
* </ul>
29+
*
30+
* <p>Examples:
31+
* <ul>
32+
* <li>{@code split("a--b--c", "--", 0)} returns {@code ["a", "b", "c"]}</li>
33+
* <li>{@code split("a--b--c", "--", 2)} returns {@code ["a", "b--c"]}</li>
34+
* <li>{@code split("--b--", "--", 0)} returns {@code ["", "b", ""]}</li>
35+
* <li>{@code split("abc", "x", 0)} returns {@code ["abc"]}</li>
36+
* <li>{@code split("", "--", 0)} returns {@code [""]}</li>
37+
* <li>{@code split("----", "--", 0)} returns {@code ["", "", ""]}</li>
38+
* <li>{@code split("a--b--c--d", "--", 3)} returns {@code ["a", "b", "c--d"]}</li>
39+
* <li>{@code split("prefix", "--", 0)} returns {@code ["prefix"]}</li>
40+
* <li>{@code split("--", "--", 0)} returns {@code ["", ""]}</li>
41+
* <li>{@code split("a-b-c", "--", 0)} returns {@code ["a-b-c"]}</li>
42+
* <li>{@code split("mybucket", "--", 1)} returns {@code ["mybucket"]}</li>
43+
* <li>{@code split("--x-s3--azid--suffix", "--", 0)} returns {@code ["", "x-s3", "azid", "suffix"]}</li>
44+
* <li>{@code split("--x-s3--azid--suffix", "--", 4)} returns {@code ["", "x-s3", "azid", "suffix"]}</li>
45+
* <li>{@code split("--x-s3--azid--suffix", "--", 2)} returns {@code ["", "x-s3--azid--suffix"]}</li>
46+
* </ul>
47+
*/
48+
@SmithyUnstableApi
49+
public final class Split extends LibraryFunction {
50+
public static final String ID = "split";
51+
private static final Definition DEFINITION = new Definition();
52+
53+
private Split(FunctionNode functionNode) {
54+
super(DEFINITION, functionNode);
55+
}
56+
57+
/**
58+
* Gets the {@link FunctionDefinition} implementation.
59+
*
60+
* @return the function definition.
61+
*/
62+
public static Definition getDefinition() {
63+
return DEFINITION;
64+
}
65+
66+
/**
67+
* Creates a {@link Split} function from the given expressions.
68+
*
69+
* @param string the string to split.
70+
* @param delimiter the delimiter.
71+
* @param limit the split limit (0 for unlimited, positive for max parts).
72+
* @return The resulting {@link Split} function.
73+
*/
74+
public static Split ofExpressions(ToExpression string, ToExpression delimiter, ToExpression limit) {
75+
return DEFINITION.createFunction(FunctionNode.ofExpressions(ID, string, delimiter, limit));
76+
}
77+
78+
@Override
79+
public <T> T accept(ExpressionVisitor<T> visitor) {
80+
return visitor.visitLibraryFunction(DEFINITION, getArguments());
81+
}
82+
83+
@Override
84+
public RulesVersion availableSince() {
85+
return RulesVersion.V1_1;
86+
}
87+
88+
/**
89+
* A {@link FunctionDefinition} for the {@link Split} function.
90+
*/
91+
public static final class Definition implements FunctionDefinition {
92+
private Definition() {}
93+
94+
@Override
95+
public String getId() {
96+
return ID;
97+
}
98+
99+
@Override
100+
public List<Type> getArguments() {
101+
return Arrays.asList(Type.stringType(), Type.stringType(), Type.integerType());
102+
}
103+
104+
@Override
105+
public Type getReturnType() {
106+
return Type.arrayType(Type.stringType());
107+
}
108+
109+
@Override
110+
public Value evaluate(List<Value> arguments) {
111+
String input = arguments.get(0).expectStringValue().getValue();
112+
String delimiter = arguments.get(1).expectStringValue().getValue();
113+
int limit = arguments.get(2).expectIntegerValue().getValue();
114+
115+
List<String> parts = split(input, delimiter, limit);
116+
117+
List<Value> values = new ArrayList<>();
118+
for (String part : parts) {
119+
values.add(Value.stringValue(part));
120+
}
121+
122+
return Value.arrayValue(values);
123+
}
124+
125+
@Override
126+
public Split createFunction(FunctionNode functionNode) {
127+
return new Split(functionNode);
128+
}
129+
}
130+
131+
/**
132+
* Split a string by a delimiter.
133+
*
134+
* @param value the string to split (must not be null).
135+
* @param delimiter the delimiter to split by (must not be null or empty).
136+
* @param limit controls the split behavior:
137+
* 0 = unlimited splits,
138+
* 1 = no split (return original),
139+
* n > 1 = split into at most n parts by performing up to n-1 split operations.
140+
* @return a non-null list of parts (never empty; returns [""] for empty input).
141+
* @throws NullPointerException if value or delimiter is null.
142+
* @throws IllegalArgumentException if delimiter is empty, or if limit is negative.
143+
*/
144+
public static List<String> split(String value, String delimiter, int limit) {
145+
Objects.requireNonNull(value, "Split value cannot be null");
146+
Objects.requireNonNull(delimiter, "Split delimiter cannot be null");
147+
if (delimiter.isEmpty()) {
148+
throw new IllegalArgumentException("Split delimiter cannot be empty");
149+
} else if (limit < 0) {
150+
throw new IllegalArgumentException("Split limit cannot be negative, but given: " + limit);
151+
} else if (value.isEmpty() || limit == 1) {
152+
return Collections.singletonList(value);
153+
}
154+
155+
final int delimLen = delimiter.length();
156+
final List<String> result = (limit > 1) ? new ArrayList<>(limit) : new ArrayList<>();
157+
int start = 0;
158+
int splits = 0;
159+
160+
while (limit == 0 || splits < limit - 1) {
161+
int pos = value.indexOf(delimiter, start);
162+
if (pos == -1) {
163+
break;
164+
}
165+
result.add(value.substring(start, pos));
166+
start = pos + delimLen;
167+
splits++;
168+
}
169+
170+
// Add remainder (or entire string if no delimiter found)
171+
result.add(value.substring(start));
172+
return result;
173+
}
174+
}

0 commit comments

Comments
 (0)