Skip to content

Commit 9fb95b6

Browse files
committed
document functions and macros
1 parent 091df19 commit 9fb95b6

File tree

1 file changed

+118
-47
lines changed

1 file changed

+118
-47
lines changed

README.md

Lines changed: 118 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -38,37 +38,31 @@ A field on its own (without an operator) will check if the field contains a non-
3838
## Usage Example
3939

4040
```go
41-
import (
42-
"fmt"
43-
"github.com/qpoint-io/rulekit/rule"
44-
)
45-
46-
func main() {
47-
// Parse a rule
48-
r, err := rule.Parse(`domain matches /example\.com$/ and port == 8080`)
49-
if err != nil {
50-
fmt.Printf("Failed to parse rule: %v\n", err)
51-
return
52-
}
41+
import "github.com/qpoint-io/rulekit"
42+
43+
// ...
44+
45+
r, err := rule.Parse(`domain matches /example\.com$/ and port == 8080`)
46+
if err != nil { /* ... */ }
5347

54-
// Define input data
55-
inputData := rulekit.KV{
56-
"domain": "example.com",
57-
"port": 8080,
58-
}
48+
// define input data
49+
input := rulekit.KV{
50+
"domain": "example.com",
51+
"port": 8080,
52+
}
5953

60-
// Evaluate the rule using Ctx
61-
result := r.Eval(&rulekit.Ctx{KV: inputData})
62-
63-
// Check for errors
64-
if !result.Ok() {
65-
fmt.Printf("Error evaluating rule: %v\n", result.Error)
66-
} else {
67-
if result.Pass() {
68-
fmt.Println("PASS!")
69-
} else if result.Fail() {
70-
fmt.Println("FAIL :(")
71-
}
54+
// evaluate the rule
55+
result := r.Eval(&rulekit.Ctx{KV: inputData})
56+
57+
// check for errors
58+
if !result.Ok() {
59+
fmt.Printf("error evaluating rule: %v\n", result.Error)
60+
} else {
61+
if result.Pass() {
62+
fmt.Println("PASS!")
63+
} else if result.Fail() {
64+
fmt.Println("FAIL :(")
65+
}
7266
}
7367
```
7468

@@ -77,29 +71,14 @@ func main() {
7771
When a rule is evaluated, it returns a `Result` struct containing:
7872

7973
- `Value`: The evaluated value, usually a boolean
80-
- `Error`: Any fields required by the rule but not present in the input
81-
- `EvaluatedRule`: The sub-rule that determined the returned value
74+
- `Error`: Any evaluation errors such as fields missing from the KV map
75+
- `EvaluatedRule`: The sub-rule that determined the returned value. Useful for debugging and understanding which part of a complex rule caused the result.
8276

8377
The Result also provides additional helper methods:
8478
- `Pass()`: Returns true if the rule returns true/a non-zero value with no errors
8579
- `Fail()`: Returns true if the rule returns false/a zero value with no errors
8680
- `Ok()`: Returns true if the rule executed with no error
8781

88-
## Context
89-
90-
Rules are evaluated with a context input containing field values. `rulekit.KV` is an alias for `map[string]any`.
91-
92-
```go
93-
ctx := &rulekit.Ctx{
94-
KV: rulekit.KV{
95-
"domain": "example.com",
96-
"port": 8080,
97-
},
98-
}
99-
100-
result := rule.Eval(ctx)
101-
```
102-
10382
## Supported Operators
10483

10584
| Operator | Alias | Description |
@@ -120,6 +99,8 @@ result := rule.Eval(ctx)
12099

121100
## Supported Types
122101

102+
### Basic values
103+
123104
| Type | Used As | Example | Description |
124105
|------|---------|---------|-------------|
125106
| **bool** | VALUE, FIELD | `true` | Valid values: `true`, `false` |
@@ -130,7 +111,97 @@ result := rule.Eval(ctx)
130111
| **Hexadecimal string** | VALUE, FIELD | `12:34:56:78:ab` (MAC address), `504f5354` (hex string "POST") | A hexadecimal string, optionally separated by colons. |
131112
| **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` |
132113

133-
Arrays are also supported using a square bracket notiation. An array may contain mixed value types. For example: `field in [1.2.3.4, "domain.com"]`.
114+
### Constructs
115+
116+
| Type | Used As | Example | Description |
117+
|------|---------|---------|-------------|
118+
| **Array** | VALUE | `[1, "string", true]` | An array of mixed value types. Can be used with most operators including `in` and `contains`. |
119+
| **Function** | VALUE | `starts_with(url, "https://")` | A function call with optional arguments. Can be built-in or custom. |
120+
| **Macro** | VALUE | `isValidRequest()` | A zero-argument function that encapsulates a predefined rule. |
121+
122+
## Macros
123+
124+
Macros can be used for complex or commonly-used rules. They are defined in the evaluation context:
125+
126+
```go
127+
// create a macro
128+
isInternalAPI, err := rulekit.Parse(`domain matches /\.internal\.example\.com$/ or ip in 10.0.0.0/8`)
129+
if err != nil { /* ... */ }
130+
131+
// create a rule that uses the macro
132+
rule, err := rulekit.Parse(`isInternalAPI() && user != "root"`)
133+
if err != nil { /* ... */ }
134+
135+
// evaluate the rule, making sure to pass the macro in the eval context
136+
result := rule.Eval(&rulekit.Ctx{
137+
Macros: map[string]rulekit.Rule{
138+
"isInternalAPI": isInternalAPI,
139+
},
140+
KV: rulekit.KV{
141+
"user": user,
142+
// ...
143+
},
144+
})
145+
```
146+
147+
## Functions
148+
149+
Functions can be called inside rules and used as value objects. Functions may accept zero or more arguments.
150+
151+
### Standard library
152+
153+
Rulekit comes with a built-in standard library of functions:
154+
155+
| Function | Description | Example |
156+
|----------|-------------|---------|
157+
| `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://")` |
158+
159+
### Custom Functions
160+
161+
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.
162+
163+
```go
164+
// define a custom function
165+
customFuncs := map[string]*rulekit.Function{
166+
"randomInt": {
167+
Args: []rulekit.FunctionArg{
168+
{Name: "min"},
169+
{Name: "max"},
170+
},
171+
Eval: func(args map[string]any) rulekit.Result {
172+
// use the rulekit.IndexFuncArg helper to retrieve args and validate types.
173+
// rulekit.IndexFuncArg[any] will skip type validation.
174+
min, err := rulekit.IndexFuncArg[int64](args, "min")
175+
if err != nil {
176+
return rulekit.Result{Error: err}
177+
}
178+
179+
max, err := rulekit.IndexFuncArg[int64](args, "max")
180+
if err != nil {
181+
return rulekit.Result{Error: err}
182+
}
183+
184+
num := rand.IntN(max-min) + min
185+
return Result{
186+
Value: num,
187+
}
188+
},
189+
},
190+
}
191+
192+
// call the function in a rule
193+
rule, err := rulekit.Parse(`randomInt(10, 20) == 15`)
194+
if err != nil { /* ... */ }
195+
196+
result1, err := rule.Eval(&rulekit.Ctx{
197+
Functions: customFuncs,
198+
})
199+
if err != nil { /* ... */ }
200+
201+
if rule.Pass() {
202+
// the random number is 15!
203+
}
204+
```
134205

135206
## License
136207

0 commit comments

Comments
 (0)