Skip to content

Commit ff131ee

Browse files
l46kokcopybara-github
authored andcommitted
Add shorthand type specifier syntax for policy configs
PiperOrigin-RevId: 963769585
1 parent 496434e commit ff131ee

9 files changed

Lines changed: 910 additions & 20 deletions

File tree

bundle/src/main/java/dev/cel/bundle/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ java_library(
9797
name = "environment",
9898
srcs = [
9999
"CelEnvironment.java",
100+
"TypeSpecifierParser.java",
100101
],
101102
tags = [
102103
],
@@ -111,6 +112,7 @@ java_library(
111112
"//common:container",
112113
"//common:options",
113114
"//common:source",
115+
"//common/formats:parser_context",
114116
"//common/types",
115117
"//common/types:type_providers",
116118
"//compiler:compiler_builder",

bundle/src/main/java/dev/cel/bundle/CelEnvironment.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,19 @@ public static TypeDecl create(String name) {
692692
return newBuilder().setName(name).build();
693693
}
694694

695+
/**
696+
* Parses a type specifier shorthand string (e.g. {@code "list<int>"}, {@code "map<string,
697+
* dyn>"}, {@code "list<~T>"}) into a {@link TypeDecl}.
698+
*/
699+
public static TypeDecl parse(String typeSpecifier) {
700+
return TypeSpecifierParser.parse(typeSpecifier);
701+
}
702+
703+
/** Creates a new {@link TypeDecl} representing a type parameter with the provided name. */
704+
public static TypeDecl ofTypeParam(String typeParamName) {
705+
return newBuilder().setName(typeParamName).setIsTypeParam(true).build();
706+
}
707+
695708
public static TypeDecl.Builder newBuilder() {
696709
return new AutoValue_CelEnvironment_TypeDecl.Builder().setIsTypeParam(false);
697710
}

bundle/src/main/java/dev/cel/bundle/CelEnvironmentYamlParser.java

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
import static dev.cel.common.formats.YamlHelper.newString;
2323
import static dev.cel.common.formats.YamlHelper.parseYamlSource;
2424
import static dev.cel.common.formats.YamlHelper.validateYamlType;
25-
import static java.util.Collections.singletonList;
2625

2726
import com.google.common.collect.ImmutableList;
2827
import com.google.common.collect.ImmutableSet;
2928
import com.google.errorprone.annotations.CanIgnoreReturnValue;
29+
import com.google.errorprone.annotations.CheckReturnValue;
3030
import dev.cel.bundle.CelEnvironment.Alias;
3131
import dev.cel.bundle.CelEnvironment.ContextVariable;
3232
import dev.cel.bundle.CelEnvironment.ExtensionConfig;
@@ -60,7 +60,7 @@
6060
*/
6161
public final class CelEnvironmentYamlParser {
6262
// Sentinel values to be returned for various declarations when parsing failure is encountered.
63-
private static final TypeDecl ERROR_TYPE_DECL = TypeDecl.create(ERROR);
63+
static final TypeDecl ERROR_TYPE_DECL = TypeSpecifierParser.ERROR_TYPE_DECL;
6464
private static final VariableDecl ERROR_VARIABLE_DECL =
6565
VariableDecl.create(ERROR, ERROR_TYPE_DECL);
6666
private static final FunctionDecl ERROR_FUNCTION_DECL =
@@ -71,9 +71,44 @@ public final class CelEnvironmentYamlParser {
7171
private static final Alias ERROR_ALIAS =
7272
Alias.newBuilder().setAlias(ERROR).setQualifiedName(ERROR).build();
7373

74-
/** Generates a new instance of {@code CelEnvironmentYamlParser}. */
74+
private final boolean enableTypeSpecifiers;
75+
76+
/** Generates a new instance of {@code CelEnvironmentYamlParser} with default options. */
7577
public static CelEnvironmentYamlParser newInstance() {
76-
return new CelEnvironmentYamlParser();
78+
return newBuilder().build();
79+
}
80+
81+
/** Creates a new builder to configure and construct a {@link CelEnvironmentYamlParser}. */
82+
public static Builder newBuilder() {
83+
return new Builder();
84+
}
85+
86+
/** Builder for {@link CelEnvironmentYamlParser}. */
87+
public static final class Builder {
88+
private boolean enableTypeSpecifiers = false;
89+
90+
/**
91+
* Configures the parser to allow for shorthand type specifiers (e.g. {@code "list<int>"},
92+
* {@code "map<string, dyn>"}, {@code "list<~T>"}) in addition to structured mapping
93+
* declarations.
94+
*/
95+
@CanIgnoreReturnValue
96+
public Builder enableTypeSpecifiers(boolean enable) {
97+
this.enableTypeSpecifiers = enable;
98+
return this;
99+
}
100+
101+
/** Builds a new instance of {@link CelEnvironmentYamlParser}. */
102+
@CheckReturnValue
103+
public CelEnvironmentYamlParser build() {
104+
return new CelEnvironmentYamlParser(enableTypeSpecifiers);
105+
}
106+
107+
private Builder() {}
108+
}
109+
110+
private CelEnvironmentYamlParser(boolean enableTypeSpecifiers) {
111+
this.enableTypeSpecifiers = enableTypeSpecifiers;
77112
}
78113

79114
/** Parsers the input {@code environmentYamlSource} and returns a {@link CelEnvironment}. */
@@ -335,6 +370,7 @@ private ContextVariable parseContextVariable(ParserContext<Node> ctx, Node node)
335370
Node valueNode = nodeTuple.getValueNode();
336371
String keyName = ((ScalarNode) keyNode).getValue();
337372
switch (keyName) {
373+
case "type":
338374
case "type_name":
339375
typeName = newString(ctx, valueNode);
340376
break;
@@ -478,7 +514,7 @@ private FunctionDecl parseFunction(ParserContext<Node> ctx, Node node) {
478514
return builder.build();
479515
}
480516

481-
private static ImmutableSet<OverloadDecl> parseOverloads(ParserContext<Node> ctx, Node node) {
517+
private ImmutableSet<OverloadDecl> parseOverloads(ParserContext<Node> ctx, Node node) {
482518
long listId = ctx.collectMetadata(node);
483519
ImmutableSet.Builder<OverloadDecl> overloadSetBuilder = ImmutableSet.builder();
484520
if (!assertYamlType(ctx, listId, node, YamlNodeType.LIST)) {
@@ -553,8 +589,7 @@ private static ImmutableList<String> parseOverloadExamples(ParserContext<Node> c
553589
return builder.build();
554590
}
555591

556-
private static ImmutableList<TypeDecl> parseOverloadArguments(
557-
ParserContext<Node> ctx, Node node) {
592+
private ImmutableList<TypeDecl> parseOverloadArguments(ParserContext<Node> ctx, Node node) {
558593
long listValueId = ctx.collectMetadata(node);
559594
if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) {
560595
return ImmutableList.of();
@@ -791,7 +826,7 @@ private static ImmutableSet<OverloadSelector> parseFunctionOverloadsSelector(
791826
}
792827

793828
@CanIgnoreReturnValue
794-
private static TypeDecl.Builder parseInlinedTypeDecl(
829+
private TypeDecl.Builder parseInlinedTypeDecl(
795830
ParserContext<Node> ctx, long keyId, Node keyNode, Node valueNode, TypeDecl.Builder builder) {
796831
if (!assertYamlType(ctx, keyId, keyNode, YamlNodeType.STRING, YamlNodeType.TEXT)) {
797832
return builder;
@@ -800,24 +835,38 @@ private static TypeDecl.Builder parseInlinedTypeDecl(
800835
// Create a synthetic node to make this behave as if a `type: ` parent node actually exists.
801836
MappingNode mapNode =
802837
new MappingNode(
803-
Tag.MAP, /* value= */ singletonList(new NodeTuple(keyNode, valueNode)), FlowStyle.AUTO);
838+
Tag.MAP,
839+
/* value= */ ImmutableList.of(new NodeTuple(keyNode, valueNode)),
840+
FlowStyle.AUTO);
804841

805842
return parseTypeDeclFields(ctx, mapNode, builder);
806843
}
807844

808-
private static TypeDecl parseTypeDecl(ParserContext<Node> ctx, Node node) {
809-
TypeDecl.Builder builder = TypeDecl.newBuilder();
845+
private TypeDecl parseTypeDecl(ParserContext<Node> ctx, Node node) {
810846
long id = ctx.collectMetadata(node);
847+
if (enableTypeSpecifiers) {
848+
if (validateYamlType(node, YamlNodeType.STRING, YamlNodeType.TEXT)) {
849+
return TypeSpecifierParser.parse(ctx, id, newString(ctx, node));
850+
}
851+
if (validateYamlType(node, YamlNodeType.MAP)) {
852+
TypeDecl.Builder builder = TypeDecl.newBuilder();
853+
return parseTypeDeclFields(ctx, (MappingNode) node, builder).build();
854+
}
855+
assertYamlType(ctx, id, node, YamlNodeType.STRING, YamlNodeType.TEXT, YamlNodeType.MAP);
856+
return ERROR_TYPE_DECL;
857+
}
858+
811859
if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) {
812860
return ERROR_TYPE_DECL;
813861
}
814862

863+
TypeDecl.Builder builder = TypeDecl.newBuilder();
815864
MappingNode mapNode = (MappingNode) node;
816865
return parseTypeDeclFields(ctx, mapNode, builder).build();
817866
}
818867

819868
@CanIgnoreReturnValue
820-
private static TypeDecl.Builder parseTypeDeclFields(
869+
private TypeDecl.Builder parseTypeDeclFields(
821870
ParserContext<Node> ctx, MappingNode mapNode, TypeDecl.Builder builder) {
822871
for (NodeTuple nodeTuple : mapNode.getValue()) {
823872
Node keyNode = nodeTuple.getKeyNode();
@@ -943,6 +992,4 @@ private CelEnvironment.Builder parseConfig(ParserContext<Node> ctx, Node node) {
943992
return builder;
944993
}
945994
}
946-
947-
private CelEnvironmentYamlParser() {}
948995
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dev.cel.bundle;
16+
17+
import static com.google.common.base.Preconditions.checkNotNull;
18+
19+
import com.google.common.collect.ImmutableList;
20+
import dev.cel.bundle.CelEnvironment.TypeDecl;
21+
import dev.cel.common.formats.ParserContext;
22+
23+
/**
24+
* Parses a type specifier shorthand string (e.g. {@code "map<string, int>"}, {@code "list<~T>"},
25+
* {@code "int"}) into a {@link TypeDecl}.
26+
*/
27+
final class TypeSpecifierParser {
28+
private static final int MAX_RECURSION_DEPTH = 64;
29+
static final TypeDecl ERROR_TYPE_DECL = TypeDecl.create("*error*");
30+
31+
private final String text;
32+
private final int length;
33+
private int pos;
34+
35+
static TypeDecl parse(String text) {
36+
checkNotNull(text);
37+
TypeSpecifierParser parser = new TypeSpecifierParser(text);
38+
return parser.parse();
39+
}
40+
41+
static TypeDecl parse(ParserContext<?> ctx, long nodeId, String text) {
42+
checkNotNull(ctx);
43+
checkNotNull(text);
44+
try {
45+
return parse(text);
46+
} catch (IllegalArgumentException e) {
47+
ctx.reportError(nodeId, e.getMessage());
48+
return ERROR_TYPE_DECL;
49+
}
50+
}
51+
52+
private TypeDecl parse() {
53+
TypeDecl res = parseTypeElem(0);
54+
skipWhitespace();
55+
if (pos < length) {
56+
throw new IllegalArgumentException(
57+
String.format(
58+
"unexpected character '%c' at position %d in %s",
59+
text.charAt(pos), pos, formatQuoted(text)));
60+
}
61+
return res;
62+
}
63+
64+
private TypeSpecifierParser(String text) {
65+
this.text = text;
66+
this.length = text.length();
67+
this.pos = 0;
68+
}
69+
70+
private TypeDecl parseTypeElem(int depth) {
71+
if (depth > MAX_RECURSION_DEPTH) {
72+
throw new IllegalArgumentException(
73+
String.format("exceeded maximum type specifier recursion depth at position %d", pos));
74+
}
75+
skipWhitespace();
76+
if (pos < length && text.charAt(pos) == '~') {
77+
pos++; // consume '~'
78+
String id = parseTypeParamIdent();
79+
return TypeDecl.ofTypeParam(id);
80+
}
81+
return parseConcreteType(depth);
82+
}
83+
84+
private TypeDecl parseConcreteType(int depth) {
85+
String id = parseNamespaceIdentifier();
86+
skipWhitespace();
87+
if (pos < length && text.charAt(pos) == '<') {
88+
pos++; // consume '<'
89+
ImmutableList.Builder<TypeDecl> params = ImmutableList.builder();
90+
while (true) {
91+
TypeDecl param = parseTypeElem(depth + 1);
92+
params.add(param);
93+
skipWhitespace();
94+
if (pos < length && text.charAt(pos) == ',') {
95+
pos++; // consume ','
96+
continue;
97+
}
98+
if (pos < length && text.charAt(pos) == '>') {
99+
pos++; // consume '>'
100+
break;
101+
}
102+
throw new IllegalArgumentException(
103+
String.format("expected ',' or '>' at position %d", pos));
104+
}
105+
return TypeDecl.newBuilder().setName(id).addParams(params.build()).build();
106+
}
107+
return TypeDecl.create(id);
108+
}
109+
110+
private String parseNamespaceIdentifier() {
111+
StringBuilder id = new StringBuilder();
112+
while (pos < length && text.charAt(pos) != '<') {
113+
char c = text.charAt(pos);
114+
if (c == '.') {
115+
id.append('.');
116+
pos++; // consume '.'
117+
}
118+
String ident = parseIdentifier();
119+
id.append(ident);
120+
if (pos < length && text.charAt(pos) != '.') {
121+
break;
122+
}
123+
}
124+
String identifier = id.toString();
125+
if (identifier.isEmpty()) {
126+
throw new IllegalArgumentException(String.format("missing identifier at position %d", pos));
127+
}
128+
return identifier;
129+
}
130+
131+
private String parseIdentifier() {
132+
if (pos >= length) {
133+
throw new IllegalArgumentException("unexpected end of input");
134+
}
135+
int start = pos;
136+
while (pos < length) {
137+
char c = text.charAt(pos);
138+
boolean isValid = (pos == start) ? (isAlpha(c) || c == '_') : (isAlphaNumeric(c) || c == '_');
139+
if (isValid) {
140+
pos++;
141+
continue;
142+
}
143+
if (pos == start) {
144+
throw new IllegalArgumentException(
145+
String.format("identifier is expected, but '%c' was found at position %d", c, pos));
146+
}
147+
break;
148+
}
149+
return text.substring(start, pos);
150+
}
151+
152+
private String parseTypeParamIdent() {
153+
if (pos >= length) {
154+
throw new IllegalArgumentException("unexpected end of input");
155+
}
156+
char c = text.charAt(pos);
157+
if (c < 'A' || c > 'Z') {
158+
throw new IllegalArgumentException(
159+
String.format(
160+
"invalid type parameter identifier '%c' at position %d, must be a single character"
161+
+ " from A-Z",
162+
c, pos));
163+
}
164+
pos++;
165+
if (pos < length) {
166+
char next = text.charAt(pos);
167+
if (isAlphaNumeric(next) || next == '_') {
168+
throw new IllegalArgumentException(
169+
String.format(
170+
"invalid type parameter identifier '%c' at position %d, must be a single character"
171+
+ " from A-Z",
172+
next, pos));
173+
}
174+
}
175+
return String.valueOf(c);
176+
}
177+
178+
private void skipWhitespace() {
179+
while (pos < length) {
180+
char c = text.charAt(pos);
181+
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
182+
pos++;
183+
} else {
184+
break;
185+
}
186+
}
187+
}
188+
189+
private static boolean isAlpha(char c) {
190+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
191+
}
192+
193+
private static boolean isAlphaNumeric(char c) {
194+
return isAlpha(c) || (c >= '0' && c <= '9');
195+
}
196+
197+
private static String formatQuoted(String s) {
198+
return "\"" + s + "\"";
199+
}
200+
}

0 commit comments

Comments
 (0)