Skip to content

lugingf/decidr

Repository files navigation

decidr

decidr is a lightweight Go rule engine for evaluating boolean rules over string values.

Supported operators:

  • equals
  • exists
  • and
  • or
  • not
  • always

Quick Start

package main

import (
	"fmt"

	"github.com/lugingf/decidr"
)

func main() {
	rules := []decidr.Rule{
		{
			ID:       "corp-ios",
			Priority: 10,
			Condition: decidr.RawCondition{
				Operator: decidr.OperatorAND,
				Conditions: []decidr.RawCondition{
					{Operator: decidr.OperatorEquals, Field: "source", Value: "intune"},
					{Operator: decidr.OperatorEquals, Field: "platform", Value: "ios"},
				},
			},
			Payload: map[string]string{"decision": "allow"},
		},
		{
			ID:       "fallback",
			Priority: 100,
			Condition: decidr.RawCondition{
				Operator: decidr.OperatorAlways,
			},
			Payload: map[string]string{"decision": "deny"},
		},
	}

	engine, err := decidr.NewEngine(rules)
	if err != nil {
		panic(err)
	}

	match, ok := engine.EvaluateFirst(decidr.Context{
		"source":   "intune",
		"platform": "ios",
	})
	if !ok {
		fmt.Println("no match")
		return
	}

	fmt.Println(match.Rule.ID, match.Rule.Payload["decision"])
}

Debug Logging with slog

You can pass a custom logger from outside and control verbosity with handler level/options.

var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
	Level: slog.LevelDebug,
}))

engine, err := decidr.NewEngine(rules, decidr.WithLogger(logger))
if err != nil {
	panic(err)
}

_, _ = engine.EvaluateFirst(decidr.Context{"source": "intune"})
fmt.Println(buf.String()) // debug trace for build/evaluation

If WithLogger is not provided, decidr uses a no-op logger by default.

Rule Structure

type Rule struct {
	ID        string
	Priority  int
	Condition RawCondition
	Payload   map[string]string
}
  • Priority: lower value means higher priority.
  • Payload: arbitrary metadata your application can use after a match.

Condition Format

type RawCondition struct {
	Operator   string
	Field      string
	Value      string
	Conditions []RawCondition
	Condition  *RawCondition
}

Examples

equals:

{"operator":"equals","field":"source","value":"intune"}

and:

{
  "operator":"and",
  "conditions":[
    {"operator":"equals","field":"source","value":"intune"},
    {"operator":"equals","field":"platform","value":"ios"}
  ]
}

or:

{
  "operator":"or",
  "conditions":[
    {"operator":"equals","field":"platform","value":"ios"},
    {"operator":"equals","field":"platform","value":"android"}
  ]
}

not:

{
  "operator":"not",
  "condition":{"operator":"equals","field":"status","value":"blocked"}
}

always:

{"operator":"always"}

exists:

{"operator":"exists","field":"tenant"}

Parsing Expression Strings

You can build RawCondition from an expression string:

expr := `((source == "intune" AND platform == "ios") OR tenant == "*") AND NOT status == "blocked"`
cond, err := decidr.ParseConditionString(expr)

Supported expression syntax:

  • parentheses
  • AND, OR, NOT
  • ALWAYS
  • EXISTS(field)
  • ==
  • double-quoted string literals

Building Rule from Strings (Recommended)

buildRule := func(id string, priority int, expr string, payload map[string]string) (decidr.Rule, error) {
	cond, err := decidr.ParseConditionString(expr)
	if err != nil {
		return decidr.Rule{}, err
	}

	return decidr.Rule{
		ID:        id,
		Priority:  priority,
		Condition: cond,
		Payload:   payload,
	}, nil
}

allowRule, err := buildRule(
	"allow-mobile",
	10,
	`(source == "intune" AND (platform == "ios" OR platform == "android")) AND NOT status == "blocked"`,
	map[string]string{"decision": "allow"},
)
if err != nil {
	panic(err)
}

fallbackRule, err := buildRule("fallback", 100, `ALWAYS`, map[string]string{"decision": "deny"})
if err != nil {
	panic(err)
}

Invalid expression syntax returns a parser error (for example: source = "intune").

Field lookup in Context is case-insensitive.

Validation and Evaluation

  • Validation: decidr.ValidateCondition, decidr.ValidateRule
  • Compilation: decidr.CompileCondition
  • Evaluate one condition: decidr.EvaluateCondition
  • First match by priority: engine.EvaluateFirst
  • All matches by priority: engine.EvaluateAll

Rule Storage

The package does not store rules itself by design.

Storage is expected to be implemented by your application (Postgres, Redis, files, API, etc.).

Typical flow:

  1. Load rules from your storage.
  2. Convert them to []decidr.Rule.
  3. Build engine := decidr.NewEngine(rules).
  4. Run EvaluateFirst or EvaluateAll.

About

lightweight Go rule engine for evaluating boolean rules over `string` values.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages