Skip to content
Closed
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
2 changes: 1 addition & 1 deletion cel/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ func configToEnvOptions(config *env.Config, provider types.Provider, optFactorie

// Configure the context variable declaration
if config.ContextVariable != nil {
typeName := config.ContextVariable.TypeName
typeName := config.ContextVariable.GetType()
if _, found := provider.FindStructType(typeName); !found {
return nil, fmt.Errorf("invalid context proto type: %q", typeName)
}
Expand Down
9 changes: 9 additions & 0 deletions common/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,15 @@ type ContextVariable struct {
// TypeName represents the fully qualified typename of the context variable.
// Currently, only protobuf types are supported.
TypeName string `yaml:"type_name"`
// Alternative spelling.
Type string `yaml:"type"`
}

func (c *ContextVariable) GetType() string {
if c.TypeName != "" {
return c.TypeName
}
return c.Type
}

// Validate validates the context-variable configuration is well-formed.
Expand Down
27 changes: 27 additions & 0 deletions policy/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,33 @@ cel_go_test(
test_suite = "testdata/nested_rules_variable_shadowing/tests.yaml",
)

cel_go_test(
name = "nested_rules_unconditional_chaining_policy",
cel_expr = "testdata/nested_rules_unconditional_chaining/policy.yaml",
config = "testdata/nested_rules_unconditional_chaining/config.yaml",
enable_coverage = True,
test_src = "//policy/test:cel_test_runner.go",
test_suite = "testdata/nested_rules_unconditional_chaining/tests.yaml",
)

cel_go_test(
name = "nested_rules_unconditional_chaining_optional_policy",
cel_expr = "testdata/nested_rules_unconditional_chaining_optional/policy.yaml",
config = "testdata/nested_rules_unconditional_chaining_optional/config.yaml",
enable_coverage = True,
test_src = "//policy/test:cel_test_runner.go",
test_suite = "testdata/nested_rules_unconditional_chaining_optional/tests.yaml",
)

cel_go_test(
name = "nested_rules_unwrap_rewrap_policy",
cel_expr = "testdata/nested_rules_unwrap_rewrap/policy.yaml",
config = "testdata/nested_rules_unwrap_rewrap/config.yaml",
enable_coverage = True,
test_src = "//policy/test:cel_test_runner.go",
test_suite = "testdata/nested_rules_unwrap_rewrap/tests.yaml",
)

cel_go_test(
name = "variable_type_propagation_policy",
cel_expr = "testdata/variable_type_propagation/policy.yaml",
Expand Down
50 changes: 50 additions & 0 deletions policy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,56 @@ rule:
- output: "'outer_default'"
```

##### Using Unconditional Nested Rules

In a first-match policy, a nested rule may be specified without an
explicit condition (or the condition set as exactly `true`). This is an
unconditional rule.

Unconditional rules are useful for two primary purposes:

_Scoping Variables_

Unconditional rules may declare variables (like foo in the example below) that
only apply to a specific group of matches. This avoids cluttering the global
scope.

_Fallback Behavior (Chaining)_

The inner matches inside an unconditional rule do not need to cover every
possible scenario. If an input runs through the inner conditions and doesn't
find a match, the engine doesn't fail or return an optional none value.
Instead, it steps back out to the parent rule and continues down the list.

Important: if an unconditional nested rule covers every possible scenario
(meaning it is exhaustive and will always generate an output), it must be the
last item in the parent rule's match list. Placing it anywhere else will
create dead code, as the engine can never reach the matches written beneath
it.

Example:

```
# Policy is a string (not wrapped)
# input -> output
# 3 -> 'b'
# 2 -> 'c'
rule:
match:
- rule:
variables:
- name: "foo"
expression: "3"
match:
- condition: "input > variables.foo"
output: '"a"'
- condition: "input == variables.foo"
output: '"b"'
- output: '"c"'

```


### Imports

When constructing complex object types such as protocol buffers, `imports` can
Expand Down
19 changes: 13 additions & 6 deletions policy/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,17 @@ func (r *CompiledRule) HasOptionalOutput() bool {
optionalOutput := false
for _, m := range r.Matches() {
if m.NestedRule() != nil && m.NestedRule().HasOptionalOutput() {
return true
}
if m.ConditionIsLiteral(types.True) {
// If the nested rule is unconditional, the matching may fallthrough to the next match
// in this context (unwrapping the optional value from the nested rule).
if !m.ConditionIsLiteral(types.True) {
return true
}
optionalOutput = true
} else if m.ConditionIsLiteral(types.True) {
return false
} else {
optionalOutput = true
}
optionalOutput = true
}
return optionalOutput
}
Expand Down Expand Up @@ -442,14 +447,16 @@ func (c *compiler) checkMatchOutputTypesAgree(rule *CompiledRule, iss *cel.Issue
}

func (c *compiler) checkUnreachableCode(rule *CompiledRule, iss *cel.Issues) {
ruleHasOptional := rule.HasOptionalOutput()
compiledMatches := rule.Matches()
matchCount := len(compiledMatches)
for i := matchCount - 1; i >= 0; i-- {
m := compiledMatches[i]
triviallyTrue := m.ConditionIsLiteral(types.True)

if triviallyTrue && !ruleHasOptional && i != matchCount-1 {
// If the match is a single output or a nested rule that always returns a value, it is
// exhaustive. If the condition is trivially true, then all subsequent branches are unreachable.
isExhaustive := triviallyTrue && (m.NestedRule() == nil || !m.NestedRule().HasOptionalOutput())
if isExhaustive && i != matchCount-1 {
if m.Output() != nil {
iss.ReportErrorAtID(m.SourceID(), "match creates unreachable outputs")
}
Expand Down
19 changes: 18 additions & 1 deletion policy/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@ func TestCompiledRuleHasOptionalOutput(t *testing.T) {
},
optional: true,
},
{
rule: &CompiledRule{
matches: []*CompiledMatch{
{
cond: mustCompileExpr(t, env, "true"),
nestedRule: &CompiledRule{
matches: []*CompiledMatch{{cond: mustCompileExpr(t, env, "1 > 0")}},
},
},
{cond: mustCompileExpr(t, env, "true")},
},
},
optional: false,
},
}
for _, tst := range tests {
got := tst.rule.HasOptionalOutput()
Expand Down Expand Up @@ -384,6 +398,9 @@ func (r *runner) run(t *testing.T) {
} else if tc.Output.Value != nil {
testOut = r.env.CELTypeAdapter().NativeToValue(tc.Output.Value)
}
if testOut.Equal(out) == types.True {
return
}
if optOut, ok := out.(*types.Optional); ok {
if optOut.Equal(types.OptionalNone) == types.True {
if testOut.Equal(types.OptionalNone) != types.True {
Expand All @@ -392,7 +409,7 @@ func (r *runner) run(t *testing.T) {
} else if testOut.Equal(optOut.GetValue()) != types.True {
t.Errorf("policy eval got %v, wanted %v", out, testOut)
}
} else if testOut.Equal(out) != types.True {
} else {
t.Errorf("policy eval got %v, wanted %v", out, testOut)
}
})
Expand Down
21 changes: 21 additions & 0 deletions policy/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,27 @@ var (
: optional.none())))
: optional.of(@index3.format([@index0, @index2])))`,
},
{
name: "nested_rules_unconditional_chaining",
expr: `
cel.@block([3],
((x > @index0) ? optional.of("a") : ((x == @index0) ? optional.of("b") : optional.none()))
.orValue("c"))`,
},
{
name: "nested_rules_unconditional_chaining_optional",
expr: `
cel.@block([3],
((x > @index0) ? optional.of("a") : ((x == @index0) ? optional.of("b") : optional.none()))
.or((x == 1) ? optional.of("c") : optional.none()))`,
},
{
name: "nested_rules_unwrap_rewrap",
expr: `
(x == 1)
? optional.of(((y == 1) ? optional.of("a") : optional.none()).orValue("b"))
: optional.none()`,
},
}

composerUnnestTests = []struct {
Expand Down
18 changes: 18 additions & 0 deletions policy/testdata/nested_rules_unconditional_chaining/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: nested_rules_unconditional_chaining
variables:
- name: x
type: int
27 changes: 27 additions & 0 deletions policy/testdata/nested_rules_unconditional_chaining/policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: nested_rules_unconditional_chaining
rule:
match:
- rule:
variables:
- name: foo
expression: "3"
match:
- condition: "x > variables.foo"
output: "'a'"
- condition: "x == variables.foo"
output: "'b'"
- output: "'c'"
36 changes: 36 additions & 0 deletions policy/testdata/nested_rules_unconditional_chaining/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

description: "Nested rule tests for unconditional chaining"
section:
- name: "chaining"
tests:
- name: "sub_rule_match_greater"
input:
x:
expr: "4"
output:
value: "a"
- name: "sub_rule_match_equal"
input:
x:
expr: "3"
output:
value: "b"
- name: "parent_rule_fallback"
input:
x:
expr: "2"
output:
value: "c"
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: nested_rules_unconditional_chaining_optional
variables:
- name: x
type: int
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: nested_rules_unconditional_chaining_optional
rule:
match:
- rule:
variables:
- name: foo
expression: "3"
match:
- condition: "x > variables.foo"
output: "'a'"
- condition: "x == variables.foo"
output: "'b'"
- condition: "x == 1"
output: "'c'"
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

description: "Nested rule tests for unconditional chaining where parent is optional"
section:
- name: "chaining"
tests:
- name: "sub_rule_match_greater"
input:
x:
expr: "4"
output:
value: "a"
- name: "sub_rule_match_equal"
input:
x:
expr: "3"
output:
value: "b"
- name: "parent_rule_match"
input:
x:
expr: "1"
output:
value: "c"
- name: "parent_rule_fallback_none"
input:
x:
expr: "2"
output:
expr: "optional.none()"
Loading