regobrick

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

README

RegoBrick

RegoBrick provides a straightforward way to parse and transform Rego modules without modifying the OPA engine. It applies certain transformations based on special import markers (for example, import data.regobrick.default_false) and also offers convenient helpers for custom builtins and Go↔Rego value conversion.

Number Type

regobrick.Number is a numeric type based on json.Number, used to pass numeric values to Rego without floating-point precision loss. It supports DB operations (sql.Scanner, driver.Valuer) and JSON marshaling.

input := map[string]any{
    "price":    regobrick.Number("123.45"),
    "quantity": regobrick.Number("10"),
}

Contract:

  • Exponent notation (1e-8, 2.5E10) is supported in UseDecimalArithmetic(): it is expanded to plain decimal notation (1e-8 → 0.00000001) without floating-point round-trips before parsing, so 1e-8 + 1 yields 1.00000001 just like standard OPA
    • Exception: if the expansion exceeds udecimal's precision (more than 19 decimal places, e.g. 1e-25), parsing still fails — default mode: no result; StrictBuiltinErrors(true): eval_builtin_error
  • Input validation is the caller's responsibility

Precision Limits (udecimal):

  • Maximum 19 decimal places — input values with more fail to parse (default mode: no result; StrictBuiltinErrors(true): eval error); they are not silently truncated
  • Magnitude: coefficients up to 128 bits (±34,028,236,692,093,846,346.3374607431768211455 at the full 19 decimal places) stay on udecimal's zero-allocation fast path; larger plain-notation values fall back to exact big.Int arithmetic (slower, allocating) instead of failing — up to udecimal's input hard cap of 200 characters per number string, beyond which parsing fails
  • Exponent notation only: expansion is capped at 64 characters (≈62 digits), so 1e61 parses but 1e62 and beyond (e.g. 1e100) fail, while the same value written out in plain notation parses fine
  • Truncation (not rounding) applies only to operation results that exceed 19 decimal places (e.g., 100 / 3 → 33.3333333333333333333)
  • Sufficient for: BTC (8 decimals), ETH (18 decimals), fiat currencies

Overview

  • Default False If your Rego module imports data.regobrick.default_false, RegoBrick will automatically insert a default rule that evaluates to false for any "if" or boolean rules. This helps ensure you don't forget to explicitly set them to false when not satisfied.

  • Custom Builtins Easily register builtins with typed arguments and return values. RegoBrick converts Rego AST terms to Go types and back, so you can write builtins in Go with minimal boilerplate.

  • Operator Overloading Optionally override Rego's arithmetic and comparison operators with precision decimal operations.

Installation

go get github.com/sky1core/regobrick

Make sure you also have OPA in your go.mod if you plan to work with the Rego engine.

Usage

Below is an example of how to use RegoBrick with Number input data. By including import data.regobrick.default_false in your policy, RegoBrick automatically inserts a default rule (for example, default allow = false), ensuring that if the condition isn't met, the rule defaults to false.

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/open-policy-agent/opa/v1/rego"
    "github.com/sky1core/regobrick"
)

func main() {
    ctx := context.Background()

    // Example policy for the "sub" package
    subPolicy := `
        package sub

        some_rule if {
            input.amount == 123.45
        }
    `
    // Example policy for the "main" package
    mainPolicy := `
        package example

        import data.regobrick.default_false

        allow if {
            input.user == "admin"
        }
    `

    // Build a rego.Rego object with your modules and input.
    query, err := rego.New(
        // Add Rego modules (which will apply "default_false" if that import is found):
        regobrick.Module("sub.rego", subPolicy, []string{"data.some.pkg"}),
        regobrick.Module("main.rego", mainPolicy, []string{"data.mycompany.util"}),

        // Specify the query we want to evaluate:
        rego.Query("data.example.allow"),

    ).PrepareForEval(ctx)

    if err != nil {
        log.Fatal(err)
    }

    // Build the input map with Number values to avoid floating-point issues.
    input := map[string]any{
        "user":   "admin",
        "amount": regobrick.Number("123.45"),
    }

    // Evaluate using rego.EvalInput to pass input.
    rs, err := query.Eval(ctx, rego.EvalInput(input))
    if err != nil {
        log.Fatal(err)
    }

    // The result of 'data.example.allow' is in rs.
    // Because 'allow if ...' is accompanied by 'default allow = false',
    // if the condition is not met, it defaults to false.
    fmt.Println("Result:", rs)
}
Fail-fast contract of Module / Modules

Module (and Modules, which applies Module to each option) is fail-fast: it panics with a message like regobrick: cannot process module "<filename>": <cause> when regobrick was actually asked to do something — that is, imports is non-empty or the source references a data.regobrick. feature — but the module could not be processed (parse error, invalid injected import path, import name conflict, or an unknown regobrick feature).

