Skip to main content

use-some-for-output-vars

Summary: Use some to declare output variables

Category: Idiomatic

Avoid

package policy

allow {
userinfo := data.users[id]
# ...
}

Prefer

allow {
some id
userinfo := data.users[id]
# ...
}

# alternatively, and arguably more idiomatic:
allow {
some id, userinfo in data.users
# ...
}

Rationale

An interesting, and likely unfamiliar aspect of Rego for developers coming from other languages, is the concept of unification. Unification happens not just explicitly via the unification operator (=), but is an integral part of Rego. In the context of this rule, unification means that a variable can either be an input or an output. What does that mean?

From the example above, consider that data.users is a map of user IDs to user objects:

{
"jane": {"email": "jane@acmecorp.com", "firstname": "Jane", "lastname": "Doe"},
"joe": {"email": "joe@example.com", "firstname": "Joe", "lastname": "Bloggs"},
"john": {"email": "john@opa.org", "firstname": "John", "lastname": "Smith"}
}
usernames contains name if {
data.users[name]
}

What is the meaning of name in the body of the usernames rule? In most programming languages, evaluation would fail unless name was defined elsewhere in the code. That is because name would be expected to be an input in the expression — the result of using, say "joe", as the input in users["joe"] would predictably be the value associated with that key. In Rego, however, name may also be an output — meaning that if the variable is not defined elsewhere, OPA will attempt to unify it with any value that satisfies the expression. In this case, that means that name will be bound to each of the keys in the users object in turn, and the rule will succeed for each of them.

This is a powerful feature of Rego, but it can also be a source of confusion. If we were to define name somewhere else in the policy, perhaps by mistake:

name := "joe"

# hundreds of lines of Rego later..

usernames contains name if {
data.users[name]
}

Our usernames rule would no longer iterate over all the users, as the condition would be satisfied by simply mapping the key "joe" to its value. By using some to locally declare name, we can avoid this problem:

name := "joe"

# hundreds of lines of Rego later..

usernames contains name if {
some name
data.users[name]
}

Even though name is defined in the global scope, the some keyword will ensure it's now considered as an output variable in the local scope of the usernames rule.

Configuration Options

This linter rule provides the following configuration options:

rules:
idiomatic:
use-some-for-output-vars:
# one of "error", "warning", "ignore"
level: error

Community

If you think you've found a problem with this rule or its documentation, would like to suggest improvements, new rules, or just talk about Regal in general, please join us in the #regal channel in the Styra Community Slack!