Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 38 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ domain matches /example\.com$/
```

When evaluated against:

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

Expand All @@ -44,13 +45,13 @@ import "github.com/qpoint-io/rulekit"

r, err := rule.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(&rulekit.Ctx{KV: inputData})

Expand All @@ -75,49 +76,50 @@ When a rule is evaluated, it returns a `Result` struct containing:
- `EvaluatedRule`: The sub-rule that determined the returned value. Useful for debugging and understanding which part of a complex rule caused the result.

The Result also provides additional helper methods:

- `Pass()`: Returns true if the rule returns true/a non-zero value with no errors
- `Fail()`: Returns true if the rule returns false/a zero value with no errors
- `Ok()`: Returns true if the rule executed with no error

## 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 |
| 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` |
| 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. |
| 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. |

## Macros

Expand Down Expand Up @@ -152,9 +154,10 @@ Functions can be called inside rules and used as value objects. Functions may ac

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://")` |
| 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://")` |
| `index(container, key)` | Indexes into a map or slice | `index(["one", "two"], 0)` -> `"one"` |

### Custom Functions

Expand Down Expand Up @@ -211,4 +214,4 @@ if rule.Pass() {
<source media="(prefers-color-scheme: dark)" srcset="./readme_assets/qpoint-open.svg">
<source media="(prefers-color-scheme: light)" srcset="./readme_assets/qpoint-open-light.svg">
<img alt="Image showing \"Qpoint ❤ OpenSource\"" src="./readme_assets/qpoint-open-light.svg">
</picture>
</picture>
52 changes: 52 additions & 0 deletions functions_stdlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,56 @@ var StdlibFuncs = map[string]*Function{
}
},
},

// index(container, key)
//
// index is used to index into a map via a string key or an array via an integer index.
//
// e.g. index(map, "key")
// map: map[string]any{"key": "value"} -> "value"
//
// index(["first", "second"], 1) -> "second"
"index": {
Args: []FunctionArg{
{Name: "container"},
{Name: "key"},
},
Eval: func(args map[string]any) Result {
container, err := IndexFuncArg[any](args, "container")
if err != nil {
return Result{Error: err}
}

switch c := container.(type) {
case KV:
key, err := IndexFuncArg[string](args, "key")
if err != nil {
return Result{Error: err}
}

val, ok := IndexKV(c, key)
if !ok {
return Result{Error: fmt.Errorf("key %q not found", key)}
}

return Result{Value: val}

case []any:
key, err := IndexFuncArg[int64](args, "key")
if err != nil {
return Result{Error: err}
}

if key < 0 || int(key) >= len(c) {
return Result{Error: fmt.Errorf("index %d out of bounds", key)}
}

return Result{
Value: c[key],
}
}

return Result{Error: fmt.Errorf("container must be a map or array")}
},
},
}
49 changes: 49 additions & 0 deletions functions_stdlib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,52 @@ starts_with(arg1)
^
function "starts_with" expects 2 arguments, got 1`)
}

func TestFn_Index(t *testing.T) {
// happy path - map
assertRulep(t,
`index(map, "key")`,
kv{"map": KV{"key": "value"}},
).Value("value")

// happy path - array
assertRulep(t, `index([1, 2, 3], 0)`, nil).Value(int64(1))

// happy path - nested map
assertRulep(t,
`index(map, "key.nested")`,
kv{"map": KV{"key": KV{"nested": "value"}}},
).Value("value")
assertRulep(t,
`index(index(map, "key"), "nested")`,
kv{"map": KV{"key": KV{"nested": "value"}}},
).Value("value")

// int key with map
assertRulep(t,
`index(map, 123)`,
kv{"map": KV{"key": "value"}},
).ErrorString(`arg key: expected string, got int64`)

// string key with array
assertRulep(t,
`index([1, 2, 3], "test")`,
kv{"map": []any{1, 2, 3}},
).ErrorString(`arg key: expected int64, got string`)

// out of bounds key with array
assertRulep(t,
`index([1, 2, 3], 10)`,
kv{"map": []any{1, 2, 3}},
).ErrorString(`index 10 out of bounds`)
assertRulep(t,
`index([1, 2, 3], -3)`,
kv{"map": []any{1, 2, 3}},
).ErrorString(`index -3 out of bounds`)

// invalid container type
assertRulep(t,
`index(map, "test")`,
kv{"map": 123},
).ErrorString(`container must be a map or array`)
}