diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java index 8614b87b5..b85f16cb1 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironment.java @@ -43,6 +43,7 @@ import dev.cel.common.types.OptionalType; import dev.cel.common.types.SimpleType; import dev.cel.common.types.TypeParamType; +import dev.cel.common.types.TypeType; import dev.cel.compiler.CelCompiler; import dev.cel.compiler.CelCompilerBuilder; import dev.cel.compiler.CelCompilerLibrary; @@ -71,27 +72,28 @@ public abstract class CelEnvironment { "math", CanonicalCelExtension.MATH, "optional", CanonicalCelExtension.OPTIONAL, "protos", CanonicalCelExtension.PROTOS, + "regex", CanonicalCelExtension.REGEX, "sets", CanonicalCelExtension.SETS, "strings", CanonicalCelExtension.STRINGS, - "comprehensions", CanonicalCelExtension.COMPREHENSIONS); + "two-var-comprehensions", CanonicalCelExtension.COMPREHENSIONS); private static final ImmutableMap> LIMIT_HANDLERS = ImmutableMap.of( "cel.limit.expression_code_points", - (options, value) -> options.maxExpressionCodePointSize(value), + CelOptions.Builder::maxExpressionCodePointSize, "cel.limit.parse_error_recovery", - (options, value) -> options.maxParseErrorRecoveryLimit(value), + CelOptions.Builder::maxParseErrorRecoveryLimit, "cel.limit.parse_recursion_depth", - (options, value) -> options.maxParseRecursionDepth(value)); + CelOptions.Builder::maxParseRecursionDepth); private static final ImmutableMap FEATURE_HANDLERS = ImmutableMap.of( "cel.feature.macro_call_tracking", - (options, enabled) -> options.populateMacroCalls(enabled), + CelOptions.Builder::populateMacroCalls, "cel.feature.backtick_escape_syntax", - (options, enabled) -> options.enableQuotedIdentifierSyntax(enabled), + CelOptions.Builder::enableQuotedIdentifierSyntax, "cel.feature.cross_type_numeric_comparisons", - (options, enabled) -> options.enableHeterogeneousNumericComparisons(enabled)); + CelOptions.Builder::enableHeterogeneousNumericComparisons); /** Environment source in textual format (ex: textproto, YAML). */ public abstract Optional source(); @@ -99,10 +101,8 @@ public abstract class CelEnvironment { /** Name of the environment. */ public abstract String name(); - /** - * Container, which captures default namespace and aliases for value resolution. - */ - public abstract CelContainer container(); + /** Container, which captures default namespace and aliases for value resolution. */ + public abstract Optional container(); /** * An optional description of the environment (example: location of the file containing the config @@ -226,7 +226,6 @@ public static Builder newBuilder() { return new AutoValue_CelEnvironment.Builder() .setName("") .setDescription("") - .setContainer(CelContainer.ofName("")) .setVariables(ImmutableSet.of()) .setFunctions(ImmutableSet.of()) .setFeatures(ImmutableSet.of()) @@ -242,7 +241,6 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) CelCompilerBuilder compilerBuilder = celCompiler .toCompilerBuilder() - .setContainer(container()) .setOptions(celOptions) .setTypeProvider(celTypeProvider) .addVarDeclarations( @@ -254,6 +252,8 @@ public CelCompiler extend(CelCompiler celCompiler, CelOptions celOptions) .map(f -> f.toCelFunctionDecl(celTypeProvider)) .collect(toImmutableList())); + container().ifPresent(compilerBuilder::setContainer); + addAllCompilerExtensions(compilerBuilder, celOptions); applyStandardLibrarySubset(compilerBuilder); @@ -416,6 +416,8 @@ public abstract static class VariableDecl { /** The type of the variable. */ public abstract TypeDecl type(); + public abstract Optional description(); + /** Builder for {@link VariableDecl}. */ @AutoValue.Builder public abstract static class Builder implements RequiredFieldsChecker { @@ -428,6 +430,8 @@ public abstract static class Builder implements RequiredFieldsChecker { public abstract VariableDecl.Builder setType(TypeDecl typeDecl); + public abstract VariableDecl.Builder setDescription(String name); + @Override public ImmutableList requiredFields() { return ImmutableList.of( @@ -459,6 +463,8 @@ public abstract static class FunctionDecl { public abstract String name(); + public abstract Optional description(); + public abstract ImmutableSet overloads(); /** Builder for {@link FunctionDecl}. */ @@ -471,6 +477,8 @@ public abstract static class Builder implements RequiredFieldsChecker { public abstract FunctionDecl.Builder setName(String name); + public abstract FunctionDecl.Builder setDescription(String description); + public abstract FunctionDecl.Builder setOverloads(ImmutableSet overloads); @Override @@ -519,6 +527,9 @@ public abstract static class OverloadDecl { /** List of function overload type values. */ public abstract ImmutableList arguments(); + /** Examples for the overload. */ + public abstract ImmutableList examples(); + /** Return type of the overload. Required. */ public abstract TypeDecl returnType(); @@ -537,8 +548,21 @@ public abstract static class Builder implements RequiredFieldsChecker { // This should stay package-private to encourage add/set methods to be used instead. abstract ImmutableList.Builder argumentsBuilder(); + abstract ImmutableList.Builder examplesBuilder(); + public abstract OverloadDecl.Builder setArguments(ImmutableList args); + @CanIgnoreReturnValue + public OverloadDecl.Builder addExamples(Iterable examples) { + this.examplesBuilder().addAll(checkNotNull(examples)); + return this; + } + + @CanIgnoreReturnValue + public OverloadDecl.Builder addExamples(String... examples) { + return addExamples(Arrays.asList(examples)); + } + @CanIgnoreReturnValue public OverloadDecl.Builder addArguments(Iterable args) { this.argumentsBuilder().addAll(checkNotNull(args)); @@ -667,6 +691,10 @@ public CelType toCelType(CelTypeProvider celTypeProvider) { CelType keyType = params().get(0).toCelType(celTypeProvider); CelType valueType = params().get(1).toCelType(celTypeProvider); return MapType.create(keyType, valueType); + case "type": + checkState( + params().size() == 1, "Expected 1 parameter for type, got %s", params().size()); + return TypeType.create(params().get(0).toCelType(celTypeProvider)); default: if (isTypeParam()) { return TypeParamType.create(name()); @@ -838,6 +866,7 @@ enum CanonicalCelExtension { SETS( (options, version) -> CelExtensions.sets(options), (options, version) -> CelExtensions.sets(options)), + REGEX((options, version) -> CelExtensions.regex(), (options, version) -> CelExtensions.regex()), LISTS((options, version) -> CelExtensions.lists(), (options, version) -> CelExtensions.lists()), COMPREHENSIONS( (options, version) -> CelExtensions.comprehensions(), @@ -1054,7 +1083,7 @@ public static OverloadSelector.Builder newBuilder() { } @FunctionalInterface - private static interface BooleanOptionConsumer { + private interface BooleanOptionConsumer { void accept(CelOptions.Builder options, boolean value); } } diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java index ce8857654..f129d9f5d 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java @@ -353,6 +353,9 @@ private VariableDecl parseVariable(ParserContext ctx, Node node) { case "name": builder.setName(newString(ctx, valueNode)); break; + case "description": + builder.setDescription(newString(ctx, valueNode)); + break; case "type": if (typeDeclBuilder != null) { ctx.reportError( @@ -428,6 +431,9 @@ private FunctionDecl parseFunction(ParserContext ctx, Node node) { case "overloads": builder.setOverloads(parseOverloads(ctx, valueNode)); break; + case "description": + builder.setDescription(newString(ctx, valueNode).trim()); + break; default: ctx.reportError(keyId, String.format("Unsupported function tag: %s", keyName)); break; @@ -479,6 +485,9 @@ private static ImmutableSet parseOverloads(ParserContext ctx case "target": overloadDeclBuilder.setTarget(parseTypeDecl(ctx, valueNode)); break; + case "examples": + overloadDeclBuilder.addExamples(parseOverloadExamples(ctx, valueNode)); + break; default: ctx.reportError(keyId, String.format("Unsupported overload tag: %s", fieldName)); break; @@ -494,6 +503,25 @@ private static ImmutableSet parseOverloads(ParserContext ctx return overloadSetBuilder.build(); } + private static ImmutableList parseOverloadExamples(ParserContext ctx, Node node) { + long listValueId = ctx.collectMetadata(node); + if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) { + return ImmutableList.of(); + } + SequenceNode paramsListNode = (SequenceNode) node; + ImmutableList.Builder builder = ImmutableList.builder(); + for (Node elementNode : paramsListNode.getValue()) { + long elementNodeId = ctx.collectMetadata(elementNode); + if (!assertYamlType(ctx, elementNodeId, elementNode, YamlNodeType.STRING)) { + continue; + } + + builder.add(((ScalarNode) elementNode).getValue()); + } + + return builder.build(); + } + private static ImmutableList parseOverloadArguments( ParserContext ctx, Node node) { long listValueId = ctx.collectMetadata(node); diff --git a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java index 179faf2ac..9d5b4b69e 100644 --- a/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java +++ b/bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlSerializer.java @@ -79,10 +79,8 @@ public Node representData(Object data) { if (!environment.description().isEmpty()) { configMap.put("description", environment.description()); } - if (!environment.container().name().isEmpty() - || !environment.container().abbreviations().isEmpty() - || !environment.container().aliases().isEmpty()) { - configMap.put("container", environment.container()); + if (environment.container().isPresent()) { + configMap.put("container", environment.container().get()); } if (!environment.extensions().isEmpty()) { configMap.put("extensions", environment.extensions().asList()); diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java index 10b9dee8e..ae0de2c18 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentExporterTest.java @@ -333,7 +333,7 @@ public void container() { CelEnvironmentExporter exporter = CelEnvironmentExporter.newBuilder().build(); CelEnvironment celEnvironment = exporter.export(cel); - CelContainer container = celEnvironment.container(); + CelContainer container = celEnvironment.container().get(); assertThat(container.name()).isEqualTo("cntnr"); assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux").inOrder(); assertThat(container.aliases()).containsAtLeast("nm", "user.name", "id", "user.id").inOrder(); @@ -368,4 +368,3 @@ public void options() { CelEnvironment.Limit.create("cel.limit.parse_recursion_depth", 10)); } } - diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java index f7eb254d7..3a352cc15 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentTest.java @@ -44,9 +44,7 @@ public void newBuilder_defaults() { assertThat(environment.source()).isEmpty(); assertThat(environment.name()).isEmpty(); assertThat(environment.description()).isEmpty(); - assertThat(environment.container().name()).isEmpty(); - assertThat(environment.container().abbreviations()).isEmpty(); - assertThat(environment.container().aliases()).isEmpty(); + assertThat(environment.container()).isEmpty(); assertThat(environment.extensions()).isEmpty(); assertThat(environment.variables()).isEmpty(); assertThat(environment.functions()).isEmpty(); @@ -65,10 +63,10 @@ public void container() { .build()) .build(); - assertThat(environment.container().name()).isEqualTo("cntr"); - assertThat(environment.container().abbreviations()).containsExactly("foo.Bar", "baz.Qux"); - assertThat(environment.container().aliases()) - .containsExactly("nm", "user.name", "id", "user.id"); + CelContainer container = environment.container().get(); + assertThat(container.name()).isEqualTo("cntr"); + assertThat(container.abbreviations()).containsExactly("foo.Bar", "baz.Qux"); + assertThat(container.aliases()).containsExactly("nm", "user.name", "id", "user.id"); } @Test @@ -81,9 +79,10 @@ public void extend_allExtensions() throws Exception { ExtensionConfig.latest("math"), ExtensionConfig.latest("optional"), ExtensionConfig.latest("protos"), + ExtensionConfig.latest("regex"), ExtensionConfig.latest("sets"), ExtensionConfig.latest("strings"), - ExtensionConfig.latest("comprehensions")); + ExtensionConfig.latest("two-var-comprehensions")); CelEnvironment environment = CelEnvironment.newBuilder().addExtensions(extensionConfigs).build(); diff --git a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java index e98f6110e..043664e8e 100644 --- a/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java +++ b/bundle/src/test/java/dev/cel/bundle/CelEnvironmentYamlParserTest.java @@ -675,9 +675,7 @@ private enum EnvironmentParseErrorTestcase { + " | - version: 0\n" + " | ..^"), ILLEGAL_LIBRARY_SUBSET_TAG( - "name: 'test_suite_name'\n" - + "stdlib:\n" - + " unknown_tag: 'test_value'\n", + "name: 'test_suite_name'\n" + "stdlib:\n" + " unknown_tag: 'test_value'\n", "ERROR: :3:3: Unsupported library subset tag: unknown_tag\n" + " | unknown_tag: 'test_value'\n" + " | ..^"), @@ -859,30 +857,40 @@ private enum EnvironmentYamlResourceTestCase { .setVariables( VariableDecl.newBuilder() .setName("msg") + .setDescription( + "msg represents all possible type permutation which CEL understands from a" + + " proto perspective") .setType(TypeDecl.create("cel.expr.conformance.proto3.TestAllTypes")) .build()) .setFunctions( - FunctionDecl.create( - "isEmpty", - ImmutableSet.of( - OverloadDecl.newBuilder() - .setId("wrapper_string_isEmpty") - .setTarget(TypeDecl.create("google.protobuf.StringValue")) - .setReturnType(TypeDecl.create("bool")) - .build(), - OverloadDecl.newBuilder() - .setId("list_isEmpty") - .setTarget( - TypeDecl.newBuilder() - .setName("list") - .addParams( - TypeDecl.newBuilder() - .setName("T") - .setIsTypeParam(true) - .build()) - .build()) - .setReturnType(TypeDecl.create("bool")) - .build()))) + FunctionDecl.newBuilder() + .setName("isEmpty") + .setDescription( + "determines whether a list is empty,\nor a string has no characters") + .setOverloads( + ImmutableSet.of( + OverloadDecl.newBuilder() + .setId("wrapper_string_isEmpty") + .setTarget(TypeDecl.create("google.protobuf.StringValue")) + .addExamples("''.isEmpty() // true") + .setReturnType(TypeDecl.create("bool")) + .build(), + OverloadDecl.newBuilder() + .setId("list_isEmpty") + .addExamples("[].isEmpty() // true") + .addExamples("[1].isEmpty() // false") + .setTarget( + TypeDecl.newBuilder() + .setName("list") + .addParams( + TypeDecl.newBuilder() + .setName("T") + .setIsTypeParam(true) + .build()) + .build()) + .setReturnType(TypeDecl.create("bool")) + .build())) + .build()) .setFeatures(CelEnvironment.FeatureFlag.create("cel.feature.macro_call_tracking", true)) .setLimits( ImmutableSet.of( diff --git a/common/src/main/java/dev/cel/common/formats/ParserContext.java b/common/src/main/java/dev/cel/common/formats/ParserContext.java index 0bdfdb299..17eff473f 100644 --- a/common/src/main/java/dev/cel/common/formats/ParserContext.java +++ b/common/src/main/java/dev/cel/common/formats/ParserContext.java @@ -42,6 +42,32 @@ public interface ParserContext { Map getIdToOffsetMap(); - /** NewString creates a new ValueString from the YAML node. */ - ValueString newValueString(T node); + /** + * @deprecated Use {@link #newSourceString} instead. + */ + @Deprecated + default ValueString newValueString(T node) { + return newSourceString(node); + } + + /** + * NewYamlString creates a new ValueString from the YAML node, evaluated according to standard + * YAML parsing rules. + * + *

This respects the whitespace folding semantics defined by the node's scalar style (e.g., + * folded string {@code >} versus literal string {@code |}). Use this method for general string + * fields such as {@code description}, {@code name}, or {@code id}. + */ + ValueString newYamlString(T node); + + /** + * NewRawString creates a new ValueString from the YAML node, preserving formatting for accurate + * source mapping. + * + *

This extracts the verbatim text directly from the source file, preserving raw block + * indentation and unmodified newlines. Use this method when the string represents code or a CEL + * expression where precise character-level offsets must be maintained for accurate diagnostic + * error reporting. + */ + ValueString newSourceString(T node); } diff --git a/common/src/main/java/dev/cel/common/formats/YamlHelper.java b/common/src/main/java/dev/cel/common/formats/YamlHelper.java index e0780b01f..c16126f95 100644 --- a/common/src/main/java/dev/cel/common/formats/YamlHelper.java +++ b/common/src/main/java/dev/cel/common/formats/YamlHelper.java @@ -136,7 +136,7 @@ public static boolean newBoolean(ParserContext ctx, Node node) { } public static String newString(ParserContext ctx, Node node) { - return ctx.newValueString(node).value(); + return ctx.newYamlString(node).value(); } private YamlHelper() {} diff --git a/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java index 456872803..9f6077562 100644 --- a/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java +++ b/common/src/main/java/dev/cel/common/formats/YamlParserContextImpl.java @@ -62,7 +62,18 @@ public Map getIdToOffsetMap() { } @Override - public ValueString newValueString(Node node) { + public ValueString newYamlString(Node node) { + long id = collectMetadata(node); + if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) { + return ValueString.of(id, ERROR); + } + + ScalarNode scalarNode = (ScalarNode) node; + return ValueString.of(id, scalarNode.getValue()); + } + + @Override + public ValueString newSourceString(Node node) { long id = collectMetadata(node); if (!assertYamlType(this, id, node, YamlNodeType.STRING, YamlNodeType.TEXT)) { return ValueString.of(id, ERROR); diff --git a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java index 43595c4ab..18b406af0 100644 --- a/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java +++ b/policy/src/main/java/dev/cel/policy/CelPolicyYamlParser.java @@ -126,13 +126,13 @@ public CelPolicy parsePolicy(PolicyParserContext ctx, Node node) { parseImports(policyBuilder, ctx, valueNode); break; case "name": - policyBuilder.setName(ctx.newValueString(valueNode)); + policyBuilder.setName(ctx.newYamlString(valueNode)); break; case "description": - policyBuilder.setDescription(ctx.newValueString(valueNode)); + policyBuilder.setDescription(ctx.newYamlString(valueNode)); break; case "display_name": - policyBuilder.setDisplayName(ctx.newValueString(valueNode)); + policyBuilder.setDisplayName(ctx.newYamlString(valueNode)); break; case "rule": policyBuilder.setRule(parseRule(ctx, policyBuilder, valueNode)); @@ -189,7 +189,7 @@ private void parseImport( continue; } - policyBuilder.addImport(Import.create(valueId, ctx.newValueString(value))); + policyBuilder.addImport(Import.create(valueId, ctx.newYamlString(value))); } } @@ -212,10 +212,10 @@ public CelPolicy.Rule parseRule( Node value = nodeTuple.getValueNode(); switch (fieldName) { case "id": - ruleBuilder.setRuleId(ctx.newValueString(value)); + ruleBuilder.setRuleId(ctx.newYamlString(value)); break; case "description": - ruleBuilder.setDescription(ctx.newValueString(value)); + ruleBuilder.setDescription(ctx.newYamlString(value)); break; case "variables": ruleBuilder.addVariables(parseVariables(ctx, policyBuilder, value)); @@ -267,7 +267,7 @@ public CelPolicy.Match parseMatch( Node value = nodeTuple.getValueNode(); switch (fieldName) { case "condition": - matchBuilder.setCondition(ctx.newValueString(value)); + matchBuilder.setCondition(ctx.newSourceString(value)); break; case "output": matchBuilder @@ -275,7 +275,7 @@ public CelPolicy.Match parseMatch( .filter(result -> result.kind().equals(Match.Result.Kind.RULE)) .ifPresent( result -> ctx.reportError(tagId, "Only the rule or the output may be set")); - matchBuilder.setResult(Match.Result.ofOutput(ctx.newValueString(value))); + matchBuilder.setResult(Match.Result.ofOutput(ctx.newSourceString(value))); break; case "explanation": matchBuilder @@ -286,7 +286,7 @@ public CelPolicy.Match parseMatch( ctx.reportError( tagId, "Explanation can only be set on output match cases, not nested rules")); - matchBuilder.setExplanation(ctx.newValueString(value)); + matchBuilder.setExplanation(ctx.newYamlString(value)); break; case "rule": matchBuilder @@ -356,8 +356,8 @@ private Variable parseVariableInline( Node keyNode = nodeTuple.getKeyNode(); long keyId = ctx.collectMetadata(keyNode); builder - .setName(ctx.newValueString(keyNode)) - .setExpression(ctx.newValueString(nodeTuple.getValueNode())); + .setName(ctx.newYamlString(keyNode)) + .setExpression(ctx.newSourceString(nodeTuple.getValueNode())); iterations++; if (iterations > 1) { @@ -385,16 +385,16 @@ private Variable parseVariableObject( String keyName = ((ScalarNode) keyNode).getValue(); switch (keyName) { case "name": - builder.setName(ctx.newValueString(valueNode)); + builder.setName(ctx.newYamlString(valueNode)); break; case "expression": - builder.setExpression(ctx.newValueString(valueNode)); + builder.setExpression(ctx.newSourceString(valueNode)); break; case "description": - builder.setDescription(ctx.newValueString(valueNode)); + builder.setDescription(ctx.newYamlString(valueNode)); break; case "display_name": - builder.setDisplayName(ctx.newValueString(valueNode)); + builder.setDisplayName(ctx.newYamlString(valueNode)); break; default: tagVisitor.visitVariableTag(ctx, keyId, keyName, valueNode, policyBuilder, builder); @@ -449,8 +449,13 @@ public Map getIdToOffsetMap() { } @Override - public ValueString newValueString(Node node) { - return ctx.newValueString(node); + public ValueString newYamlString(Node node) { + return ctx.newYamlString(node); + } + + @Override + public ValueString newSourceString(Node node) { + return ctx.newSourceString(node); } } diff --git a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java index f8327c255..22aec6746 100644 --- a/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java +++ b/policy/src/test/java/dev/cel/policy/CelPolicyYamlParserTest.java @@ -99,6 +99,20 @@ public void parseYamlPolicy_withDescription() throws Exception { .hasValue(ValueString.of(10, "this is a description of the variable")); } + @Test + public void parseYamlPolicy_withDescription_foldedStyle() throws Exception { + String policySource = + "name: 'policy_name'\n" + + "description: >-\n" + + " this is a multiline string\n" + + " that gets folded into a single line"; + + CelPolicy policy = POLICY_PARSER.parse(policySource); + + assertThat(policy.description().map(ValueString::value)) + .hasValue("this is a multiline string that gets folded into a single line"); + } + @Test public void parseYamlPolicy_withDisplayName() throws Exception { String policySource = @@ -144,7 +158,7 @@ public void parseYamlPolicy_withImports() throws Exception { assertThat(policy.imports()) .containsExactly( Import.create(8L, ValueString.of(9L, "foo")), - Import.create(12L, ValueString.of(13L, " bar"))) + Import.create(12L, ValueString.of(13L, "bar"))) .inOrder(); } diff --git a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java index 8d9e0084b..18d5ffc69 100644 --- a/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java +++ b/policy/src/test/java/dev/cel/policy/PolicyTestHelper.java @@ -268,7 +268,7 @@ public void visitPolicyTag( CelPolicy.Builder policyBuilder) { switch (tagName) { case "kind": - policyBuilder.putMetadata("kind", ctx.newValueString(node)); + policyBuilder.putMetadata("kind", ctx.newYamlString(node)); break; case "metadata": long metadataId = ctx.collectMetadata(node); @@ -299,7 +299,7 @@ public void visitRuleTag( Rule.Builder ruleBuilder) { switch (tagName) { case "failurePolicy": - policyBuilder.putMetadata(tagName, ctx.newValueString(node)); + policyBuilder.putMetadata(tagName, ctx.newYamlString(node)); break; case "matchConstraints": long matchConstraintsId = ctx.collectMetadata(node); @@ -343,13 +343,13 @@ public void visitMatchTag( case "expression": // The K8s expression to validate must return false in order to generate a violation // message. - ValueString conditionValue = ctx.newValueString(node); + ValueString conditionValue = ctx.newYamlString(node); conditionValue = conditionValue.toBuilder().setValue("!(" + conditionValue.value() + ")").build(); matchBuilder.setCondition(conditionValue); break; case "messageExpression": - matchBuilder.setResult(Result.ofOutput(ctx.newValueString(node))); + matchBuilder.setResult(Result.ofOutput(ctx.newYamlString(node))); break; default: TagVisitor.super.visitMatchTag(ctx, id, tagName, node, policyBuilder, matchBuilder); diff --git a/testing/src/test/resources/environment/extended_env.yaml b/testing/src/test/resources/environment/extended_env.yaml index 4763c868f..9fc2d511d 100644 --- a/testing/src/test/resources/environment/extended_env.yaml +++ b/testing/src/test/resources/environment/extended_env.yaml @@ -15,32 +15,43 @@ name: "extended-env" container: "cel.expr" extensions: - - name: "optional" - version: "2" - - name: "math" - version: "latest" +- name: "optional" + version: "2" +- name: "math" + version: "latest" variables: - - name: "msg" - type_name: "cel.expr.conformance.proto3.TestAllTypes" +- name: "msg" + type_name: "cel.expr.conformance.proto3.TestAllTypes" + description: >- + msg represents all possible type permutation which + CEL understands from a proto perspective functions: - - name: "isEmpty" - overloads: - - id: "wrapper_string_isEmpty" - target: - type_name: "google.protobuf.StringValue" - return: - type_name: "bool" - - id: "list_isEmpty" - target: - type_name: "list" - params: - - type_name: "T" - is_type_param: true - return: - type_name: "bool" +- name: "isEmpty" + description: |- + determines whether a list is empty, + or a string has no characters + overloads: + - id: "wrapper_string_isEmpty" + examples: + - "''.isEmpty() // true" + target: + type_name: "google.protobuf.StringValue" + return: + type_name: "bool" + - id: "list_isEmpty" + examples: + - "[].isEmpty() // true" + - "[1].isEmpty() // false" + target: + type_name: "list" + params: + - type_name: "T" + is_type_param: true + return: + type_name: "bool" features: - - name: cel.feature.macro_call_tracking - enabled: true +- name: cel.feature.macro_call_tracking + enabled: true limits: - name: cel.limit.expression_code_points value: 1000