Skip to content

Instantly share code, notes, and snippets.

@silesky
Last active March 5, 2026 21:10
Show Gist options
  • Select an option

  • Save silesky/04ef24b72d18d8e51737483487ff53a1 to your computer and use it in GitHub Desktop.

Select an option

Save silesky/04ef24b72d18d8e51737483487ff53a1 to your computer and use it in GitHub Desktop.
CEL Cheat Sheet -- Object Mapping (Go + JS)

CEL Cheat Sheet -- Object Mapping

CEL expressions for mapping fields from input objects to output fields.


cel_mapping.go

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment