Skip to content

Commit 2d826be

Browse files
michaelmcneesclaude
andcommitted
fix: address PR #21 review round 3 — test coverage, trailing JSON, docs accuracy
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3473d2a commit 2d826be

4 files changed

Lines changed: 65 additions & 33 deletions

File tree

docs/superpowers/plans/2026-03-24-data-transformation.md

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -822,8 +822,9 @@ In `functions.go`, add to `collectionLib.CompileOptions()`:
822822
cel.Overload("default_any_any",
823823
[]*cel.Type{cel.DynType, cel.DynType},
824824
cel.DynType,
825+
cel.OverloadIsNonStrict(),
825826
cel.BinaryBinding(func(lhs, rhs ref.Val) ref.Val {
826-
if types.IsError(lhs) || types.IsUnknown(lhs) {
827+
if types.IsError(lhs) || types.IsUnknown(lhs) || lhs == types.NullValue {
827828
return rhs
828829
}
829830
return lhs
@@ -836,20 +837,20 @@ In `functions.go`, add to `collectionLib.CompileOptions()`:
836837
cel.ListType(cel.DynType),
837838
cel.UnaryBinding(func(val ref.Val) ref.Val {
838839
list := val.(traits.Lister)
839-
var result []ref.Val
840+
var result []any
840841
it := list.Iterator()
841842
for it.HasNext() == types.True {
842843
item := it.Next()
843844
if sub, ok := item.(traits.Lister); ok {
844845
subIt := sub.Iterator()
845846
for subIt.HasNext() == types.True {
846-
result = append(result, subIt.Next())
847+
result = append(result, refToNative(subIt.Next()))
847848
}
848849
} else {
849-
result = append(result, item)
850+
result = append(result, refToNative(item))
850851
}
851852
}
852-
return types.DefaultTypeAdapter.NativeToValue(nativeSlice(result))
853+
return types.DefaultTypeAdapter.NativeToValue(result)
853854
}),
854855
),
855856
),
@@ -974,11 +975,13 @@ func (l *jsonLib) CompileOptions() []cel.EnvOption {
974975
cel.DynType,
975976
cel.UnaryBinding(func(val ref.Val) ref.Val {
976977
s := string(val.(types.String))
978+
dec := json.NewDecoder(strings.NewReader(s))
979+
dec.UseNumber()
977980
var result any
978-
if err := json.Unmarshal([]byte(s), &result); err != nil {
981+
if err := dec.Decode(&result); err != nil {
979982
return types.NewErr("jsonDecode: %v", err)
980983
}
981-
return types.DefaultTypeAdapter.NativeToValue(result)
984+
return types.DefaultTypeAdapter.NativeToValue(normalizeJSONNumbers(result))
982985
}),
983986
),
984987
),
@@ -1091,11 +1094,20 @@ func (l *timeLib) CompileOptions() []cel.EnvOption {
10911094
cel.TimestampType,
10921095
cel.UnaryBinding(func(val ref.Val) ref.Val {
10931096
s := string(val.(types.String))
1094-
t, err := time.Parse(time.RFC3339, s)
1095-
if err != nil {
1096-
return types.NewErr("parseTimestamp: %v", err)
1097+
layouts := []string{
1098+
time.RFC3339,
1099+
time.RFC3339Nano,
1100+
"2006-01-02T15:04:05",
1101+
"2006-01-02",
1102+
"01/02/2006",
1103+
"Jan 2, 2006",
1104+
}
1105+
for _, layout := range layouts {
1106+
if t, err := time.Parse(layout, s); err == nil {
1107+
return types.Timestamp{Time: t}
1108+
}
10971109
}
1098-
return types.Timestamp{Time: t}
1110+
return types.NewErr("parseTimestamp: unable to parse %q (tried RFC3339, ISO 8601 date, and common formats)", s)
10991111
}),
11001112
),
11011113
),
@@ -1258,27 +1270,29 @@ In `examples/data-transform-api-to-db.yaml`:
12581270
```yaml
12591271
name: data-transform-api-to-db
12601272
description: >
1261-
Fetches user data from an API, transforms each record using CEL
1262-
expressions to match a database schema, and inserts the normalized
1263-
records into Postgres. Demonstrates map(), obj(), toLower(), and
1264-
type coercion without requiring an AI model.
1273+
Fetches a user from an API, transforms the record using CEL expressions
1274+
to match a database schema, and inserts the normalized data into Postgres.
1275+
Demonstrates toLower() and string functions without requiring an AI model.
12651276
12661277
steps:
1267-
- name: fetch-users
1278+
- name: fetch-user
12681279
action: http/request
12691280
timeout: "15s"
12701281
params:
12711282
method: GET
1272-
url: "https://jsonplaceholder.typicode.com/users"
1283+
url: "https://jsonplaceholder.typicode.com/users/1"
12731284
headers:
12741285
Accept: "application/json"
12751286

1276-
- name: store-users
1287+
- name: store-user
12771288
action: postgres/query
12781289
credential: app-db
12791290
params:
12801291
query: "INSERT INTO users (username, email, city) VALUES ($1, $2, $3)"
1281-
params: "{{ steps['fetch-users'].output.json.map(u, [u.username.toLower(), u.email.toLower(), u.address.city]) }}"
1292+
args:
1293+
- "{{ steps['fetch-user'].output.json.username.toLower() }}"
1294+
- "{{ steps['fetch-user'].output.json.email.toLower() }}"
1295+
- "{{ steps['fetch-user'].output.json.address.city }}"
12821296
```
12831297
12841298
- [ ] **Step 2: Create AI enrichment example**
@@ -1346,7 +1360,7 @@ steps:
13461360
query: >
13471361
INSERT INTO urgent_tickets (priority, category, products, raw_body)
13481362
VALUES ($1, $2, $3, $4)
1349-
params:
1363+
args:
13501364
- "{{ steps.classify.output.json.priority }}"
13511365
- "{{ steps.classify.output.json.category }}"
13521366
- "{{ jsonEncode(steps.classify.output.json.products) }}"

internal/cel/functions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ func (l *jsonLib) CompileOptions() []cel.EnvOption {
280280
if err := dec.Decode(&result); err != nil {
281281
return types.NewErr("jsonDecode: %v", err)
282282
}
283+
// Reject trailing data (e.g., "{} {}")
284+
if dec.More() {
285+
return types.NewErr("jsonDecode: unexpected trailing data after JSON value")
286+
}
283287
return types.DefaultTypeAdapter.NativeToValue(normalizeJSONNumbers(result))
284288
}),
285289
),

