Documentation
¶
Overview ¶
Package celsius is a CEL-driven rule engine for runtime decisions.
Celsius compiles a YAML rule file at startup, hot-reloads it on change, and exposes a typed Engine whose Engine.Match method evaluates a named rule group against a map of inputs. Each rule's payload is decoded into a user-supplied generic parameter T.
Transport adapters (net/http, Gin, Fiber) live in separate sub-packages so the core has no web-framework dependencies.
Custom environments ¶
Extend DefaultEnv with your own variables and functions via EnvBuilder.WithVariable / EnvBuilder.WithFunction. The same EnvBuilder should be passed to both New (in your server) and to clikit.Run (in a thin validation CLI) so the CLI accepts exactly the identifiers your production engine accepts. See docs/custom-env.md and the runnable example at examples/customenv.
Validation tooling ¶
ValidateBytes / ValidateFile check a rules file against an EnvBuilder — drop them in a Go test, or use the shipped CLI at cmd/celsius. EvalExpr compiles and runs a single expression against an EnvBuilder and is the recommended unit-test entry point for custom CEL functions.
See https://github.com/amkarkhi/celsius for the complete README and design document.
Index ¶
- Variables
- func EvalExpr(env *EnvBuilder, expr string, inputs map[string]any) (any, error)
- func MatchOnce(data []byte, group string, inputs map[string]any, opts ValidateOptions) (tag string, out map[string]any, matched bool, err error)
- func WithResult[T any](ctx context.Context, rule *Rule[T]) context.Context
- type Config
- type Ctx
- type Engine
- type EnvBuilder
- type EnvOpt
- type FunctionOpt
- type Input
- type Issue
- type Rule
- type Source
- type Type
- type ValidateOptions
Constants ¶
This section is empty.
Variables ¶
var ErrClosed = errors.New("celsius: engine is closed")
ErrClosed is returned when an operation is performed on a closed engine.
var ErrNoMatch = errors.New("celsius: no rule matched")
ErrNoMatch is returned by Engine.Match when no rule in the group evaluated to true.
var ErrNoRuleGroup = errors.New("celsius: rule group not found")
ErrNoRuleGroup is returned by Engine.Match when the named rule group does not exist in the loaded configuration.
Functions ¶
func EvalExpr ¶
EvalExpr compiles and evaluates a single CEL expression against env. Every key in inputs is auto-declared as cel.DynType if env doesn't already declare it, so callers don't need to mirror their production declarations in tests.
Intended for unit-testing custom functions registered on an EnvBuilder:
env := celsius.DefaultEnv().WithFunction("myhash", myHashFn())
got, err := celsius.EvalExpr(env, `myhash("abc") % 10`, nil)
require.NoError(t, err)
require.EqualValues(t, 7, got)
If env is nil, DefaultEnv is used.
func MatchOnce ¶
func MatchOnce(data []byte, group string, inputs map[string]any, opts ValidateOptions) (tag string, out map[string]any, matched bool, err error)
MatchOnce parses bytes, builds a one-shot engine, and matches the named group against inputs. Variable names in inputs are auto-declared as cel.DynType. It is intended for ad-hoc testing (CLI, REPL); production code should use New + Engine.Match.
The matched rule's Out is returned as a map[string]any. tag is the rule's tag, matched is false when no rule matched.
Types ¶
type Config ¶
type Config struct {
// Source provides the raw rule-file bytes and notifies on change.
// FileSource(path) is the default implementation.
Source Source
// Env declares the variables and functions visible to rule scripts.
// Defaults to DefaultEnv() if nil.
Env *EnvBuilder
// Input parses incoming requests into the variable map. Required.
Input Input
// Logger receives operational logs (compile errors, reload events).
// If nil, a no-op logger is used.
Logger *zerolog.Logger
// AllowMissingFile, when true, lets New succeed even if the rule file
// does not yet exist. The engine starts with zero rules; the watcher
// will pick the file up if it appears later.
AllowMissingFile bool
}
Config is the input to New.
type Ctx ¶
type Ctx interface {
// Header returns the value of the named request header, or defaultVal
// if the header is absent or empty.
Header(key, defaultVal string) string
// Value returns the value associated with key in the underlying
// request context (e.g. a value placed there by upstream middleware),
// or nil if no such value exists.
Value(key any) any
}
Ctx is the abstraction the Input.Parse hook uses to read request data. Transport adapters provide concrete implementations.
func NewHTTPCtx ¶
NewHTTPCtx wraps an *http.Request as a celsius.Ctx.
type Engine ¶
type Engine[T any] struct { // contains filtered or unexported fields }
Engine is the compiled rule set. It is safe for concurrent use; reads are guarded by a sync.RWMutex and hot reloads swap the internal snapshot atomically.
func New ¶
New constructs an engine from the given config. It performs the initial load synchronously; any compile error in the rule file is returned.
If cfg.Source returns os.ErrNotExist and cfg.AllowMissingFile is true, the engine starts with an empty rule set and the watcher (if any) will pick up the file when it appears.
func (*Engine[T]) Close ¶
Close stops the file watcher and releases resources. Subsequent calls to Match return ErrClosed.
func (*Engine[T]) Compile ¶
Compile compiles an ad-hoc script against the engine's environment and returns the AST. Useful for validating user input before storing it.
func (*Engine[T]) Eval ¶
Eval compiles and evaluates an ad-hoc script against inputs. The result type is whatever the script returns.
func (*Engine[T]) Input ¶
Input returns the engine's configured input parser. Transport adapters call this; user code rarely needs it.
func (*Engine[T]) Match ¶
Match evaluates the named rule group against inputs. The first rule whose script returns true is returned. A non-matching rule whose script errors at evaluation time is logged and skipped.
When the rule file declares a top-level `compute:` block, each entry is evaluated once against `inputs` before the rule loop runs and its result is added to the input map under the entry's name. A compute script that errors is logged and treated as nil; rules can guard with `has()` or default checks.
type EnvBuilder ¶
type EnvBuilder struct {
// contains filtered or unexported fields
}
EnvBuilder accumulates CEL environment options. Use NewEnv for an empty environment or DefaultEnv to start with the built-in helper functions (hash, md5, contains, rand, replace, to_str).
func DefaultEnv ¶
func DefaultEnv() *EnvBuilder
DefaultEnv returns an environment pre-populated with the built-in helper functions. It declares no variables — callers add their own.
func (*EnvBuilder) With ¶
func (e *EnvBuilder) With(opts ...EnvOpt) *EnvBuilder
With applies one or more declarations. It returns the receiver so that declarations can be chained.
func (*EnvBuilder) WithFunction ¶
func (e *EnvBuilder) WithFunction(name string, opts ...cel.FunctionOpt) *EnvBuilder
WithFunction is a convenience equivalent to With(Function(name, opts...)).
func (*EnvBuilder) WithVariable ¶
func (e *EnvBuilder) WithVariable(name string, t *cel.Type) *EnvBuilder
WithVariable is a convenience equivalent to With(Variable(name, t)).
type EnvOpt ¶
type EnvOpt func(*EnvBuilder)
EnvOpt is a single declaration applied to an EnvBuilder.
type FunctionOpt ¶
type FunctionOpt = cel.FunctionOpt
Type and FunctionOpt are re-exported from cel-go so users do not need to import cel-go directly for the common case of declaring variables.
type Input ¶
Input parses a request into the variable map a CEL program will see.
Map should return the *current* state of the input — it is called by the engine after Parse to build the variable map. Parse is allowed (and expected) to mutate the receiver.
type Issue ¶
Issue describes a single problem found by ValidateBytes or ValidateFile.
Phase is one of:
- "parse" — the YAML could not be decoded.
- "expand" — a var.NAME expansion error (e.g. cycle).
- "script" — the CEL expression failed to parse or type-check.
func ValidateBytes ¶
func ValidateBytes(data []byte, opts ValidateOptions) ([]Issue, error)
ValidateBytes parses a Celsius YAML config and validates every rule's CEL script. It returns a slice of Issue; an empty slice means the config is valid. The second return value is a hard error (e.g. YAML decode failure) that prevented validation from running.
func ValidateFile ¶
func ValidateFile(path string, opts ValidateOptions) ([]Issue, error)
ValidateFile reads path and runs ValidateBytes on its contents.
type Rule ¶
type Rule[T any] struct { Script string `yaml:"script" mapstructure:"script"` Tag string `yaml:"tag" mapstructure:"tag"` Out T `yaml:"out" mapstructure:"out"` Replace map[string]any `yaml:"replace" mapstructure:"replace"` // contains filtered or unexported fields }
Rule is a single rule. T is the user-defined payload type.
`Replace` declares per-field overlays for `Out`. Each key is a top-level field name in T (matched by its `mapstructure:` tag); each value is a CEL script evaluated at Match time against the same input map the rule script saw, plus any `compute:` results. The evaluated value replaces the static `Out.<key>` for the returned rule.
Two value shapes are accepted (kept for back-compat with rule files that already use the wrapper form):
# Plain script string — preferred for new rules:
replace:
flow: 'call_arbiter(uid).sub_id == "331" ? "hybrid_search_flow" : "es8_search_flow"'
# Wrapper form — equivalent, older convention:
replace:
flow:
type: script
value: 'call_arbiter(uid).sub_id == "331" ? "hybrid_search_flow" : "es8_search_flow"'
A replace script that errors at Match time is logged at debug and the static `Out.<key>` is kept (graceful fallback).
func ResultFrom ¶
ResultFrom retrieves a rule previously stored by WithResult. The second return value is false when no rule was stored.
type Source ¶
type Source interface {
// Read returns the current rule bytes. Returning os.ErrNotExist signals
// that the source is empty; the engine treats that as "no rules" if
// Config.AllowMissingFile is true.
Read() ([]byte, error)
// Watch installs a callback invoked when the source changes. Calling
// Watch more than once replaces the previous callback. A Source that
// does not support change notifications may return nil without
// installing a callback.
Watch(onChange func()) error
// Close releases any resources held by the source.
Close() error
}
Source supplies the rule-file bytes and notifies the engine when they change. Implementations must be safe for concurrent reads.
func FileSource ¶
FileSource reads rules from a file on disk and watches it via fsnotify.
type Type ¶
Type and FunctionOpt are re-exported from cel-go so users do not need to import cel-go directly for the common case of declaring variables.
type ValidateOptions ¶
type ValidateOptions struct {
// Env is the CEL environment to use for compilation. If nil, DefaultEnv()
// is used. The validator will additionally declare every name listed in
// Variables as cel.DynType so unknown identifiers don't fail type-check.
Env *EnvBuilder
// Variables is an optional list of variable names to declare as
// cel.DynType for the purpose of validation. Useful for pipeline
// type-checking when the production code declares concrete types.
Variables []string
// SyntaxOnly, when true, only parses each script (no type-check). This
// catches grammar errors but does not require variable declarations.
// Use this mode for the simplest "is this YAML well-formed and is each
// CEL expression syntactically valid" check.
SyntaxOnly bool
}
ValidateOptions controls validator behavior.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package clikit is the reusable implementation of the `celsius` CLI.
|
Package clikit is the reusable implementation of the `celsius` CLI. |
|
cmd
|
|
|
celsius
command
celsius is the default CLI for validating and exercising Celsius rule files.
|
celsius is the default CLI for validating and exercising Celsius rule files. |
|
examples
|
|
|
basic
command
Basic Celsius example: stdlib net/http + a single rule group.
|
Basic Celsius example: stdlib net/http + a single rule group. |
|
customenv/cli
command
Thin CLI wrapper that gives `celsius` validate/test/eval/repl visibility into this service's custom CEL functions and variables.
|
Thin CLI wrapper that gives `celsius` validate/test/eval/repl visibility into this service's custom CEL functions and variables. |
|
customenv/extensions
Package extensions shows how a downstream service ships its own CEL variables and functions on top of Celsius' DefaultEnv.
|
Package extensions shows how a downstream service ships its own CEL variables and functions on top of Celsius' DefaultEnv. |
|
customenv/server
command
HTTP server example using a custom Celsius env (custom variables + custom CEL functions).
|
HTTP server example using a custom Celsius env (custom variables + custom CEL functions). |
|
fiber
command
Fiber Celsius example.
|
Fiber Celsius example. |
|
gin
command
Gin Celsius example.
|
Gin Celsius example. |
|
internal
|
|
|
celfn
Package celfn contains the implementations of Celsius' default CEL helper functions.
|
Package celfn contains the implementations of Celsius' default CEL helper functions. |
|
varexpand
Package varexpand resolves `var.NAME` references inside rule scripts.
|
Package varexpand resolves `var.NAME` references inside rule scripts. |
|
transport
|
|
|
fiber
Package fiber provides a Fiber v2 middleware adapter for Celsius.
|
Package fiber provides a Fiber v2 middleware adapter for Celsius. |
|
gin
Package gin provides a Gin middleware adapter for Celsius.
|
Package gin provides a Gin middleware adapter for Celsius. |
|
nethttp
Package nethttp provides a net/http middleware adapter for Celsius.
|
Package nethttp provides a net/http middleware adapter for Celsius. |