rulekit

package module
v2.0.0-...-cf22d6f Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 16 Imported by: 0

README

Rulekit icon

Rulekit

Rulekit is a flexible expression-based rules engine for Go, providing a simple and expressive syntax for defining business rules that can be evaluated against key-value data.

Rulekit Demo

Overview

This package implements an expression-based rules engine that evaluates expressions against a key-value map of values, returning a true/false result with additional context.

Rules follow a simple and intuitive syntax. For example, the following rule:

domain matches /example\.com$/

When evaluated against:

  • map[string]any{"domain": "example.com"} → returns true
  • map[string]any{"domain": "qpoint.io"} → returns false

In this document, domain is referred to as a field and /example\.com$/ as a value.

Rulekit supports a flexible syntax where fields and values may appear on either side of an operator:

  • field operator value (e.g., domain == "example.com")
  • value operator field (e.g., "example.com" == domain)
  • value operator value (e.g., 123 == 123)
  • field operator field (e.g., src.port == dst.port)

A field on its own (without an operator) will check if the field contains a non-zero value. For example: hash && version > 1 will check if the hash field is non-zero and the version is greater than 1.

Usage Example

import "github.com/qpoint-io/rulekit"

// ...

r, err := rulekit.Parse(`domain matches /example\.com$/ and port == 8080`)
if err != nil { /* ... */ }

// define input data
input := rulekit.KV{
    "domain": "example.com",
    "port": 8080,
}

// evaluate the rule
result := r.Eval(context.Background(), rulekit.FromKV(input), rulekit.Opts{})

// check for errors, missing input, then the rule result
if result.Error != nil {
    fmt.Printf("error evaluating rule: %v\n", result.Error)
} else if result.Unknown() {
    fmt.Printf("missing fields: %v\n", result.MissingFields)
} else if result.Pass() {
    fmt.Println("PASS!")
} else {
    fmt.Println("FAIL :(")
}

Result

When a rule is evaluated, it returns a Result struct containing:

  • Value: The evaluated value, usually a boolean
  • Error: Any operational evaluation error
  • MissingFields: Fields required to complete evaluation but absent from the input
  • Trace: Optional evaluation explanation when Opts.Trace is enabled

The Result also provides additional helper methods:

  • Pass(): Returns true if the rule completed and returned true/a non-zero value
  • Fail(): Returns true if the rule completed and returned false/a zero value
  • Ok() / Complete(): Returns true if the rule completed with no error or missing fields
  • Unknown(): Returns true if the rule needs more input but did not otherwise fail

Supported Operators

Operator Alias Description
or || Logical OR
and && Logical AND
not ! Logical NOT
() Parentheses for grouping
== eq Equal to
!= ne Not equal to
> gt Greater than
>= ge Greater than or equal to
< lt Less than
<= le Less than or equal to
contains Check if a value contains another value
in Check if a value is contained within an array or an IP within a CIDR
matches Match against a regular expression

Supported Types

Basic values
Type Used As Example Description
bool VALUE, FIELD true Valid values: true, false
number VALUE, FIELD 8080 Integer or float. Parsed as either int64 or uint64 if out of range for int64, or float64 if float.
string VALUE, FIELD "domain.com" A double-quoted string. Quotes may be escaped with a backslash: "a string \"with\" quotes". Any quoted value is parsed as a string.
IP address VALUE, FIELD 192.168.1.1, 2001:db8:3333:4444:cccc:dddd:eeee:ffff An IPv4, IPv6, or an IPv6 dual address. Maps to Go type: net.IP
CIDR VALUE 192.168.1.0/24, 2001:db8:3333:4444:cccc:dddd:eeee:ffff/64 An IPv4 or IPv6 CIDR block. Maps to Go type: *net.IPNet
Hexadecimal string VALUE, FIELD 12:34:56:78:ab (MAC address), 504f5354 (hex string "POST") A hexadecimal string, optionally separated by colons.
Regex VALUE /example\.com$/ A Go-style regular expression. Must be surrounded by forward slashes. May not be quoted with double quotes (otherwise it will be parsed as a string). Maps to Go type: *regexp.Regexp
Constructs
Type Used As Example Description
Array VALUE [1, "string", true] An array of mixed value types. Can be used with most operators including in and contains.
Function VALUE starts_with(url, "https://") A function call with optional arguments. Can be built-in or custom.
Macro VALUE isValidRequest() A zero-argument function that encapsulates a predefined rule.
Path Access

Dot syntax traverses nested maps and objects:

destination.ip == 192.168.1.1

Use bracket syntax for exact map keys that contain dots, spaces, slashes, reserved words, or other punctuation:

labels["app.kubernetes.io/name"] == "api"
request.headers["user-agent"] == "curl"
["destination.ip"] == 192.168.1.1
items[0].name == "first"

Plain dotted fields do not fall back to flat keys. If the input contains a top-level key named destination.ip, use ["destination.ip"].

JSON Input Helpers

DecodeJSON converts JSON documents into rulekit.KV. Plain JSON decodes dynamically by default. Annotated key suffixes are opt-in and are intended for values that JSON cannot represent natively:

kv, err := rulekit.DecodeJSON(data, rulekit.JSONOptions{AnnotatedKeys: true})

Supported suffixes include .$ip, .$cidr, .$mac, .$hex, .$base64, .$bytes_hex, .$bytes_base64, .$string, .$bool, .$int64, .$uint64, and .$float64.

Fully typed documents are a separate mode. In this mode, every field value must be a typed object and annotated keys are rejected:

kv, err := rulekit.DecodeJSON(data, rulekit.JSONOptions{TypedDocument: true})
{
  "src": { "$type": "ip", "value": "1.2.3.4" },
  "payload": { "$type": "bytes", "encoding": "hex", "value": "474554" },
  "u64": { "$type": "uint64", "value": "18446744073709551615" }
}
AST API

Use ParseAST when tools need to inspect expression structure before compiling to an evaluator rule:

ast, err := rulekit.ParseAST(`request.headers["user-agent"] == "curl"`)
if err != nil { /* ... */ }

fmt.Println(ast.String()) // compact canonical expression
rule, err := rulekit.Compile(ast)

The public AST view is read-only. Build edited expressions by parsing replacement source and compiling the resulting AST.

AST.Tokens() returns the token stream with byte spans plus leading and trailing whitespace/comment trivia for source-aware tools.

Use Print and Format for explicit output modes:

source := rule.Print(rulekit.Source())
compact := rule.Print(rulekit.Compact())
multiline := rule.Print(rulekit.Multiline("  "))

formattedAST := rulekit.Format(ast, rulekit.Multiline("  "))

Use Rewrite to preserve unchanged source while replacing selected AST nodes:

updated, err := rulekit.Rewrite(ast, []rulekit.Edit{{Target: node, Replacement: replacementAST}}, rulekit.Compact())

Enable evaluation traces when a caller needs short-circuit visibility for debugging or UI explanation:

result := rule.Eval(context.Background(), rulekit.FromKV(kv), rulekit.Opts{Trace: true})
trace := result.Trace

Each trace node includes the expression, value, error, missing fields, and a Status of passed, failed, missing, error, pruned, or unknown. Short-circuited branches are marked as pruned.

Use Input adapters for lazy values or custom path resolution:

input := rulekit.FromKV(rulekit.KV{
    "user": rulekit.LazyContextValue(func(ctx context.Context) (any, error) {
        return ctx.Value("user"), nil
    }),
})

result := rule.Eval(ctx, input, rulekit.Opts{})

Nested Input values inside a KV can take over resolution for an entire subtree.

Macros

Macros can be used for complex or commonly-used rules. They are defined in the evaluation context:

// create macros
macros := rulekit.MacroSet{}
err := macros.Register("isInternalAPI", `domain matches /\.internal\.example\.com$/ or ip in 10.0.0.0/8`)
if err != nil { /* ... */ }

// create a rule that uses the macro
rule, err := rulekit.Parse(`isInternalAPI() && user != "root"`)
if err != nil { /* ... */ }

// evaluate the rule, making sure to pass the macro in eval opts
input := rulekit.FromKV(rulekit.KV{
		"user": user,
		// ...
})
result := rule.Eval(context.Background(), input, rulekit.Opts{Macros: macros})

When tracing is enabled, a macro call appears as its own trace node with the expanded macro expression as a child.

Functions

Functions can be called inside rules and used as value objects. Functions may accept zero or more arguments.

Standard library

Rulekit comes with a built-in standard library of functions:

Function Description Example
starts_with(value, prefix) Checks if a value starts with the given prefix. Works with strings, numbers, and other types by converting them to strings. starts_with(url, "https://")
Custom Functions

Custom functions may be used to extend Rulekit with additional functionality. Note that functions only have access to their arguments and do not have access to the context KV map. Rulekit will validate the function's arguments per the provided spec before executing the handler.