internal/cel/functions_test.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,20 +253,31 @@ func TestFunc_Default(t *testing.T) {
253253
eval, err := NewEvaluator()
254254
require.NoError(t, err)
255255
tests := []struct {
256-
name string
257-
expr string
258-
want any
256+
name string
257+
expr string
258+
want any
259+
wantErr bool
259260
}{
260-
{"value exists returns value", `default("hello", "fallback")`, "hello"},
261-
{"empty string returns empty string", `default("", "fallback")`, ""},
262-
{"null returns fallback", `default(null, "fallback")`, "fallback"},
263-
{"non-null int unchanged", `default(42, 0)`, int64(42)},
261+
{"value exists returns value", `default("hello", "fallback")`, "hello", false},
262+
{"empty string returns empty string", `default("", "fallback")`, "", false},
263+
{"null returns fallback", `default(null, "fallback")`, "fallback", false},
264+
{"non-null int unchanged", `default(42, 0)`, int64(42), false},
265+
{
266+
"missing_key_returns_fallback",
267+
`default(steps.fetch.output.missing, "fallback")`,
268+
"fallback",
269+
false,
270+
},
264271
}
265272
for _, tt := range tests {
266273
t.Run(tt.name, func(t *testing.T) {
267274
result, err := eval.Eval(tt.expr, newTestContext())
268-
require.NoError(t, err)
269-
assert.Equal(t, tt.want, result)
275+
if tt.wantErr {
276+
require.Error(t, err)
277+
} else {
278+
require.NoError(t, err)
279+
assert.Equal(t, tt.want, result)
280+
}
270281
})
271282
}
272283
}
@@ -362,6 +373,11 @@ func TestFunc_JsonDecode(t *testing.T) {
362373
expr: `jsonDecode("not json")`,
363374
wantErr: true,
364375
},
376+
{
377+
name: "trailing_data",
378+
expr: `jsonDecode("{} {}")`,
379+
wantErr: true,
380+
},
365381
}
366382
for _, tt := range tests {
367383
t.Run(tt.name, func(t *testing.T) {

site/src/content/docs/getting-started/data-transformations.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,8 @@ steps:
8686
8787
- name: classify
8888
action: ai/completion
89+
credential: openai
8990
params:
90-
provider: openai
91-
credential: openai
9291
model: gpt-4o-mini
9392
prompt: >
9493
Classify the following support ticket. Extract the priority, product
@@ -180,9 +179,8 @@ steps:
180179
181180
- name: enrich
182181
action: ai/completion
182+
credential: openai
183183
params:
184-
provider: openai
185-
credential: openai
186184
model: gpt-4o-mini
187185
prompt: >
188186
Analyze the following product reviews and classify the sentiment

0 commit comments

Comments
 (0)