If nothing regobrick-specific was requested (empty imports and no data.regobrick. reference), a parse failure is not a panic: Module falls back to rego.Module(filename, src), so plain modules keep compiling and surface their own errors later during compilation.

If you want to handle these errors yourself instead of risking a panic, call ParseModule and pass the resulting *ast.Module to rego.ParsedModule(...).

v0 syntax constraint: Module parses with the v1 parser, so a module written in v0 syntax can only pass through the plain fallback path (empty imports and no data.regobrick. feature). Combining v0 syntax with injected imports or a regobrick feature triggers a v1 parse error on the requested path, which becomes a panic under the fail-fast contract.

Precision Arithmetic

RegoBrick provides operator overloading for precision arithmetic using udecimal internally. Call UseDecimalArithmetic() once at startup to replace Rego's default float-based operators.

func init() {
    regobrick.UseDecimalArithmetic()
}

This overloads:

  • Arithmetic: +, -, *, /, %
  • Comparison: >, >=, <, <=, ==, !=
  • Unary: abs(), round(), ceil(), floor()
  • Aggregates: sum(), product(), max(), min()

Notes:

  • On error (e.g., divide by zero, invalid number format):
    • Default mode: operation silently fails (rule not satisfied)
    • StrictBuiltinErrors(true): returns eval_builtin_error
  • % (modulo) supports floating-point operands (standard OPA allows integers only)
  • Decimal arithmetic configuration is process-global; call UseDecimalArithmetic(...) once at application startup, before any evaluation begins
  • The string coercion flag is stored atomically, so a late call cannot corrupt the flag reads on the evaluation path. But UseDecimalArithmetic also rewrites OPA's builtin registry without synchronization, so calling it concurrently with evaluations is still unsafe — "once at startup" is the contract, not just a recommendation
  • The documented parsing/truncation contract relies on udecimal's default global settings, and UseDecimalArithmetic checks them on entry: it panics with regobrick: udecimal default settings have been changed ... if a 19-decimal-place value no longer parses, or if a 20-decimal-place value now parses (which would mean out-of-precision input is silently truncated). This is a snapshot taken at call time — calling udecimal.SetDefaultPrecision or udecimal.SetDefaultParseMode after UseDecimalArithmetic returns cannot be detected and would silently change the contract
String Coercion (opt-in)

Use WithStringCoercion() to enable automatic string-to-number conversion. Numeric strings (e.g., "0.73", "100") from input or data are automatically converted to numbers in arithmetic, comparison, unary, and aggregate operations. This is useful when external systems pass decimal values as JSON strings to preserve precision.

func init() {
    regobrick.UseDecimalArithmetic(regobrick.WithStringCoercion())
}
  • Applied to: +, -, *, /, %, >, >=, <, <=, abs, round, ceil, floor, sum, product, max, min
  • Not applied to: ==, != (different types are always unequal, matching standard OPA behavior)
  • Non-numeric strings (e.g., "abc") result in undefined / eval error
# input: {"qty": "0.73", "pos": 0.5}
remaining := input.qty - input.pos    # 0.23 — string "0.73" auto-converted
can_trade := input.qty > 0            # true
rounded := round(input.qty)           # 1

Note: String coercion is primarily for runtime values from input/data. Arithmetic (+, -, ...), unary (abs, ...), and the sum/product aggregates declare numeric operand types, so string literals written directly in Rego source (e.g., "0.73" + 1, sum(["0.1"])) are rejected by OPA's compile-time type checker before runtime coercion can run. This does not apply to max/min, whose operand is an Any collection: string literals pass the type checker and reach runtime, where non-numeric (or mixed) collections fall back to the default comparison ordering.

Comparison with Standard OPA

Below, Decimal = UseDecimalArithmetic(), +Coercion = UseDecimalArithmetic(WithStringCoercion()).

Arithmetic (number-only — same with or without WithStringCoercion):

Expression Decimal / +Coercion Standard OPA (big.Float)
1.1 + 2.2 3.3 3.3000000000000000002
0.3 - 0.1 0.2 0.20000000000000000002
100.25 * 0.03 3.0075 3.0075
100 / 3 33.3333333333333333333 (19 dp) 33.333333333333333332 (20 dp)
10 % 3 1 1
10.5 % 3 1.5 undefined / eval error (integers only)
1e-8 + 1 1.00000001 1.00000001
1e-25 + 1 undefined / eval error (expands past 19 dp) 1 (big.Float precision loss)
{1,2,3} - {2} {1,3} (set diff) {1,3} (set diff)

Comparison (number-only — same with or without WithStringCoercion):

Expression Decimal / +Coercion Standard OPA
0.3 - 0.1 == 0.2 true false
1.1 + 2.2 == 3.3 true false
3.3 > 2.2 true true
3.3 >= 3.3 true true
2.2 < 3.3 true true
2.2 <= 3.3 true true
1e-8 == 0.00000001 true true
1e-8 < 1 true true
"a" < "b" undefined true (type ordering)
"hello" > 123 undefined true (type ordering)

