xcel

package module
v0.0.0-...-885b5f6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2025 License: MPL-2.0 Imports: 9 Imported by: 0

README

xcel

Extend CEL expressions with custom (native Go) objects and functions.

Usage

$ go get github.com/picatz/xcel@latest

Example

type Person struct {
	Name string
	Age  int
}

person := &Person{
	Name: "test",
	Age:  -1,
}

ta, tp := xcel.NewTypeAdapter(), xcel.NewTypeProvider()

obj, typ := xcel.NewObject(person)

xcel.RegisterObject(ta, tp, obj, typ, map[string]*types.FieldType{
	"name": {
		Type: types.StringType,
		IsSet: ref.FieldTester(func(target any) bool {
			x := target.(*xcel.Object[*Person])

			if x.Raw == nil || x.Raw.Name == "" {
				return false
			}

			return true
		}),
		GetFrom: ref.FieldGetter(func(target any) (any, error) {
			x := target.(*xcel.Object[*Person])

			if x.Raw == nil {
				return nil, fmt.Errorf("celval: object is nil")
			}

			return x.Raw.Name, nil
		}),
	},
	"age": {
		Type: types.IntType,
		IsSet: ref.FieldTester(func(target any) bool {
			x := target.(*xcel.Object[*Person])

			if x.Raw == nil || x.Raw.Age < 0 {
				return false
			}

			return true
		}),
		GetFrom: ref.FieldGetter(func(target any) (any, error) {
			x := target.(*xcel.Object[*Person])

			if x.Raw == nil {
				return nil, fmt.Errorf("celval: object is nil")
			}

			return x.Raw.Age, nil
		}),
	},
})

env, _ := cel.NewEnv(
    cel.Types(typ),
    cel.Variable("obj", typ),
    cel.CustomTypeAdapter(ta),
    cel.CustomTypeProvider(tp),
)

ast, _ := env.Compile("obj.name == 'test' && obj.age > 0")

prg, _ := env.Program(ast)

out, _, _ := prg.Eval(map[string]any{
    "obj": obj,
})

fmt.Println(out.Value())
// Output: false
Object Fields via Reflection

Fields can be registered via reflection, which is a bit more concise, but less flexible and less performant:

type Person struct {
	Name string
	Age  int
}

person := &Person{
	Name: "test",
	Age:  -1,
}

ta, tp := xcel.NewTypeAdapter(), xcel.NewTypeProvider()

obj, typ := xcel.NewObject(person)

xcel.RegisterObject(ta, tp, obj, typ, xcel.NewFields(obj))
Benchmarks

Showing some minimal performance differences between manual fields and reflection based fields for the same object:

ex := &Example{
	Name: "test",
	Age:  1,
	Tags: []string{"test"},
	Parent: &Example{
		Name: "root",
		Age:  -1,
		Tags: []string{"a", "b", "c"},
	},
	Pressure: 1.5,
	Fn: func(i int) string {
		return fmt.Sprintf("~%d~", i)
	},
	Blob: []byte("test"),
}
obj.name == 'test' && obj.age > 0 && ('test' in obj.tags) && obj.parent.name == 'root' && obj.pressure > 1.0 && obj.fn(1) == '~1~' && has(obj.blob)
$ go test -benchmem -run=^$ -bench ^BenchmarkNewObject github.com/picatz/xcel -v
goos: darwin
goarch: arm64
pkg: github.com/picatz/xcel
BenchmarkNewObjectManualFields
BenchmarkNewObjectManualFields-8          677050              1620 ns/op             784 B/op         22 allocs/op
BenchmarkNewObjectReflectionFields
BenchmarkNewObjectReflectionFields-8      546022              2138 ns/op             880 B/op         31 allocs/op

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var DefaultTypeProvider = &TypeProvider{
	Idents:           map[string]ref.Val{},
	Types:            map[string]*types.Type{},
	Structs:          map[string]map[string]*types.FieldType{},
	StructFieldTypes: map[string]map[string]*types.FieldType{},
}

Functions

func NewFields

func NewFields[T any](objt *Object[T]) map[string]*types.FieldType

NewFields returns CEL field metadata for the immediate fields of objt.

func RegisterIdent

func RegisterIdent(tp *TypeProvider, name string, value ref.Val)

func RegisterObject

func RegisterObject[T any](ta TypeAdapter, tp *TypeProvider, objt *Object[T], t *types.Type, fields map[string]*types.FieldType)

RegisterObject registers objt and its type with the given adapter and provider. It derives field metadata from reflection (optionally overlaid by fields), registers the struct type, and registers reachable named nested struct types so nested field access type-checks at compile time.

func RegisterStructType

func RegisterStructType(tp *TypeProvider, name string, fields map[string]*types.FieldType)

func RegisterType

func RegisterType(tp *TypeProvider, t *types.Type)

Types

type Object

type Object[T any] struct {
	Raw T
}

Object wraps a Go value for use as a CEL object. The wrapper type is used as the CEL object type so member functions dispatch to the wrapper.

func NewObject

func NewObject[T any](val T) (*Object[T], *types.Type)

NewObject returns a CEL wrapper for val and its CEL object type.

Example
package main

import (
	"fmt"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/common/types"
	"github.com/picatz/xcel"
)

func main() {
	type Person struct {
		Name string
		Age  int
	}

	person := &Person{
		Name: "test",
		Age:  -1,
	}

	ta, tp := xcel.NewTypeAdapter(), xcel.NewTypeProvider()

	obj, typ := xcel.NewObject(person)

	xcel.RegisterObject(ta, tp, obj, typ, map[string]*types.FieldType{
		"name": {
			Type: types.StringType,
			IsSet: func(target any) bool {
				x := target.(*xcel.Object[*Person])

				return x.Raw != nil && x.Raw.Name != ""
			},
			GetFrom: func(target any) (any, error) {
				x := target.(*xcel.Object[*Person])

				if x.Raw == nil {
					return nil, fmt.Errorf("celval: object is nil")
				}

				return x.Raw.Name, nil
			},
		},
		"age": {
			Type: types.IntType,
			IsSet: func(target any) bool {
				x := target.(*xcel.Object[*Person])

				return x.Raw != nil && x.Raw.Age >= 0
			},
			GetFrom: func(target any) (any, error) {
				x := target.(*xcel.Object[*Person])

				if x.Raw == nil {
					return nil, fmt.Errorf("celval: object is nil")
				}

				return x.Raw.Age, nil
			},
		},
	})

	env, _ := cel.NewEnv(
		cel.Types(typ),
		cel.Variable("obj", typ),
		cel.CustomTypeAdapter(ta),
		cel.CustomTypeProvider(tp),
	)

	ast, _ := env.Compile("obj.name == 'test' && obj.age > 0")

	prg, _ := env.Program(ast)

	out, _, _ := prg.Eval(map[string]any{
		"obj": obj,
	})

	fmt.Println(out.Value())
}
Output:

false

func (*Object[T]) ConvertToNative

func (o *Object[T]) ConvertToNative(typeDesc reflect.Type) (any, error)

ConvertToNative returns the underlying Go value when typeDesc matches the wrapped type.

func (*Object[T]) ConvertToType

func (o *Object[T]) ConvertToType(typeValue ref.Type) ref.Val

ConvertToType implements ref.Val.ConvertToType for the wrapper.

func (*Object[T]) Equal

func (o *Object[T]) Equal(other ref.Val) ref.Val

Equal reports whether other is an *Object[T] with an equal underlying value.

func (*Object[T]) Type

func (o *Object[T]) Type() ref.Type

Type returns the CEL type of the wrapper.

func (*Object[T]) Value

func (o *Object[T]) Value() any

Value returns the wrapper itself. Adapters handle unwrapping when needed.

type TypeAdapter

type TypeAdapter map[reflect.Type]func(value any) ref.Val

func NewTypeAdapter

func NewTypeAdapter() TypeAdapter

func (TypeAdapter) NativeToValue

func (ta TypeAdapter) NativeToValue(value any) ref.Val

type TypeProvider

type TypeProvider struct {
	Idents           map[string]ref.Val
	Types            map[string]*types.Type
	Structs          map[string]map[string]*types.FieldType
	StructFieldTypes map[string]map[string]*types.FieldType
}

func NewTypeProvider

func NewTypeProvider() *TypeProvider

func (TypeProvider) EnumValue

func (TypeProvider) EnumValue(enumName string) ref.Val

func (*TypeProvider) FindIdent

func (tp *TypeProvider) FindIdent(identName string) (ref.Val, bool)

func (*TypeProvider) FindStructFieldNames

func (tp *TypeProvider) FindStructFieldNames(structType string) ([]string, bool)

func (*TypeProvider) FindStructFieldType

func (tp *TypeProvider) FindStructFieldType(messageType, fieldName string) (*types.FieldType, bool)

func (*TypeProvider) FindStructType

func (tp *TypeProvider) FindStructType(structType string) (*types.Type, bool)

func (TypeProvider) NewValue

func (TypeProvider) NewValue(typeName string, fields map[string]ref.Val) ref.Val

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL