decidr is a lightweight Go rule engine for evaluating boolean rules over string values.
Supported operators:
equalsexistsandornotalways
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"])
}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/evaluationIf WithLogger is not provided, decidr uses a no-op logger by default.
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.
type RawCondition struct {
Operator string
Field string
Value string
Conditions []RawCondition
Condition *RawCondition
}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"}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,NOTALWAYSEXISTS(field)==- double-quoted string literals
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:
decidr.ValidateCondition,decidr.ValidateRule - Compilation:
decidr.CompileCondition - Evaluate one condition:
decidr.EvaluateCondition - First match by priority:
engine.EvaluateFirst - All matches by priority:
engine.EvaluateAll
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:
- Load rules from your storage.
- Convert them to
[]decidr.Rule. - Build
engine := decidr.NewEngine(rules). - Run
EvaluateFirstorEvaluateAll.