Unary (number-only — same with or without WithStringCoercion):

Expression Decimal / +Coercion Standard OPA
abs(-3.3) 3.3 3.3
round(2.5) 3 (half away from zero) 3
round(-2.5) -3 (half away from zero) -3
ceil(3.1) 4 4
floor(3.9) 3 3

Aggregates (number-only — same with or without WithStringCoercion):

Expression Decimal / +Coercion Standard OPA
sum([0.1, 0.2, 0.3]) 0.6 0.6
sum([]) 0 0
product([0.1, 0.2, 0.3]) 0.006 0.006000000000000000001
product([]) 1 1
max([0.1, 0.11, 0.09]) 0.11 0.11
min([0.1, 0.11, 0.09]) 0.09 0.09
max(["b", "a", "c"]) "c" "c"
min(["b", "a", "c"]) "a" "a"

String coercion — values from input/data, only with WithStringCoercion():

Expression Decimal +Coercion Standard OPA
input.s + 1 ({"s":"0.73"}) undefined 1.73 undefined
input.s - 0.5 ({"s":"0.73"}) undefined 0.23 undefined
input.a * input.b ({"a":"5.5","b":"2"}) undefined 11 undefined
input.s / 4 ({"s":"10"}) undefined 2.5 undefined
input.s % 3 ({"s":"10"}) undefined 1 undefined
input.s > 0.5 ({"s":"0.73"}) undefined true (numeric) true (type ordering)
input.s < 1 ({"s":"0.73"}) undefined true (numeric) false (type ordering)
input.s >= 3.3 ({"s":"3.3"}) undefined true (numeric) true (type ordering)
input.s <= 3.3 ({"s":"2.2"}) undefined true (numeric) false (type ordering)
input.s == 3.3 ({"s":"3.3"}) false false (not coerced) false
input.s != 3.3 ({"s":"3.3"}) true true (not coerced) true
abs(input.s) ({"s":"-3.3"}) undefined 3.3 undefined
round(input.s) ({"s":"3.5"}) undefined 4 undefined
ceil(input.s) ({"s":"3.1"}) undefined 4 undefined
floor(input.s) ({"s":"3.9"}) undefined 3 undefined
sum(input.arr) ({"arr":["0.1","0.2"]}) undefined 0.3 undefined
product(input.arr) ({"arr":["2","3"]}) undefined 6 undefined
max(input.arr) ({"arr":["1","10","2"]}) "2" (lexicographic) "10" (numeric) "2" (lexicographic)
min(input.arr) ({"arr":["1","10","2"]}) "1" (lexicographic) "1" (numeric) "1" (lexicographic)
input.s + 1 ({"s":"abc"}) undefined undefined undefined

Note: Standard OPA's comparison operators (>, <, >=, <=) support all types using type ordering (null < bool < number < string < ...). With UseDecimalArithmetic, comparison operators become numeric-only — non-number comparisons like "a" < "b" or "hello" > 123 result in undefined. With WithStringCoercion(), numeric strings are additionally accepted as numbers.

Performance

Perhaps counterintuitively, UseDecimalArithmetic() is faster than standard OPA on numeric workloads, not slower. Standard OPA evaluates non-integer arithmetic on heap-allocated arbitrary-precision big.Float values, while regobrick uses udecimal's fixed-width uint128 representation, which parses and computes on the stack within the magnitude fast path described above (larger values fall back to exact big.Int arithmetic).

On numeric-heavy benchmark policies (Apple M1 Max, Go 1.26.5, OPA v1.11.0), decimal mode measured 1.26×–5.7× faster than standard OPA (geomean −59% evaluation time) with 26–90% fewer heap allocations. WithStringCoercion() adds no measurable cost on those policies: with the same values supplied as JSON strings, no scenario measured slower than its numeric-input counterpart. The largest gap was −7.9% on aggregates, which is not attributable to the coercion path — the two runs evaluate different input documents.

The gains come from the numeric hot path, so policies dominated by evaluator overhead rather than arithmetic see smaller improvements (1.26× on a single-comparison guard policy). Full results, methodology, and reproduction commands: internal/bench/README.md.

Writing Custom Builtins

You can register a custom function that OPA calls within your policies. RegoBrick provides helper functions (like RegisterBuiltin1, RegisterBuiltin2, etc.) for builtins that accept typed Go arguments and return typed Go values.

package main

import (
    "github.com/open-policy-agent/opa/v1/rego"
    "github.com/sky1core/regobrick"
)

// Example builtin that checks if a user is "admin"
func isAdmin(ctx rego.BuiltinContext, user string) (bool, error) {
    return user == "admin", nil
}

func init() {
    regobrick.RegisterBuiltin1[string, bool](
        "is_admin",
        isAdmin,
        regobrick.WithCategories("my_custom_category"),
    )
}
Argument and Return Type Mapping

The Go types in the builtin signature determine the Rego types the builtin is declared with, and every argument and return value is converted at the boundary through JSON.

Go type Rego type Round-trip characteristics
string string Verbatim, except for control characters (see below)
bool boolean Verbatim
int, int64 (and the other Go integer types) number Exact for integers within the Go type's range, except uint64 return values above 2^63−1 (see below); a number with a fractional part is rejected, not truncated (default mode: no result; StrictBuiltinErrors(true): eval_builtin_error)
float64 number Rounded to float64 precision; large magnitudes serialize in exponent notation (see below)
float32 number Rounded to float32 precision; serialized in plain decimal notation
json.Number, regobrick.Number number Digits preserved exactly in both directions — declare these when precision matters
time.Time string RFC3339 in both directions
struct object Keys are the JSON tags
[]T array A nil slice returns null, an empty slice returns []
map[string]T object A nil map returns null
pointer shape of the pointed-to value A nil pointer returns null
any any Accepts any Rego value representable as JSON — which excludes sets (see below)

Every scalar row above is declared to OPA with its concrete Rego type — including time.Time, which is declared as string — so a mismatched call (for example a string passed to an int parameter) is rejected at compile time, before evaluation. The composite rows (struct, []T, map[string]T, pointer, any) are declared as any, so their shape is checked only while converting, at evaluation time.

Known limitations

Argument conversion decodes each argument's Rego representation as JSON, so Rego values whose representation is not JSON cannot reach a builtin at all. The declared Go parameter type does not change this: the failure happens before the Go type is involved.

  • Sets cannot be passed as arguments. A set's Rego representation ({1, 2, 3}) is not valid JSON, so the call fails — default mode: no result; StrictBuiltinErrors(true): eval_builtin_error with invalid character '1' looking for beginning of object key string. This also applies to a set nested inside an object or array argument. Convert the set to an array before passing it (for example [x | x := my_set[_]]).
  • Strings containing control characters cannot be passed as arguments. Rego string representations are Go-quoted, so U+0001 becomes \x01 and U+0007 becomes \a — escapes JSON does not have. The call fails the same way — default mode: no result; StrictBuiltinErrors(true): eval_builtin_error with invalid character 'x' in string escape code. Escapes shared with JSON (\n, \t, ...) and printable non-ASCII text are unaffected.

On the return side, two numeric conversions do not preserve what the Go value held:

  • float64 returns may come back in exponent notation. The value is formatted with Go's shortest-round-trip float formatting, so a builtin returning float64(9007199254740993) yields 9.007199254740992e+15 — the lost digit is float64's own precision limit, the notation is the formatting. Declare json.Number or regobrick.Number to get exact decimal digits.
  • uint64 return values above 2^63−1 come back negative. OPA narrows a returned uint64 to a signed int before building the Rego number (v1.11.0, ast/interning.go), so uint64(1 << 63) reaches the policy as -9223372036854775808 and uint64(math.MaxUint64) as -1, with no error raised anywhere. Arguments are not affected — a uint64 parameter receives 18446744073709551615 intact — and neither is uint, which takes a different conversion route. For return values above 2^63−1, declare json.Number or regobrick.Number.
Memoization with WithMemoize

For expensive computations, use WithMemoize() to cache results for the same arguments within a single evaluation:

regobrick.RegisterBuiltin1[string, int](
    "expensive_lookup",
    expensiveLookup,
    regobrick.WithMemoize(),
)
Advanced Options with ConfigureFunction

For advanced use cases not covered by built-in options, use ConfigureFunction to directly configure the underlying rego.Function:

regobrick.RegisterBuiltin1[string, int](
    "custom_func",
    customFunc,
    regobrick.ConfigureFunction(func(f *rego.Function) {
        f.Memoize = true
        f.Nondeterministic = true
    }),
)

Filtering Builtins with FilterCapabilities

If you want to restrict which builtins are allowed when evaluating a policy, you can use the FilterCapabilities function to include or exclude builtins by name and category.

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/open-policy-agent/opa/v1/rego"
    "github.com/sky1core/regobrick"
)

func main() {
    allowedNames := []string{"is_admin", "concat"}
    allowedCats := []string{"my_custom_category", "strings"}

    caps := regobrick.FilterCapabilities(allowedNames, allowedCats)

    ctx := context.Background()
    query, err := rego.New(
        rego.Query("data.example.allow"),
        rego.Capabilities(caps),
    ).PrepareForEval(ctx)
    if err != nil {
        log.Fatal(err)
    }

    rs, err := query.Eval(ctx)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println("Query result:", rs)
}

Running the Integration Tests

