Built-in Functions can be added inside the topdown package.

Built-in functions may be upstreamed if they are generally useful and provide functionality that would be impractical to implement natively in Rego (e.g., CIDR arithmetic). Implementations should avoid thirdparty dependencies. If absolutely necessary, consider importing the code manually into the internal package.

Read more about extending OPA with custom built-in functions in go here.

Adding a new built-in function involves the following steps:

  1. Declare and register the function
  2. Implementation the function
  3. Test the function
  4. Document the function

Example

The following example adds a simple built-in function, repeat(string, int), that returns a given string repeated a given number of times.

Declare and Register

In ast/builtins.go, we declare the structure of our built-in function with a Builtin struct instance:

  1. // Repeat returns, as a string, the given string repeated the given number of times.
  2. var Repeat = &Builtin{
  3. Name: "repeat", // The name of the function
  4. Decl: types.NewFunction(
  5. types.Args( // The built-in takes two arguments, where ..
  6. types.S, // .. the first is a string, and ..
  7. types.N, // .. the second is a number.
  8. ),
  9. types.S, // The return type is a string.
  10. ),
  11. }

To register the new built-in function, we locate the DefaultBuiltins array in ast/builtins.go, and add the Builtin instance to it:

  1. var DefaultBuiltins = [...]*Builtin{
  2. ...
  3. Repeat,
  4. ...
  5. }

Implement

In the topdown package, we locate a suitable source file for our new built-in function, or add a new file, as appropriate.

In this example, we introduce a new source file, topdown/repeat.go:

  1. package topdown
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/open-policy-agent/opa/ast"
  6. "github.com/open-policy-agent/opa/topdown/builtins"
  7. )
  8. // implements topdown.BuiltinFunc
  9. func builtinRepeat(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
  10. // Get the first argument as a string, returning an error if it's not the correct type.
  11. str, err := builtins.StringOperand(operands[0].Value, 1)
  12. if err != nil {
  13. return err
  14. }
  15. // Get the first argument as an int, returning an error if it's not the correct type or not a positive value.
  16. count, err := builtins.IntOperand(operands[1].Value, 2)
  17. if err != nil {
  18. return err
  19. } else if count < 0 {
  20. // Defensive check, strings.Repeat(...) will panic for count<0
  21. return fmt.Errorf("count must be a positive integer")
  22. }
  23. // Return a string by invoking the given iterator function
  24. return iter(ast.StringTerm(strings.Repeat(string(str), count)))
  25. }
  26. func init() {
  27. RegisterBuiltinFunc(ast.Repeat.Name, builtinRepeat)
  28. }

In the above code, builtinRepeat implements the topdown.BuiltinFunc function type. The call to RegisterBuiltinFunc(...) in init() adds the built-in function to the evaluation engine; binding the implementation to ast.Repeat that was registered in an earlier step.

Test

All built-in function implementations must include a test suite. Test cases for built-in functions are written in YAML and located under test/cases/testdata.

We create two new test cases (one positive, expecting a string output; and one negative, expecting an error) for our built-in function:

  1. cases:
  2. - note: repeat/positive
  3. query: data.test.p = x
  4. modules:
  5. - |
  6. package test
  7. p := repeated {
  8. repeated := repeat(input.str, input.count)
  9. }
  10. input: {"str": "Foo", "count": 3}
  11. want_result:
  12. - x: FooFooFoo
  13. - note: repeat/negative
  14. query: data.test.p = x
  15. modules:
  16. - |
  17. package test
  18. p := repeated {
  19. repeated := repeat(input.str, input.count)
  20. }
  21. input: { "str": "Foo", "count": -3 }
  22. strict_error: true
  23. want_error_code: eval_builtin_error
  24. want_error: 'repeat: count must be a positive integer'

The above test cases can be run separate from all other tests through: go test ./topdown -v -run 'TestRego/repeat'

See test/cases/testdata/helloworld for a more detailed example of how to implement tests for your built-in functions.

Note: We can manually test our new built-in function by building and running the eval command. E.g.: $./opa_<OS>_<ARCH> eval 'repeat("Foo", 3)'

Document

All built-in functions must be documented in docs/content/policy-reference.md under an appropriate subsection.

For this example, we add an entry for our new function under the Strings section:

  1. ### Strings
  2. | Built-in | Description | Wasm Support |
  3. | ------- |-------------|---------------|
  4. ...
  5. | <span class="opa-keep-it-together">``output := repeat(string, count)``</span> | ``output`` is ``string`` repeated ``count``times | ``SDK-dependent`` |
  6. ...