CEL expressions for mapping fields from input objects to output fields.
- Go: github.com/google/cel-go --
go get github.com/google/cel-go - Spec: cel-spec/langdef.md
package main
import (
"fmt"
"log"
"github.com/google/cel-go/cel"
)
// eval: compile + evaluate a CEL expression and print the result.
// The env declares which fields the expression is allowed to reference.
// Compile() rejects anything not in the env (that's your validation).
func eval(env *cel.Env, expr string, data map[string]interface{}) {
ast, issues := env.Compile(expr)
if issues != nil && issues.Err() != nil {
log.Fatalf("compile: %s\n expr: %s", issues.Err(), expr)
}
prg, _ := env.Program(ast)
out, _, err := prg.Eval(data)
if err != nil {
log.Fatalf("eval: %s\n expr: %s", err, expr)
}
fmt.Printf("%v\n", out)
}
func main() {
// =====================================================================
// 1. RENAME A FIELD -- the simplest mapping
// =====================================================================
// Input has "name", output wants "full_name". The CEL expression is
// just the source field name. You control the output key in your
// mapping config -- CEL only produces the value.
env, _ := cel.NewEnv(cel.Variable("name", cel.StringType))
eval(env, `name`, map[string]interface{}{"name": "Alice"})
// => Alice
// mapping: { "full_name": "name" } -- output key is "full_name", CEL expr is "name"
env, _ = cel.NewEnv(cel.Variable("age", cel.IntType))
eval(env, `age`, map[string]interface{}{"age": int64(30)})
// => 30
env, _ = cel.NewEnv(cel.Variable("email", cel.StringType))
eval(env, `email`, map[string]interface{}{"email": "alice@example.com"})
// => alice@example.com
// =====================================================================
// 1b. GRAB NESTED FIELDS -- use dot notation to reach into objects
// =====================================================================
// Declare the top-level object with DynType so CEL can traverse it.
env, _ = cel.NewEnv(cel.Variable("user", cel.MapType(cel.StringType, cel.DynType)))
data := map[string]interface{}{
"user": map[string]interface{}{
"name": "Alice",
"email": "alice@example.com",
"address": map[string]interface{}{
"city": "Seattle",
"state": "WA",
"zip": "98101",
},
},
}
// Pull a single nested field
eval(env, `user.name`, data)
// => Alice
eval(env, `user.email`, data)
// => alice@example.com
// Reach deeper
eval(env, `user.address.city`, data)
// => Seattle
eval(env, `user.address.zip`, data)
// => 98101
// Combine nested fields
eval(env, `user.address.city + ", " + user.address.state`, data)
// => Seattle, WA
// Mix top-level and nested
eval(env, `user.name + " (" + user.address.city + ", " + user.address.state + ")"`, data)
// => Alice (Seattle, WA)
// Multiple top-level objects
env, _ = cel.NewEnv(
cel.Variable("order", cel.MapType(cel.StringType, cel.DynType)),
cel.Variable("customer", cel.MapType(cel.StringType, cel.DynType)),
)
eval(env, `customer.name + " - order " + order.id`, map[string]interface{}{
"order": map[string]interface{}{"id": "ORD-123", "total": 99.50},
"customer": map[string]interface{}{"name": "Bob", "tier": "premium"},
})
// => Bob - order ORD-123
// =====================================================================
// 2. TRANSFORM A FIELD -- change the value, not just the name
// =====================================================================
env, _ = cel.NewEnv(cel.Variable("code", cel.StringType))
eval(env, `code.toUpperCase()`, map[string]interface{}{"code": "abc"})
// => ABC
env, _ = cel.NewEnv(cel.Variable("count", cel.IntType))
eval(env, `string(count)`, map[string]interface{}{"count": int64(3)})
// => 3
env, _ = cel.NewEnv(cel.Variable("count", cel.IntType))
eval(env, `string(count) + " items"`, map[string]interface{}{"count": int64(3)})
// => 3 items
// =====================================================================
// 3. COMBINE FIELDS -- build one value from multiple source fields
// =====================================================================
env, _ = cel.NewEnv(
cel.Variable("first", cel.StringType),
cel.Variable("last", cel.StringType),
)
eval(env, `first + " " + last`, map[string]interface{}{
"first": "Alice", "last": "Smith",
})
// => Alice Smith
env, _ = cel.NewEnv(
cel.Variable("city", cel.StringType),
cel.Variable("state", cel.StringType),
cel.Variable("zip", cel.StringType),
)
eval(env, `city + ", " + state + " " + zip`, map[string]interface{}{
"city": "Seattle", "state": "WA", "zip": "98101",
})
// => Seattle, WA 98101
env, _ = cel.NewEnv(
cel.Variable("amount", cel.DoubleType),
cel.Variable("currency", cel.StringType),
)
eval(env, `"$" + string(amount) + " " + currency`, map[string]interface{}{
"amount": 125.50, "currency": "USD",
})
// => $125.5 USD
// =====================================================================
// 4. STRING FUNCTIONS
// =====================================================================
// .contains(s) .startsWith(s) .endsWith(s)
// .matches(re) .size() .toLowerCase() .toUpperCase()
env, _ = cel.NewEnv(cel.Variable("email", cel.StringType))
eval(env, `email.contains("@")`, map[string]interface{}{"email": "alice@example.com"})
// => true
eval(env, `email.endsWith(".com")`, map[string]interface{}{"email": "alice@example.com"})
// => true
env, _ = cel.NewEnv(cel.Variable("zip", cel.StringType))
eval(env, `zip.matches("^[0-9]{5}$")`, map[string]interface{}{"zip": "98101"})
// => true
// =====================================================================
// 5. TYPE CONVERSIONS
// =====================================================================
// string(x) int(x) double(x)
// GOTCHA: int * double is a type error. Cast explicitly.
env, _ = cel.NewEnv(
cel.Variable("count", cel.IntType),
cel.Variable("rate", cel.DoubleType),
)
eval(env, `double(count) * rate`, map[string]interface{}{
"count": int64(5), "rate": 1.5,
})
// => 7.5
// =====================================================================
// 6. BOOLEAN -- derive true/false from a field
// =====================================================================
env, _ = cel.NewEnv(cel.Variable("total", cel.DoubleType))
eval(env, `total > 100.0`, map[string]interface{}{"total": 125.50})
// => true
env, _ = cel.NewEnv(
cel.Variable("active", cel.BoolType),
cel.Variable("role", cel.StringType),
)
eval(env, `active && role == "admin"`, map[string]interface{}{
"active": true, "role": "admin",
})
// => true
// =====================================================================
// 7. LISTS
// =====================================================================
// x in list -- membership
// list[i] -- index access
// list.size() -- length
// list.all(x, expr) -- true if all match
// list.exists(x, expr) -- true if any match
// list.filter(x, expr) -- keep matching
// list.map(x, expr) -- transform
env, _ = cel.NewEnv(cel.Variable("tags", cel.ListType(cel.StringType)))
eval(env, `"rush" in tags`, map[string]interface{}{
"tags": []string{"rush", "fragile"},
})
// => true
eval(env, `tags.size()`, map[string]interface{}{
"tags": []string{"a", "b", "c"},
})
// => 3
eval(env, `tags[0]`, map[string]interface{}{
"tags": []string{"first", "second"},
})
// => first
env, _ = cel.NewEnv(cel.Variable("prices", cel.ListType(cel.DoubleType)))
eval(env, `prices.all(p, p > 0.0)`, map[string]interface{}{
"prices": []float64{10.0, 25.0, 3.50},
})
// => true
eval(env, `prices.filter(p, p > 20.0)`, map[string]interface{}{
"prices": []float64{10.0, 50.0, 3.50},
})
// => [50.0]
env, _ = cel.NewEnv(cel.Variable("names", cel.ListType(cel.StringType)))
eval(env, `names.filter(n, n.startsWith("A"))`, map[string]interface{}{
"names": []string{"Alice", "Bob", "Anna"},
})
// => ["Alice", "Anna"]
eval(env, `names.map(n, n.toUpperCase())`, map[string]interface{}{
"names": []string{"alice", "bob"},
})
// => ["ALICE", "BOB"]
// =====================================================================
// 8. COMBINED -- multiple techniques together
// =====================================================================
env, _ = cel.NewEnv(
cel.Variable("first", cel.StringType),
cel.Variable("last", cel.StringType),
cel.Variable("count", cel.IntType),
cel.Variable("total", cel.DoubleType),
cel.Variable("tier", cel.StringType),
)
eval(env,
`first + " " + last + " - " + string(count) + " items, $" + string(total) + " (" + tier + ")"`,
map[string]interface{}{
"first": "Alice", "last": "Smith", "count": int64(3), "total": 125.50, "tier": "premium",
},
)
// => Alice Smith - 3 items, $125.5 (premium)
// =====================================================================
// 9. CONDITIONAL / FALLBACK -- derive a value from logic
// =====================================================================
env, _ = cel.NewEnv(cel.Variable("country", cel.StringType))
eval(env, `country == "US" ? "domestic" : "international"`, map[string]interface{}{
"country": "US",
})
// => domestic
env, _ = cel.NewEnv(cel.Variable("notes", cel.StringType))
eval(env, `notes != "" ? notes : "N/A"`, map[string]interface{}{
"notes": "",
})
// => N/A
env, _ = cel.NewEnv(cel.Variable("age", cel.IntType))
eval(env, `age >= 65 ? "senior" : age >= 18 ? "adult" : "minor"`, map[string]interface{}{
"age": int64(30),
})
// => adult
env, _ = cel.NewEnv(
cel.Variable("tier", cel.StringType),
cel.Variable("total", cel.DoubleType),
)
eval(env, `tier == "premium" && total > 100.0 ? "high" : "normal"`, map[string]interface{}{
"tier": "premium", "total": 125.50,
})
// => high
env, _ = cel.NewEnv(
cel.Variable("tier", cel.StringType),
cel.Variable("rush", cel.BoolType),
)
eval(env,
`tier == "premium" && rush ? "express" : tier == "premium" ? "priority" : "standard"`,
map[string]interface{}{"tier": "premium", "rush": true},
)
// => express
}
// =====================================================================
// VALIDATION -- validate on write, trust on read
// =====================================================================
// Validate CEL expressions once when the user submits them (API write
// path). Use a strict typed env so Compile() catches unknown fields and
// type mismatches. Once stored, the expression is trusted -- use DynType
// on the read path so you don't need the schema again.
// --- Write path: strict schema, reject bad expressions ---
func validateOnWrite(expr string) error {
schema, _ := cel.NewEnv(
cel.Variable("first", cel.StringType),
cel.Variable("last", cel.StringType),
cel.Variable("age", cel.IntType),
)
_, issues := schema.Compile(expr)
if issues != nil && issues.Err() != nil {
return issues.Err()
}
return nil
}
// validateOnWrite(`first + " " + last`) => nil (valid)
// validateOnWrite(`first + " " + email`) => ERROR: undeclared reference to 'email'
// validateOnWrite(`name`) => ERROR: undeclared reference to 'name'
// validateOnWrite(`first + age`) => ERROR: found no matching overload for '_+_'
// validateOnWrite(`first + string(age)`) => nil (valid -- age cast to string)
// --- Read path: expression already validated, just run it ---
// Use DynType so you don't need the typed schema. Every top-level key
// in the data map becomes a variable automatically.
func envFromData(data map[string]interface{}) *cel.Env {
opts := make([]cel.EnvOption, 0, len(data))
for key := range data {
opts = append(opts, cel.Variable(key, cel.DynType))
}
env, _ := cel.NewEnv(opts...)
return env
}
func evalOnRead(expr string, data map[string]interface{}) (interface{}, error) {
env := envFromData(data)
ast, _ := env.Compile(expr)
prg, _ := env.Program(ast)
out, _, err := prg.Eval(data)
if err != nil {
return nil, err
}
return out.Value(), nil
}
// Usage:
// // Expression was validated and stored earlier. Now evaluate it:
// result, err := evalOnRead(`first + " " + last`, map[string]interface{}{
// "first": "Alice",
// "last": "Smith",
// })
// // result: "Alice Smith", err: nil
// You can also build the write-path schema dynamically from a config:
func schemaFromFields(fields map[string]*cel.Type) *cel.Env {
opts := make([]cel.EnvOption, 0, len(fields))
for name, typ := range fields {
opts = append(opts, cel.Variable(name, typ))
}
env, _ := cel.NewEnv(opts...)
return env
}
// Usage:
// schema := schemaFromFields(map[string]*cel.Type{
// "first": cel.StringType,
// "last": cel.StringType,
// "age": cel.IntType,
// "tags": cel.ListType(cel.StringType),
// })
// err := validateOnWrite(`"rush" in tags`) // nil
// err = validateOnWrite(`"rush" in categories`) // error: undeclared reference to 'categories'
// =====================================================================
// GOTCHAS
// =====================================================================
// 1. No assignments -- each expression produces ONE value.
// 2. int * double -- type error. Cast: double(myInt) * myDouble.
// 3. No join() -- get a list with map(), join outside CEL.
// 4. No reduce/sum -- can't fold a list to a number. Do math outside.
// 5. "x" in map -- checks KEYS not values.
// 6. Regex is RE2 -- no backreferences or lookahead.