|
18 | 18 | import static com.google.common.collect.ImmutableList.toImmutableList; |
19 | 19 | import static java.util.stream.Collectors.toCollection; |
20 | 20 |
|
| 21 | +import com.google.common.base.Preconditions; |
21 | 22 | import com.google.common.collect.ImmutableList; |
22 | 23 | import com.google.common.collect.Lists; |
23 | 24 | import dev.cel.bundle.Cel; |
|
32 | 33 | import dev.cel.common.formats.ValueString; |
33 | 34 | import dev.cel.common.navigation.CelNavigableMutableAst; |
34 | 35 | import dev.cel.common.navigation.CelNavigableMutableExpr; |
| 36 | +import dev.cel.common.types.CelType; |
| 37 | +import dev.cel.common.types.CelTypes; |
35 | 38 | import dev.cel.extensions.CelOptionalLibrary.Function; |
36 | 39 | import dev.cel.optimizer.AstMutator; |
37 | 40 | import dev.cel.optimizer.CelAstOptimizer; |
38 | 41 | import dev.cel.policy.CelCompiledRule.CelCompiledMatch; |
39 | 42 | import dev.cel.policy.CelCompiledRule.CelCompiledMatch.OutputValue; |
| 43 | +import dev.cel.policy.CelCompiledRule.CelCompiledMatch.Result; |
40 | 44 | import dev.cel.policy.CelCompiledRule.CelCompiledVariable; |
41 | 45 | import java.util.ArrayList; |
42 | 46 | import java.util.Arrays; |
@@ -74,54 +78,70 @@ private Step optimizeRule(Cel cel, CelCompiledRule compiledRule) { |
74 | 78 | } |
75 | 79 |
|
76 | 80 | long lastOutputId = 0; |
| 81 | + // The expected output type of the rule, used to verify that all branches agree on the type. |
| 82 | + CelType lastOutputType = null; |
77 | 83 | for (CelCompiledMatch match : Lists.reverse(compiledRule.matches())) { |
78 | 84 | CelAbstractSyntaxTree conditionAst = match.condition(); |
79 | 85 | boolean isTriviallyTrue = match.isConditionTriviallyTrue(); |
80 | 86 | CelMutableAst condAst = CelMutableAst.fromCelAst(conditionAst); |
81 | 87 |
|
| 88 | + long currentSourceId = lastOutputId; |
| 89 | + |
82 | 90 | switch (match.result().kind()) { |
83 | 91 | case OUTPUT: |
84 | 92 | // If the match has an output, then it is considered a non-optional output since |
85 | 93 | // it is explicitly stated. If the rule itself is optional, then the base case value |
86 | 94 | // of output being optional.none() will convert the non-optional value to an optional |
87 | 95 | // one. |
88 | 96 | OutputValue matchOutput = match.result().output(); |
89 | | - CelMutableAst outAst = CelMutableAst.fromCelAst(matchOutput.ast()); |
90 | | - Step step = Step.newNonOptionalStep(!isTriviallyTrue, condAst, outAst); |
| 97 | + Step step = |
| 98 | + Step.newNonOptionalStep( |
| 99 | + !isTriviallyTrue, condAst, CelMutableAst.fromCelAst(matchOutput.ast())); |
| 100 | + currentSourceId = matchOutput.sourceId(); |
| 101 | + |
91 | 102 | output = combine(astMutator, step, output); |
92 | 103 |
|
93 | | - assertComposedAstIsValid( |
94 | | - cel, |
95 | | - output.expr, |
96 | | - "incompatible output types found.", |
97 | | - matchOutput.sourceId(), |
98 | | - lastOutputId); |
99 | | - lastOutputId = matchOutput.sourceId(); |
| 104 | + String outputFailureMessage = |
| 105 | + String.format( |
| 106 | + "incompatible output types: block has output type %s, but previous outputs have" |
| 107 | + + " type %s", |
| 108 | + lastOutputType == null ? "" : CelTypes.format(lastOutputType), |
| 109 | + CelTypes.format(matchOutput.ast().getResultType())); |
| 110 | + lastOutputType = |
| 111 | + assertComposedAstIsValid( |
| 112 | + cel, output.expr, outputFailureMessage, currentSourceId, lastOutputId) |
| 113 | + .getResultType(); |
| 114 | + |
100 | 115 | break; |
101 | 116 | case RULE: |
102 | 117 | // If the match has a nested rule, then compute the rule and whether it has |
103 | 118 | // an optional return value. |
104 | 119 | CelCompiledRule matchNestedRule = match.result().rule(); |
105 | 120 | Step nestedRule = optimizeRule(cel, matchNestedRule); |
106 | | - boolean nestedHasOptional = matchNestedRule.hasOptionalOutput(); |
107 | | - |
108 | 121 | Step ruleStep = |
109 | | - nestedHasOptional |
110 | | - ? Step.newOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr) |
111 | | - : Step.newNonOptionalStep(!isTriviallyTrue, condAst, nestedRule.expr); |
| 122 | + new Step( |
| 123 | + matchNestedRule.hasOptionalOutput(), !isTriviallyTrue, condAst, nestedRule.expr); |
| 124 | + currentSourceId = getFirstOutputSourceId(matchNestedRule); |
| 125 | + |
112 | 126 | output = combine(astMutator, ruleStep, output); |
113 | 127 |
|
114 | | - assertComposedAstIsValid( |
115 | | - cel, |
116 | | - output.expr, |
117 | | - String.format( |
118 | | - "failed composing the subrule '%s' due to incompatible output types.", |
119 | | - matchNestedRule.ruleId().map(ValueString::value).orElse("")), |
120 | | - lastOutputId); |
| 128 | + lastOutputType = |
| 129 | + assertComposedAstIsValid( |
| 130 | + cel, |
| 131 | + output.expr, |
| 132 | + String.format( |
| 133 | + "failed composing the subrule '%s' due to incompatible output types.", |
| 134 | + matchNestedRule.ruleId().map(ValueString::value).orElse("")), |
| 135 | + currentSourceId, |
| 136 | + lastOutputId) |
| 137 | + .getResultType(); |
121 | 138 | break; |
122 | 139 | } |
| 140 | + |
| 141 | + lastOutputId = currentSourceId; |
123 | 142 | } |
124 | 143 |
|
| 144 | + Preconditions.checkState(output != null, "Policy contains no outputs."); |
125 | 145 | CelMutableAst resultExpr = output.expr; |
126 | 146 | resultExpr = inlineCompiledVariables(resultExpr, compiledRule.variables()); |
127 | 147 | resultExpr = astMutator.renumberIdsConsecutively(resultExpr); |
@@ -266,21 +286,34 @@ private CelMutableAst inlineCompiledVariables( |
266 | 286 | return mutatedAst; |
267 | 287 | } |
268 | 288 |
|
269 | | - private void assertComposedAstIsValid( |
| 289 | + private CelAbstractSyntaxTree assertComposedAstIsValid( |
270 | 290 | Cel cel, CelMutableAst composedAst, String failureMessage, Long... ids) { |
271 | | - assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids)); |
| 291 | + return assertComposedAstIsValid(cel, composedAst, failureMessage, Arrays.asList(ids)); |
272 | 292 | } |
273 | 293 |
|
274 | | - private void assertComposedAstIsValid( |
| 294 | + private CelAbstractSyntaxTree assertComposedAstIsValid( |
275 | 295 | Cel cel, CelMutableAst composedAst, String failureMessage, List<Long> ids) { |
276 | 296 | try { |
277 | | - cel.check(composedAst.toParsedAst()).getAst(); |
| 297 | + return cel.check(composedAst.toParsedAst()).getAst(); |
278 | 298 | } catch (CelValidationException e) { |
279 | 299 | ids = ids.stream().filter(id -> id > 0).collect(toCollection(ArrayList::new)); |
280 | 300 | throw new RuleCompositionException(failureMessage, e, ids); |
281 | 301 | } |
282 | 302 | } |
283 | 303 |
|
| 304 | + private static long getFirstOutputSourceId(CelCompiledRule rule) { |
| 305 | + for (CelCompiledMatch match : rule.matches()) { |
| 306 | + if (match.result().kind() == Result.Kind.OUTPUT) { |
| 307 | + return match.result().output().sourceId(); |
| 308 | + } else if (match.result().kind() == Result.Kind.RULE) { |
| 309 | + return getFirstOutputSourceId(match.result().rule()); |
| 310 | + } |
| 311 | + } |
| 312 | + |
| 313 | + // Fallback to the nested rule ID if the policy is invalid and contains no output |
| 314 | + return rule.sourceId(); |
| 315 | + } |
| 316 | + |
284 | 317 | // Step represents an intermediate stage of rule and match expression composition. |
285 | 318 | // |
286 | 319 | // The CelCompiledRule and CelCompiledMatch types are meant to represent standalone tuples of |
@@ -311,11 +344,6 @@ private Step( |
311 | 344 | this.expr = expr; |
312 | 345 | } |
313 | 346 |
|
314 | | - private static Step newOptionalStep( |
315 | | - boolean isConditional, CelMutableAst cond, CelMutableAst expr) { |
316 | | - return new Step(/* isOptional= */ true, isConditional, cond, expr); |
317 | | - } |
318 | | - |
319 | 347 | private static Step newNonOptionalStep( |
320 | 348 | boolean isConditional, CelMutableAst cond, CelMutableAst expr) { |
321 | 349 | return new Step(/* isOptional= */ false, isConditional, cond, expr); |
|
0 commit comments