The integration tests live under tests/integration in a separate Go module (its own go.mod) and are gated behind the integration build tag. They require Docker (they use testcontainers to spin up real databases), so they are not run by the default go test ./....

cd tests/integration && go test -tags=integration -v

Documentation

Overview

Package regobrick provides high-level functions for applying RegoBrick transformations and adding modules to OPA rego.Rego objects.

Custom builtin type conversion

The Go types in a RegisterBuiltinX signature determine the Rego types the builtin is declared with, and every argument and return value is converted at the boundary through JSON.

Go type                Rego type   Round-trip characteristics
string                 string      verbatim, except for control characters (see below)
bool                   boolean     verbatim
int, int64, ...        number      exact within the Go type's range, except uint64
                                   return values above 2^63-1 (see below); a number
                                   with a fractional part is rejected, not truncated
float64                number      rounded to float64 precision; large magnitudes
                                   serialize in exponent notation (see below)
float32                number      rounded to float32 precision; serialized in plain
                                   decimal notation
json.Number, Number    number      digits preserved exactly in both directions;
                                   declare these when precision matters
time.Time              string      RFC3339 in both directions
struct                 object      keys are the JSON tags
[]T                    array       a nil slice returns null, an empty slice returns []
map[string]T           object      a nil map returns null
pointer                pointed-to  a nil pointer returns null
any                    any         any Rego value representable as JSON, which excludes sets

Every scalar row above is declared to OPA with its concrete Rego type — including time.Time, which is declared as string — so a mismatched call is rejected at compile time, before evaluation. The composite rows (struct, []T, map[string]T, pointer, any) are declared as any, so their shape is checked only while converting, at evaluation time.

Custom builtin conversion limits

Argument conversion decodes each argument's Rego representation as JSON, so a Rego value whose representation is not JSON cannot reach a builtin at all. The declared Go parameter type does not change this: the failure happens before the Go type is involved.

  • Sets cannot be passed as arguments. A set's representation ({1, 2, 3}) is not valid JSON, so the call fails — default mode: no result; rego.StrictBuiltinErrors(true): eval_builtin_error with "invalid character '1' looking for beginning of object key string". This also applies to a set nested inside an object or array argument. Convert the set to an array before passing it.
  • Strings containing control characters cannot be passed as arguments. Rego string representations are Go-quoted, so U+0001 becomes \x01 and U+0007 becomes \a — escapes JSON does not have. The call fails the same way, with "invalid character 'x' in string escape code". Escapes shared with JSON (\n, \t, ...) and printable non-ASCII text are unaffected.

On the return side, two numeric conversions do not preserve what the Go value held:

  • Returned float64 values are formatted with Go's shortest round-trip float formatting, so a builtin returning float64(9007199254740993) yields 9.007199254740992e+15: the lost digit is float64's own precision limit, the notation is the formatting.
  • Returned uint64 values above 2^63-1 come back negative. OPA narrows a returned uint64 to a signed int before building the Rego number (v1.11.0, ast/interning.go), so uint64(1 << 63) reaches the policy as -9223372036854775808 and uint64(math.MaxUint64) as -1, with no error raised anywhere. Arguments are not affected — a uint64 parameter receives 18446744073709551615 intact — and neither is uint, which takes a different conversion route.

Declare json.Number or Number to get exact decimal digits in both cases.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FilterCapabilities added in v0.3.0

func FilterCapabilities(allowedNames []string, allowedCats []string) *ast.Capabilities

FilterCapabilities filters OPA built-in functions based on a list of allowed names and categories, along with built-ins whose infix is in coreInfixes.

  • If a built-in's infix is one of the core infixes, it is kept.
  • Otherwise, if the built-in name is in allowedNames or any of its categories are in allowedCats, it is kept.
  • If a built-in has custom category mappings, those are also checked against allowedCats.

The resulting capabilities object contains only the filtered built-ins.

Call order contract: custom builtins registered via RegisterBuiltinX must be registered (typically from init) before FilterCapabilities is called, so that their category mappings are already recorded. Only then can those custom builtins be matched and included by their categories.

func Module

func Module(filename, src string, imports []string) func(*rego.Rego)

Module returns a rego.Rego option that adds a single Rego module from the given filename, source, and optional imports.

Fail-fast contract: if the module cannot be processed, Module panics with a message like `regobrick: cannot process module "<filename>": <cause>` UNLESS the caller requested nothing regobrick-specific (imports is empty AND the source does not reference any "data.regobrick." feature). In that case Module falls back to rego.Module(filename, src), preserving plain (including v0) workflows. If you want to handle errors yourself instead of risking a panic, call ParseModule and pass the resulting *ast.Module to rego.ParsedModule(...).

func Modules added in v0.5.0

func Modules(opts ...ModuleOption) func(*rego.Rego)

