Skip to content

Commit 624863f

Browse files
committed
feat(server): add uuid() and ulid() built-in expression functions
Add uuid() and ulid() as built-in functions available in expr-lang expressions and {{ }} interpolation. uuid() supports v4 (default) and v7 via an optional version argument: uuid(), uuid("v4"), uuid("v7").
1 parent d5292a7 commit 624863f

4 files changed

Lines changed: 238 additions & 3 deletions

File tree

packages/server/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/expr-lang/expr v1.17.7
1010
github.com/goccy/go-json v0.10.5
1111
github.com/golang-jwt/jwt/v5 v5.3.0
12+
github.com/google/uuid v1.6.0
1213
github.com/klauspost/compress v1.18.2
1314
github.com/lithammer/fuzzysearch v1.1.8
1415
github.com/oklog/ulid/v2 v2.1.1
@@ -43,7 +44,6 @@ require (
4344
github.com/go-logr/stdr v1.2.2 // indirect
4445
github.com/google/generative-ai-go v0.20.1 // indirect
4546
github.com/google/s2a-go v0.1.9 // indirect
46-
github.com/google/uuid v1.6.0 // indirect
4747
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
4848
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
4949
github.com/mattn/go-isatty v0.0.20 // indirect

packages/server/pkg/expression/builtins.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
//nolint:revive // exported
22
package expression
33

4-
import "fmt"
4+
import (
5+
"fmt"
6+
7+
"github.com/google/uuid"
8+
"github.com/oklog/ulid/v2"
9+
)
510

611
// =============================================================================
712
// Built-in AI Expression Function (method on UnifiedEnv)
@@ -13,6 +18,34 @@ import "fmt"
1318
// Usage: ai("varName", "description", "type")
1419
// Returns: value if exists, error if not found
1520

21+
// helperUUID generates a new UUID string. Defaults to v4.
22+
// Usage in expressions: uuid() or uuid("v4") or uuid("v7")
23+
func helperUUID(args ...string) (string, error) {
24+
version := "v4"
25+
if len(args) > 0 {
26+
version = args[0]
27+
}
28+
29+
switch version {
30+
case "v4":
31+
return uuid.New().String(), nil
32+
case "v7":
33+
id, err := uuid.NewV7()
34+
if err != nil {
35+
return "", fmt.Errorf("uuid: failed to generate v7: %w", err)
36+
}
37+
return id.String(), nil
38+
default:
39+
return "", fmt.Errorf("uuid: unsupported version %q, use \"v4\" or \"v7\"", version)
40+
}
41+
}
42+
43+
// helperULID generates a new ULID string.
44+
// Usage in expressions: ulid()
45+
func helperULID() string {
46+
return ulid.Make().String()
47+
}
48+
1649
// helperAI returns the value of varName if it exists, otherwise returns an error.
1750
// The description and varType parameters are metadata hints for AI tooling.
1851
func (e *UnifiedEnv) helperAI(name, description, varType string) (any, error) {

packages/server/pkg/expression/builtins_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,208 @@ package expression
22

33
import (
44
"context"
5+
"regexp"
56
"strings"
67
"testing"
78
)
89

10+
// =============================================================================
11+
// UUID Built-in Tests
12+
// =============================================================================
13+
14+
var (
15+
uuidV4Regex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
16+
uuidV7Regex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
17+
)
18+
19+
func TestBuiltinUUID_DefaultIsV4(t *testing.T) {
20+
env := NewUnifiedEnv(nil)
21+
ctx := context.Background()
22+
23+
result, err := env.Eval(ctx, "uuid()")
24+
if err != nil {
25+
t.Fatalf("unexpected error: %v", err)
26+
}
27+
28+
str, ok := result.(string)
29+
if !ok {
30+
t.Fatalf("expected string, got %T", result)
31+
}
32+
33+
if !uuidV4Regex.MatchString(str) {
34+
t.Errorf("expected valid UUID v4, got: %s", str)
35+
}
36+
}
37+
38+
func TestBuiltinUUID_ExplicitV4(t *testing.T) {
39+
env := NewUnifiedEnv(nil)
40+
ctx := context.Background()
41+
42+
result, err := env.Eval(ctx, `uuid("v4")`)
43+
if err != nil {
44+
t.Fatalf("unexpected error: %v", err)
45+
}
46+
47+
str, ok := result.(string)
48+
if !ok {
49+
t.Fatalf("expected string, got %T", result)
50+
}
51+
52+
if !uuidV4Regex.MatchString(str) {
53+
t.Errorf("expected valid UUID v4, got: %s", str)
54+
}
55+
}
56+
57+
func TestBuiltinUUID_V7(t *testing.T) {
58+
env := NewUnifiedEnv(nil)
59+
ctx := context.Background()
60+
61+
result, err := env.Eval(ctx, `uuid("v7")`)
62+
if err != nil {
63+
t.Fatalf("unexpected error: %v", err)
64+
}
65+
66+
str, ok := result.(string)
67+
if !ok {
68+
t.Fatalf("expected string, got %T", result)
69+
}
70+
71+
if !uuidV7Regex.MatchString(str) {
72+
t.Errorf("expected valid UUID v7, got: %s", str)
73+
}
74+
}
75+
76+
func TestBuiltinUUID_InvalidVersion(t *testing.T) {
77+
env := NewUnifiedEnv(nil)
78+
ctx := context.Background()
79+
80+
_, err := env.Eval(ctx, `uuid("v5")`)
81+
if err == nil {
82+
t.Fatal("expected error for unsupported version, got nil")
83+
}
84+
85+
if !strings.Contains(err.Error(), "unsupported version") {
86+
t.Errorf("expected 'unsupported version' in error, got: %v", err)
87+
}
88+
}
89+
90+
func TestBuiltinUUID_UniquePerCall(t *testing.T) {
91+
env := NewUnifiedEnv(nil)
92+
ctx := context.Background()
93+
94+
result1, err := env.Eval(ctx, "uuid()")
95+
if err != nil {
96+
t.Fatalf("unexpected error: %v", err)
97+
}
98+
99+
result2, err := env.Eval(ctx, "uuid()")
100+
if err != nil {
101+
t.Fatalf("unexpected error: %v", err)
102+
}
103+
104+
if result1 == result2 {
105+
t.Errorf("expected unique UUIDs, got same value twice: %v", result1)
106+
}
107+
}
108+
109+
func TestBuiltinUUID_Interpolation(t *testing.T) {
110+
env := NewUnifiedEnv(nil)
111+
112+
result, err := env.Interpolate("id={{ uuid() }}")
113+
if err != nil {
114+
t.Fatalf("unexpected error: %v", err)
115+
}
116+
117+
if !strings.HasPrefix(result, "id=") {
118+
t.Errorf("expected 'id=' prefix, got: %s", result)
119+
}
120+
121+
uuidPart := strings.TrimPrefix(result, "id=")
122+
if !uuidV4Regex.MatchString(uuidPart) {
123+
t.Errorf("expected valid UUID v4 after prefix, got: %s", uuidPart)
124+
}
125+
}
126+
127+
func TestBuiltinUUID_V7Interpolation(t *testing.T) {
128+
env := NewUnifiedEnv(nil)
129+
130+
result, err := env.Interpolate(`id={{ uuid("v7") }}`)
131+
if err != nil {
132+
t.Fatalf("unexpected error: %v", err)
133+
}
134+
135+
uuidPart := strings.TrimPrefix(result, "id=")
136+
if !uuidV7Regex.MatchString(uuidPart) {
137+
t.Errorf("expected valid UUID v7 after prefix, got: %s", uuidPart)
138+
}
139+
}
140+
141+
// =============================================================================
142+
// ULID Built-in Tests
143+
// =============================================================================
144+
145+
var ulidRegex = regexp.MustCompile(`^[0-9A-HJKMNP-TV-Z]{26}$`)
146+
147+
func TestBuiltinULID_Eval(t *testing.T) {
148+
env := NewUnifiedEnv(nil)
149+
ctx := context.Background()
150+
151+
result, err := env.Eval(ctx, "ulid()")
152+
if err != nil {
153+
t.Fatalf("unexpected error: %v", err)
154+
}
155+
156+
str, ok := result.(string)
157+
if !ok {
158+
t.Fatalf("expected string, got %T", result)
159+
}
160+
161+
if !ulidRegex.MatchString(str) {
162+
t.Errorf("expected valid ULID, got: %s", str)
163+
}
164+
}
165+
166+
func TestBuiltinULID_UniquePerCall(t *testing.T) {
167+
env := NewUnifiedEnv(nil)
168+
ctx := context.Background()
169+
170+
result1, err := env.Eval(ctx, "ulid()")
171+
if err != nil {
172+
t.Fatalf("unexpected error: %v", err)
173+
}
174+
175+
result2, err := env.Eval(ctx, "ulid()")
176+
if err != nil {
177+
t.Fatalf("unexpected error: %v", err)
178+
}
179+
180+
if result1 == result2 {
181+
t.Errorf("expected unique ULIDs, got same value twice: %v", result1)
182+
}
183+
}
184+
185+
func TestBuiltinULID_Interpolation(t *testing.T) {
186+
env := NewUnifiedEnv(nil)
187+
188+
result, err := env.Interpolate("id={{ ulid() }}")
189+
if err != nil {
190+
t.Fatalf("unexpected error: %v", err)
191+
}
192+
193+
if !strings.HasPrefix(result, "id=") {
194+
t.Errorf("expected 'id=' prefix, got: %s", result)
195+
}
196+
197+
ulidPart := strings.TrimPrefix(result, "id=")
198+
if !ulidRegex.MatchString(ulidPart) {
199+
t.Errorf("expected valid ULID after prefix, got: %s", ulidPart)
200+
}
201+
}
202+
203+
// =============================================================================
204+
// AI Built-in Tests
205+
// =============================================================================
206+
9207
func TestBuiltinAI_ErrorWhenNotFound(t *testing.T) {
10208
env := NewUnifiedEnv(nil)
11209

packages/server/pkg/expression/eval.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ func (e *UnifiedEnv) buildExprEnv() map[string]any {
226226
// Add built-in AI helper function (closure that captures 'e' for variable lookup)
227227
env["ai"] = e.helperAI
228228

229+
// Add built-in ID generator functions
230+
env["uuid"] = helperUUID
231+
env["ulid"] = helperULID
232+
229233
return env
230234
}
231235

@@ -344,7 +348,7 @@ func isKeyword(s string) bool {
344348
"contains": true, "startsWith": true, "endsWith": true,
345349
"now": true, "date": true, "duration": true,
346350
// Custom helper functions
347-
"get": true, "has": true, "ai": true,
351+
"get": true, "has": true, "ai": true, "uuid": true, "ulid": true,
348352
}
349353
return keywords[s]
350354
}

0 commit comments

Comments
 (0)