// define a custom function
customFuncs := map[string]*rulekit.Function{
    "randomInt": {
        Args: []rulekit.FunctionArg{
            {Name: "min"},
            {Name: "max"},
        },
        Eval: func(args map[string]any) rulekit.Result {
            // use the rulekit.IndexFuncArg helper to retrieve args and validate types.
            // rulekit.IndexFuncArg[any] will skip type validation.
            min, err := rulekit.IndexFuncArg[int64](args, "min")
            if err != nil {
                return rulekit.Result{Error: err}
            }

            max, err := rulekit.IndexFuncArg[int64](args, "max")
            if err != nil {
                return rulekit.Result{Error: err}
            }

			num := rand.IntN(max-min) + min
			return rulekit.Result{
                Value: num,
            }
        },
    },
}

// call the function in a rule
rule, err := rulekit.Parse(`randomInt(10, 20) == 15`)
if err != nil { /* ... */ }

result := rule.Eval(context.Background(), nil, rulekit.Opts{Functions: customFuncs})
if result.Error != nil { /* ... */ }

if result.Pass() {
    // the random number is 15!
}

License

MIT

Image showing \

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidOperation = errors.New("invalid operation")
View Source
var StdlibFuncs = map[string]*Function{
	"starts_with": {
		Args: []FunctionArg{
			{Name: "value"},
			{Name: "prefix"},
		},
		Eval: func(args map[string]any) Result {
			value, err := IndexFuncArg[any](args, "value")
			if err != nil {
				return Result{Error: err}
			}
			prefix, err := IndexFuncArg[any](args, "prefix")
			if err != nil {
				return Result{Error: err}
			}

			return Result{
				Value: strings.HasPrefix(fmt.Sprint(value), fmt.Sprint(prefix)),
			}
		},
	},
}

Functions

func Format

func Format(ast *AST, mode PrintMode) string

Format prints an AST using an explicit print mode.

func IndexFuncArg

func IndexFuncArg[T any](args map[string]any, name string) (T, error)

func IndexKV

func IndexKV(m KV, key string) (any, bool)

IndexKV gets a value from a map by interpreting periods as explicit path traversal.

func NodeCallName

func NodeCallName(node ASTNode) (string, bool)

NodeCallName returns the function or macro name for call nodes.

func NodeLiteral

func NodeLiteral(node ASTNode) (raw string, ok bool)

NodeLiteral returns the raw literal token for literal nodes.

func NodeRawOperator

func NodeRawOperator(node ASTNode) string

NodeRawOperator returns the source operator spelling for unary and binary nodes.

func Rewrite

func Rewrite(ast *AST, edits []Edit, mode PrintMode) (string, error)

Rewrite applies AST-node replacements while preserving unchanged source text.

func SetErrorVerbose

func SetErrorVerbose(bool)

SetErrorVerbose is retained for API compatibility. The hand-written parser always returns verbose ParseError values.

Types

type AST

type AST struct {
	// contains filtered or unexported fields
}

AST is a parsed expression tree. The current public AST is immutable; callers that edit expressions should parse, inspect, and compile replacement trees.

func ParseAST

func ParseAST(expr string) (*AST, error)

ParseAST parses an expression and returns its editable-source AST boundary.

func (*AST) Root

func (a *AST) Root() ASTNode

Root returns the root expression node.

func (*AST) Source

func (a *AST) Source() string

Source returns the expression text used to build the AST.

func (*AST) String

func (a *AST) String() string

String returns the compact canonical AST expression.

func (*AST) Tokens

func (a *AST) Tokens() []Token

Tokens returns the parsed token stream, including the EOF token.

type ASTKind

type ASTKind int

ASTKind identifies the shape of an AST node.

const (
	ASTUnknown ASTKind = iota
	ASTLiteral
	ASTPath
	ASTArray
	ASTCall
	ASTUnary
	ASTBinary
)

type ASTNode

type ASTNode interface {
	Kind() ASTKind
	Span() Span
	String() string
	Children() []ASTNode
}

ASTNode is the read-only public view of a parsed expression node.

type ArrayValue

type ArrayValue struct {
	// contains filtered or unexported fields
}

func (*ArrayValue) Eval

func (a *ArrayValue) Eval(ctx context.Context, input Input, opts Opts) Result

func (*ArrayValue) Print

func (a *ArrayValue) Print(PrintMode) string

func (*ArrayValue) String

func (a *ArrayValue) String() string

type Diagnostic

type Diagnostic struct {
	Code      DiagnosticCode
	Message   string
	LeftType  string
	Operator  string
	RightType string
}

Diagnostic provides optional evaluation details for trace consumers.

type DiagnosticCode

type DiagnosticCode string
const (
	DiagnosticComparisonIncomparable        DiagnosticCode = "comparison_incomparable"
	DiagnosticComparisonInvalidShape        DiagnosticCode = "comparison_invalid_shape"
	DiagnosticComparisonUnsupportedOperator DiagnosticCode = "comparison_unsupported_operator"
)

type Edit

type Edit struct {
	Target      ASTNode
	Replacement *AST
}

Edit replaces one AST node with another AST.

type ErrInvalidFunctionArg

type ErrInvalidFunctionArg struct {
	Name     string
	Expected string
	Got      string
}

func (*ErrInvalidFunctionArg) Error

func (e *ErrInvalidFunctionArg) Error() string

type FieldValue

type FieldValue string

func (FieldValue) Eval

func (f FieldValue) Eval(ctx context.Context, input Input, opts Opts) Result

func (FieldValue) Print

func (f FieldValue) Print(PrintMode) string

func (FieldValue) String

func (f FieldValue) String() string

type Function

type Function struct {
	// Args is an optional list of arguments that the function expects.
	// If set, rulekit will ensure validity of the arguments and pass them as a named map to the Eval function.
	Args []FunctionArg
	// Eval is the function that will be called with the arguments.
	Eval func(map[string]any) Result
}

type FunctionArg

type FunctionArg struct {
	Name string
}

type FunctionValue

type FunctionValue struct {
	// contains filtered or unexported fields
}

func (*FunctionValue) Eval

func (f *FunctionValue) Eval(ctx context.Context, input Input, opts Opts) Result

func (*FunctionValue) Print

func (f *FunctionValue) Print(PrintMode) string

func (*FunctionValue) String

func (f *FunctionValue) String() string

func (*FunctionValue) ValidateStdlibFnArgs

func (f *FunctionValue) ValidateStdlibFnArgs() error

type HexString

type HexString struct {
	Bytes []byte
	// contains filtered or unexported fields
}

HexString represents a hex-encoded string retaining the original input string

func ParseHexString

func ParseHexString(s string) (HexString, error)

func (HexString) String

func (h HexString) String() string

type Input

type Input interface {
	Get(context.Context, []PathSegment) (any, bool, error)
}

Input resolves rule paths against an evaluation input source.

func FromContextFunc

func FromContextFunc(fn func(context.Context, []PathSegment) (any, bool, error)) Input

func FromFunc

func FromFunc(fn func([]PathSegment) (any, bool, error)) Input

func FromKV

func FromKV(kv KV) Input

type JSONOptions

type JSONOptions struct {
	// AnnotatedKeys enables suffix-based type hints such as "src.$ip".
	AnnotatedKeys bool
	// TypedDocument requires every JSON field value to use a typed object such as
	// {"$type":"ip","value":"1.2.3.4"}. It cannot be combined with AnnotatedKeys.
	TypedDocument bool
}

type KV

type KV = map[string]any

func DecodeJSON

func DecodeJSON(data []byte, opts JSONOptions) (KV, error)

DecodeJSON decodes a JSON object into a Rulekit KV value map.

type LazyContextValue

type LazyContextValue func(context.Context) (any, error)

type LazyValue

type LazyValue func() (any, error)

type LiteralValue

type LiteralValue[T any] struct {
	// contains filtered or unexported fields
}

func (*LiteralValue[T]) Eval

func (l *LiteralValue[T]) Eval(ctx context.Context, input Input, opts Opts) Result

func (*LiteralValue[T]) Print

func (l *LiteralValue[T]) Print(PrintMode) string

func (*LiteralValue[T]) String

func (l *LiteralValue[T]) String() string

type Macro

type Macro struct {
	Source string
	AST    *AST
	Rule   Rule
	Doc    string
}

Macro is a named zero-argument rule expression that can be reused by calls.

func MustMacro

func MustMacro(source string) *Macro

func NewMacro

func NewMacro(source string) (*Macro, error)

type MacroSet

type MacroSet map[string]*Macro

func (*MacroSet) Register

func (m *MacroSet) Register(name string, source string) error

type Operator

type Operator int

Operator identifies normalized unary and binary operators.

const (
	OperatorUnknown Operator = iota
	OperatorNot
	OperatorAnd
	OperatorOr
	OperatorEQ
	OperatorNE
	OperatorGT
	OperatorGE
	OperatorLT
	OperatorLE
	OperatorContains
	OperatorMatches
	OperatorIn
)

func NodeOperator

func NodeOperator(node ASTNode) Operator

NodeOperator returns a normalized operator for unary and binary nodes.

type Opts

type Opts struct {
	Trace     bool
	Macros    MacroSet
	Functions map[string]*Function
}

func (Opts) Validate

func (o Opts) Validate() error

type ParseError

type ParseError struct {
	Line       int
	Column     int
	Message    string
	Input      string
	Suggestion string
}

func (*ParseError) Error

func (e *ParseError) Error() string

type PathSegment

type PathSegment struct {
	Key     string
	Index   int
	IsIndex bool
	Bracket bool
}

PathSegment is a public read-only path segment value.

func NodePath

func NodePath(node ASTNode) ([]PathSegment, bool)

NodePath returns path segments for path nodes.

type PathValue

type PathValue struct {
	// contains filtered or unexported fields
}

PathValue evaluates an explicit map/slice path, including bracket key and numeric index segments.

func (*PathValue) Eval

func (p *PathValue) Eval(ctx context.Context, input Input, opts Opts) Result

func (*PathValue) Print

func (p *PathValue) Print(PrintMode) string

func (*PathValue) String

func (p *PathValue) String() string

type PrintMode

type PrintMode interface {
	// contains filtered or unexported methods
}

PrintMode selects how a rule or AST should be printed.

func Compact

func Compact() PrintMode

Compact prints compact canonical expression output.

func Multiline

func Multiline(indent string) PrintMode

Multiline prints canonical multiline expression output with the given indent.

func Source

func Source() PrintMode

Source prints the original expression bytes when they are available.

type Result

type Result struct {
	Value         any
	Error         error
	MissingFields []string
	Trace         *Trace
}

func (Result) Complete

func (r Result) Complete() bool

Complete returns true if the rule evaluated without errors or missing fields.

func (Result) Fail

func (r Result) Fail() bool

Fail returns true if the rule is ok and returns a zero value. This is usually used for boolean rules.

func (Result) Ok

func (r Result) Ok() bool

Ok returns true if the rule evaluated completely without errors or missing fields.

func (Result) Pass

func (r Result) Pass() bool

Pass returns true if the result is ok with a non-zero value. This is usually used for boolean rules.

func (Result) Unknown

func (r Result) Unknown() bool

Unknown returns true if evaluation needs more input but did not otherwise fail.

type Rule

type Rule interface {
	// Evaluates the rule with the context and input.
	Eval(context.Context, Input, Opts) Result
	// Print prints the rule with the requested mode.
	Print(PrintMode) string
	// String representation of the rule
	String() string
}

func Compile

func Compile(ast *AST) (Rule, error)

Compile lowers a parsed AST to the evaluator Rule representation.

func MustParse

func MustParse(str string) Rule

func Parse

func Parse(str string) (Rule, error)

Parse parses a rule expression and returns a Rule.

type RuleFunc

type RuleFunc func(context.Context, Input, Opts) Result

func (RuleFunc) Eval

func (f RuleFunc) Eval(ctx context.Context, input Input, opts Opts) Result

func (RuleFunc) Print

func (f RuleFunc) Print(PrintMode) string

func (RuleFunc) String

func (f RuleFunc) String() string

type Span

type Span struct {
	Start int
	End   int
}

Span identifies a byte range in the original expression.

type Token

type Token struct {
	Kind           string
	Raw            string
	Span           Span
	LeadingTrivia  string
	TrailingTrivia string
}

Token is a lossless lexical token with attached trivia.

type Trace

type Trace struct {
	Node          ASTNode
	Expr          string
	Value         any
	Error         error
	MissingFields []string
	Diagnostics   []Diagnostic
	Status        TraceStatus
	Active        bool
	Pruned        bool
	Children      []*Trace
}

Trace explains how a rule evaluation reached its result.

type TraceStatus

type TraceStatus string
const (
	TraceUnknown TraceStatus = "unknown"
	TracePassed  TraceStatus = "passed"
	TraceFailed  TraceStatus = "failed"
	TraceMissing TraceStatus = "missing"
	TraceError   TraceStatus = "error"
	TracePruned  TraceStatus = "pruned"
)

type ValueParseError

type ValueParseError struct {
	TokenType int
	Value     string
	Err       error
}

func (ValueParseError) Error

func (e ValueParseError) Error() string

Directories

Path Synopsis
demo
wasm command

Jump to

Keyboard shortcuts

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