Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 895bc56

Browse files
committed
feature: Implement a @range expression
1 parent fcc36d6 commit 895bc56

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

doc/reference-expression.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@ Checks if an element exists within a list.
225225
- "@map": ["$$.image", "$.spec.containers"]
226226
```
227227

228+
#### `@range`
229+
Generates a list of sequential integers from a start value (inclusive) to an end value (exclusive).
230+
231+
* **Signature**: `{"@range": [<start_expression>, <end_expression>]}`
232+
* **Arguments**:
233+
1. An expression that evaluates to an integer (the start value, inclusive).
234+
2. An expression that evaluates to an integer (the end value, exclusive).
235+
* **Returns**: A `list` of integers `[start, start+1, ..., end-1]`. Returns an empty list if `start >= end`.
236+
* **Example**:
237+
```yaml
238+
# Generates [0, 1, 2] for a ReplicaSet with 3 replicas
239+
podSlots:
240+
"@range": [0, "$.spec.replicas"]
241+
```
242+
228243
### String Operators
229244

230245
#### `@concat`

pkg/expression/expression.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type Expression struct {
5959
// Evaluate processes an expression.
6060
func (e *Expression) Evaluate(ctx EvalCtx) (any, error) {
6161
if e == nil || len(e.Op) == 0 {
62-
return nil, NewInvalidArgumentsError(fmt.Sprintf("empty operator in expession %q", e.String()))
62+
return nil, NewInvalidArgumentsError(fmt.Sprintf("empty operator in expression %q", e.String()))
6363
}
6464

6565
switch e.Op {
@@ -1020,6 +1020,23 @@ func (e *Expression) Evaluate(ctx EvalCtx) (any, error) {
10201020

10211021
return v, nil
10221022

1023+
case "@range":
1024+
args, err := AsIntList(arg)
1025+
if err != nil {
1026+
return nil, NewExpressionError(e, err)
1027+
}
1028+
if len(args) != 2 {
1029+
return nil, NewExpressionError(e, errors.New("expected 2 arguments [start, end]"))
1030+
}
1031+
if args[0] > args[1] {
1032+
args[0] = args[1]
1033+
}
1034+
result := make([]any, 0, args[1]-args[0])
1035+
for i := args[0]; i < args[1]; i++ {
1036+
result = append(result, i)
1037+
}
1038+
return result, nil
1039+
10231040
case "@hash":
10241041
js, err := json.Marshal(arg)
10251042
if err != nil {

pkg/expression/expression_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,113 @@ var _ = Describe("Expressions", func() {
13041304
Expect(vs).To(Equal([]any{int64(1), int64(2), int64(3), int64(4), int64(5)}))
13051305
})
13061306

1307+
It("should evaluate a @range sequence from 0 to n-1", func() {
1308+
expr := Expression{
1309+
Op: "@range",
1310+
Arg: &Expression{Op: "@list", Literal: []Expression{
1311+
{Op: "@int", Literal: int64(0)},
1312+
{Op: "@int", Literal: int64(5)},
1313+
}},
1314+
}
1315+
1316+
result, err := expr.Evaluate(EvalCtx{Log: logger})
1317+
Expect(err).NotTo(HaveOccurred())
1318+
1319+
list, ok := result.([]any)
1320+
Expect(ok).To(BeTrue())
1321+
Expect(list).To(HaveLen(5))
1322+
Expect(list).To(Equal([]any{int64(0), int64(1), int64(2), int64(3), int64(4)}))
1323+
})
1324+
1325+
It("should evaluate a @range sequence with custom start", func() {
1326+
expr := Expression{
1327+
Op: "@range",
1328+
Arg: &Expression{Op: "@list", Literal: []Expression{
1329+
{Op: "@int", Literal: int64(3)},
1330+
{Op: "@int", Literal: int64(7)},
1331+
}},
1332+
}
1333+
1334+
result, err := expr.Evaluate(EvalCtx{Log: logger})
1335+
Expect(err).NotTo(HaveOccurred())
1336+
1337+
list, ok := result.([]any)
1338+
Expect(ok).To(BeTrue())
1339+
Expect(list).To(HaveLen(4))
1340+
Expect(list).To(Equal([]any{int64(3), int64(4), int64(5), int64(6)}))
1341+
})
1342+
1343+
It("should return empty list on @range when start equals end", func() {
1344+
expr := Expression{
1345+
Op: "@range",
1346+
Arg: &Expression{Op: "@list", Literal: []Expression{
1347+
{Op: "@int", Literal: int64(5)},
1348+
{Op: "@int", Literal: int64(5)},
1349+
}},
1350+
}
1351+
1352+
result, err := expr.Evaluate(EvalCtx{Log: logger})
1353+
Expect(err).NotTo(HaveOccurred())
1354+
1355+
list, ok := result.([]any)
1356+
Expect(ok).To(BeTrue())
1357+
Expect(list).To(BeEmpty())
1358+
})
1359+
1360+
It("should return empty list on @range when start is greater than end", func() {
1361+
expr := Expression{
1362+
Op: "@range",
1363+
Arg: &Expression{Op: "@list", Literal: []Expression{
1364+
{Op: "@int", Literal: int64(10)},
1365+
{Op: "@int", Literal: int64(5)},
1366+
}},
1367+
}
1368+
1369+
result, err := expr.Evaluate(EvalCtx{Log: logger})
1370+
Expect(err).NotTo(HaveOccurred())
1371+
1372+
list, ok := result.([]any)
1373+
Expect(ok).To(BeTrue())
1374+
Expect(list).To(BeEmpty())
1375+
})
1376+
1377+
It("should evaluate a @range with JSONPath expressions for end value", func() {
1378+
obj := map[string]any{
1379+
"spec": map[string]any{
1380+
"replicas": int64(3),
1381+
},
1382+
}
1383+
1384+
expr := Expression{
1385+
Op: "@range",
1386+
Arg: &Expression{Op: "@list", Literal: []Expression{
1387+
{Op: "@int", Literal: int64(0)},
1388+
{Op: "@string", Literal: "$.spec.replicas"},
1389+
}},
1390+
}
1391+
1392+
result, err := expr.Evaluate(EvalCtx{Object: obj, Log: logger})
1393+
Expect(err).NotTo(HaveOccurred())
1394+
1395+
list, ok := result.([]any)
1396+
Expect(ok).To(BeTrue())
1397+
Expect(list).To(HaveLen(3))
1398+
Expect(list).To(Equal([]any{int64(0), int64(1), int64(2)}))
1399+
})
1400+
1401+
It("should evaluate a @range with error on non-integer arguments", func() {
1402+
expr := Expression{
1403+
Op: "@range",
1404+
Arg: &Expression{Op: "@list", Literal: []Expression{
1405+
{Op: "@string", Literal: "not-a-number"},
1406+
{Op: "@int", Literal: int64(5)},
1407+
}},
1408+
}
1409+
1410+
_, err := expr.Evaluate(EvalCtx{Log: logger})
1411+
Expect(err).To(HaveOccurred())
1412+
})
1413+
13071414
// It("should evaluate stacked @map expressions", func() {
13081415
// jsonData := `{"@map":[{"@lte":["$",2]},{"@first":[{"@map":["$.spec.x"]}]}]}`
13091416
// var exp Expression

pkg/expression/marshaler.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ func (e *Expression) UnmarshalJSON(b []byte) error {
7171

7272
// UnmarshalJSON serializes an expression into JSON.
7373
func (e *Expression) MarshalJSON() ([]byte, error) {
74+
if len(e.Op) == 0 {
75+
return []byte("<nil>"), nil
76+
}
77+
7478
switch e.Op {
7579
case "@any":
7680
return json.Marshal(e.Literal)
@@ -165,6 +169,9 @@ func (e *Expression) MarshalJSON() ([]byte, error) {
165169

166170
// String stringifies an expression.
167171
func (e *Expression) String() string {
172+
if e == nil {
173+
return "<invalid>"
174+
}
168175
b, err := json.Marshal(e)
169176
if err != nil {
170177
return ""

0 commit comments

Comments
 (0)