Modules returns a rego.Rego option that adds multiple Rego modules in one call, each specified via a ModuleOption. It follows the same fail-fast contract as Module: a module that requested regobrick behavior but failed to parse causes a panic, while a fully plain module falls back to rego.Module.

func ParseModule

func ParseModule(filename, src string, imports []string) (*ast.Module, error)

ParseModule parses a Rego source file into an AST module, optionally appending additional imports. If the module includes "import data.regobrick.default_false", it applies the default_false transform. METADATA annotations are preserved.

Any "data.regobrick.*" marker import is stripped from the returned module: it only triggers transforms and would otherwise be an unused import. This is observable to callers using rego.Strict(true), which rejects unused imports.

ParseModule never panics; it returns an error for parse failures, invalid injected import paths, import name conflicts, or unknown regobrick features.

func RegisterBuiltin0 added in v0.2.0

func RegisterBuiltin0[R any](name string, fn func(rego.BuiltinContext) (R, error), opts ...BuiltinRegisterOption)

RegisterBuiltin0 registers a builtin with no arguments. Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin0_ added in v0.2.0

func RegisterBuiltin0_(name string, fn func(rego.BuiltinContext) error, opts ...BuiltinRegisterOption)

RegisterBuiltin0_ registers a builtin with no arguments that returns only an error (null to Rego). Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin1 added in v0.2.0

func RegisterBuiltin1[T1 any, R any](name string, fn func(rego.BuiltinContext, T1) (R, error), opts ...BuiltinRegisterOption)

RegisterBuiltin1 registers a builtin with 1 argument. Must be called during package initialization (init function). Calling after initialization may cause race conditions.

For how argument and return types map to Rego types, and which Rego values cannot be converted, see the package documentation: "Custom builtin type conversion" and "Custom builtin conversion limits". The same rules apply to every RegisterBuiltinX arity.

func RegisterBuiltin1_ added in v0.2.0

func RegisterBuiltin1_[T1 any](name string, fn func(rego.BuiltinContext, T1) error, opts ...BuiltinRegisterOption)

RegisterBuiltin1_ registers a builtin with 1 argument that returns only an error (null to Rego). Must be called during package initialization (init function). Calling after initialization may cause race conditions.

For how argument types map to Rego types, and which Rego values cannot be converted, see the package documentation: "Custom builtin type conversion" and "Custom builtin conversion limits". The same rules apply to every RegisterBuiltinX_ arity.

func RegisterBuiltin2 added in v0.2.0

func RegisterBuiltin2[T1 any, T2 any, R any](name string, fn func(rego.BuiltinContext, T1, T2) (R, error), opts ...BuiltinRegisterOption)

RegisterBuiltin2 registers a builtin with 2 arguments. Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin2_ added in v0.2.0

func RegisterBuiltin2_[T1 any, T2 any](name string, fn func(rego.BuiltinContext, T1, T2) error, opts ...BuiltinRegisterOption)

RegisterBuiltin2_ registers a builtin with 2 arguments that returns only an error (null to Rego). Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin3 added in v0.2.0

func RegisterBuiltin3[T1 any, T2 any, T3 any, R any](name string, fn func(rego.BuiltinContext, T1, T2, T3) (R, error), opts ...BuiltinRegisterOption)

RegisterBuiltin3 registers a builtin with 3 arguments. Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin3_ added in v0.2.0

func RegisterBuiltin3_[T1 any, T2 any, T3 any](name string, fn func(rego.BuiltinContext, T1, T2, T3) error, opts ...BuiltinRegisterOption)

RegisterBuiltin3_ registers a builtin with 3 arguments that returns only an error (null to Rego). Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin4 added in v0.2.0

func RegisterBuiltin4[T1 any, T2 any, T3 any, T4 any, R any](name string, fn func(rego.BuiltinContext, T1, T2, T3, T4) (R, error), opts ...BuiltinRegisterOption)

RegisterBuiltin4 registers a builtin with 4 arguments. Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin4_ added in v0.2.0

func RegisterBuiltin4_[T1 any, T2 any, T3 any, T4 any](name string, fn func(rego.BuiltinContext, T1, T2, T3, T4) error, opts ...BuiltinRegisterOption)

RegisterBuiltin4_ registers a builtin with 4 arguments that returns only an error (null to Rego). Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin5 added in v0.2.0

func RegisterBuiltin5[T1 any, T2 any, T3 any, T4 any, T5 any, R any](name string, fn func(rego.BuiltinContext, T1, T2, T3, T4, T5) (R, error), opts ...BuiltinRegisterOption)

RegisterBuiltin5 registers a builtin with 5 arguments. Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func RegisterBuiltin5_ added in v0.2.0

func RegisterBuiltin5_[T1 any, T2 any, T3 any, T4 any, T5 any](name string, fn func(rego.BuiltinContext, T1, T2, T3, T4, T5) error, opts ...BuiltinRegisterOption)

RegisterBuiltin5_ registers a builtin with 5 arguments that returns only an error (null to Rego). Must be called during package initialization (init function). Calling after initialization may cause race conditions.

func UseDecimalArithmetic added in v0.7.0

func UseDecimalArithmetic(opts ...DecimalArithmeticOption)

UseDecimalArithmetic replaces Rego's numeric operations with precision decimal operations.

Overloaded operators

  • Arithmetic: +, -, *, /, %
  • Comparison: >, >=, <, <=, ==, !=
  • Unary: abs(), round(), ceil(), floor()
  • Aggregates: sum(), product(), max(), min()

Standard OPA differences

Standard OPA comparison operators (>, <, >=, <=) support all types using type ordering (null < bool < number < string < ...). With UseDecimalArithmetic, comparison operators become numeric-only, and non-numeric string comparisons ("a" < "b") will not work.

The % (rem) operator accepts decimal operands (e.g. 10.5 % 3), whereas standard OPA restricts modulo to integers.

Precision limits (udecimal)

  • Maximum 19 decimal places; values with more fail to parse (default mode: no result; StrictBuiltinErrors: eval error). Truncation (not rounding) applies only to operation results, e.g. 100/3.
  • Magnitude: coefficients up to 128 bits (±34,028,236,692,093,846,346.3374607431768211455 at the full 19 decimal places) stay on udecimal's zero-allocation fast path; larger plain-notation values fall back to udecimal's exact big.Int arithmetic instead of failing, up to udecimal's input hard cap of 200 characters per number string, beyond which parsing fails.
  • Exponent notation is expanded to plain notation before parsing under a 64-character (≈62-digit) budget: 1e61 parses, 1e62 and beyond (e.g. 1e100) fail, while the same magnitude written out in plain notation parses fine.

Error handling

  • Default mode: operation failure results in rule not satisfied (no result)
  • StrictBuiltinErrors(true): returns eval_builtin_error

Options

  • WithStringCoercion(): auto-convert numeric strings to numbers

Usage

// Basic precision arithmetic
regobrick.UseDecimalArithmetic()

// With string-to-number coercion
regobrick.UseDecimalArithmetic(regobrick.WithStringCoercion())

Concurrency

Call this once at application startup, before any evaluation begins.

It writes two pieces of process-global state, with different guarantees. The coercion setting is stored atomically, so a late call cannot corrupt the flag reads on the evaluation path. OPA's builtin function registry, however, is rewritten without synchronization. Calling this concurrently with evaluation therefore remains unsafe, and "once at startup" is the contract rather than just a recommendation.

Fail-fast on changed udecimal defaults

The parsing contract documented above relies on udecimal's default global settings. This function verifies them on entry: a 19-decimal-place value must parse, and a 20-decimal-place value must fail to parse. If either check fails — as it would after a udecimal.SetDefaultPrecision or udecimal.SetDefaultParseMode call elsewhere in the process — it panics with a message starting "regobrick: udecimal default settings have been changed", rather than silently degrading precision or truncating.

The check is a snapshot taken at call time. Changing udecimal's defaults after this function returns cannot be detected, and would silently change the contract.

Types

type BuiltinRegisterOption added in v0.4.0

type BuiltinRegisterOption func(*builtinRegisterConfig)

BuiltinRegisterOption configures how a builtin is registered by the RegisterBuiltinX / RegisterBuiltinX_ functions. Pass any number of these options to attach categories or customize the underlying rego.Function.

func ConfigureFunction added in v0.7.0

func ConfigureFunction(configurator func(*rego.Function)) BuiltinRegisterOption

ConfigureFunction allows direct configuration of the rego.Function before registration. Use this for advanced options not directly supported by other options. Passing nil is a no-op.

func WithCategories added in v0.4.0

func WithCategories(cats ...string) BuiltinRegisterOption

WithCategories assigns one or more categories to the registered builtin. These categories are stored per builtin name and consulted by FilterCapabilities: a filtered capabilities set keeps a custom builtin when one of its categories is in the allowed category list.

func WithMemoize added in v0.7.0

func WithMemoize() BuiltinRegisterOption

WithMemoize enables memoization for the builtin. Memoized builtins cache results for the same inputs within a single evaluation.

func WithNondeterministic added in v0.4.0

func WithNondeterministic() BuiltinRegisterOption

WithNondeterministic marks the builtin as nondeterministic. Nondeterministic builtins may return different results for the same inputs.

type DecimalArithmeticOption added in v0.8.0

type DecimalArithmeticOption func(*decimalArithmeticConfig)

DecimalArithmeticOption configures UseDecimalArithmetic behavior.

func WithStringCoercion added in v0.8.0

func WithStringCoercion() DecimalArithmeticOption

WithStringCoercion enables automatic string-to-number coercion.

