prefer-some-in-iteration
Summary: Prefer some .. in
for iteration
Category: Style
Avoid
package policy
import rego.v1
engineering_roles := {"engineer", "dba", "developer"}
engineers contains employee if {
employee := data.employees[_]
employee.role in engineering_roles
}
Prefer
package policy
import rego.v1
engineering_roles := {"engineer", "dba", "developer"}
engineers contains employee if {
some employee in data.employees
employee.role in engineering_roles
}
Rationale
Using the some .. in
construct for iteration removes ambiguity around iteration vs. membership checks, and is
generally more pleasant to read. Consider the following example:
some_condition if {
other_rule[user]
# ...
}
Are we iterating users over a partial "other_rule" here, or checking if the set contains a user defined elsewhere?
Or is other_rule
a map-generating rule, and we're checking for the existence of a key? We won't know without looking
elsewhere in the code. Using some .. in
removes this ambiguity, and makes the intent clear without having to jump
around in the policy.
Improved readability is not the only benefit of using some .. in
. The some
keyword ensures that the bindings
following the keyword are bound to the local scope, and modifications outside of e.g. a rule body won't affect how the
variables are evaluated. Consider the following simplified example to iterate over the keys of a map:
package policy
key_traversal if {
map[key]
# do something with key
}
key_traversal if {
some key in object.keys(map)
# do something with key
}
The two rules above are equivalent in that they both bind the variable key
to the keys of map
. The first
example would however change behavior entirely if a rule named key
was introduced in the package, as the expression
would then mean "does map have key key
?". While this isn't common, using some .. in
means one less thing to worry
about.
Exceptions
Deeply nested iteration is often easier to read using the more compact form.
package policy
import rego.v1
# These rules are equivalent, but the more compact form is arguably easier to read
any_user_is_admin if {
some user in input.users
some attribute in user.attributes
some role in attribute.roles
role == "admin"
}
any_user_is_admin if {
input.users[_].attributes[_].roles[_] == "admin"
}
# Using "if", we may even omit the brackets for single line rules
any_user_is_admin if input.users[_].attributes[_].roles[_] == "admin"
The ignore-nesting-level
configuration option allows setting the threshold for nesting. Any level of nesting
equal or greater than the threshold won't be considered a violation. The default setting of 2
allows all nested
iteration, but not e.g. my_array[x]
.
Note: not all nesting is iteration! The following example is considered to have a nesting level of 1
, as only
one of the variables (including wildcards: _
) is an output variable bound in iteration:
package policy
import rego.v1
example_users contains user if {
domain := "example.com"
user := input.sites[domain].users[_]
}
Configuration Options
This linter rule provides the following configuration options:
rules:
style:
prefer-some-in-iteration:
# one of "error", "warning", "ignore"
level: error
# except iteration if nested at or above the level i.e. setting of
# '2' will allow `input[_].users[_]` but not `input[_]`
ignore-nesting-level: 2
# except iteration over items with sub-attributes, like
# `name := input.users[_].name`
# default is true
ignore-if-sub-attribute: true
Related Resources
- Rego Style Guide: Prefer some .. in for iteration
- Regal Docs: Use
some
to declare output variables - OPA Docs: Membership and Iteration:
in
- OPA Docs: Some Keyword
- GitHub: Source Code
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!