Numeric strings (e.g., "0.73", "100") from input or data are automatically converted to numbers in arithmetic, comparison, unary, and aggregate operations.

  • Applied to: +, -, *, /, %, >, >=, <, <=, abs, round, ceil, floor, sum, product, max, min
  • NOT applied to: ==, != (different types are always unequal — standard OPA behavior)
  • Non-numeric strings ("abc"): operation fails (undefined or eval error)

String coercion is primarily intended for runtime values from input/data. Arithmetic, unary, and the sum/product aggregates declare numeric operand types, so string literals in Rego source code (e.g., "0.73" + 1, sum(["0.1"])) are rejected by OPA's compile-time type checker before our runtime coercion logic runs. This does not apply to max/min, whose operand is an Any collection: such literals reach runtime, where non-numeric or mixed collections fall back to the default comparison ordering.

type ModuleOption added in v0.5.0

type ModuleOption = module.ModuleOption

ModuleOption is an alias for module.ModuleOption, used for specifying Rego module parameters or configuration.

type Number added in v0.7.0

type Number json.Number

Number is a numeric representation passed to Rego. It is emitted as a numeric literal during JSON serialization and integrates directly with numeric DB columns.

Contract:

  • Exponent notation (e/E) is supported. In UseDecimalArithmetic() operations it is expanded to plain decimal notation before parsing (e.g. "1e-8" → "0.00000001"), so it behaves the same as standard OPA. However, if the expanded result exceeds udecimal's precision (19 decimal places, e.g. "1e-25"), evaluation still fails with a parse error.
  • Input validity is the provider's responsibility; regobrick validates only at operation time.
  • DB Scan accepts string, []byte, int64, and float64.
  • Preserving the precision of DECIMAL columns is the driver's responsibility (it must return string/[]byte). Verifying this up front with a driver integrity test is recommended.

NULL/empty handling (json.Number and decimal library conventions):

  • JSON: null → empty string, empty string → emitted as 0 (Go zero-value convention)
  • DB: NULL/empty → error (decimal library convention)

Example:

input := map[string]any{
    "price": regobrick.Number("123.45"),
}

func (Number) MarshalJSON added in v0.7.1

func (n Number) MarshalJSON() ([]byte, error)

MarshalJSON emits a JSON numeric literal (without quotes). json.Number emits an empty string ("") as 0 (Go convention: the zero value is valid).

func (*Number) Scan added in v0.7.1

func (n *Number) Scan(src any) error

Scan implements sql.Scanner, reading a numeric column from the DB. NULL/empty values return an error (a convention of decimal libraries such as udecimal and shopspring).

func (Number) String added in v0.7.1

func (n Number) String() string

String returns the string representation.

func (*Number) UnmarshalJSON added in v0.7.1

func (n *Number) UnmarshalJSON(b []byte) error

UnmarshalJSON parses a JSON number. json.Number treats null as an empty string (""). An empty string is emitted as 0 by MarshalJSON.

func (Number) Value added in v0.7.1

func (n Number) Value() (driver.Value, error)

Value implements driver.Valuer, writing to a DECIMAL/NUMERIC column in the DB. An empty value returns an error (decimal library convention).

type RegoDecimal deprecated added in v0.6.0

type RegoDecimal = types.RegoDecimal

RegoDecimal is an alias for types.RegoDecimal. It represents a numeric value that serializes to JSON as a numeric literal (e.g., 123.456) rather than a string (e.g., "123.456").

Deprecated: Use Number instead. RegoDecimal will be removed in a future version.

func NewRegoDecimal deprecated added in v0.6.0

func NewRegoDecimal(d decimal.Decimal) RegoDecimal

NewRegoDecimal creates a RegoDecimal from an existing decimal.Decimal value. The resulting RegoDecimal retains the same precision and scale.

Deprecated: Use Number(d.String()) instead.

func NewRegoDecimalFromInt deprecated added in v0.6.0

func NewRegoDecimalFromInt(i int64) RegoDecimal

NewRegoDecimalFromInt creates a RegoDecimal from an int64. This is a shortcut for decimal.NewFromInt(...) wrapped in a RegoDecimal.

Deprecated: Use Number(strconv.FormatInt(i, 10)) instead.

Directories

Path Synopsis
internal
bench/benchcommon
Package benchcommon holds the policy sources, inputs and runner shared by the standard-OPA and decimal-mode benchmark packages.
Package benchcommon holds the policy sources, inputs and runner shared by the standard-OPA and decimal-mode benchmark packages.
module
Package module provides utilities for parsing and transforming Rego modules, particularly for detecting and applying regobrick features like "default_false".
Package module provides utilities for parsing and transforming Rego modules, particularly for detecting and applying regobrick features like "default_false".
types
Package types provides wrappers around shopspring/decimal.
Package types provides wrappers around shopspring/decimal.

Jump to

Keyboard shortcuts

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