Skip to content

Commit c8478bd

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

9 files changed

Lines changed: 844 additions & 22 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+
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+
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: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
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;
@@ -60,7 +59,7 @@
6059
*/
6160
public final class CelEnvironmentYamlParser {
6261
// Sentinel values to be returned for various declarations when parsing failure is encountered.
63-
private static final TypeDecl ERROR_TYPE_DECL = TypeDecl.create(ERROR);
62+
private static final TypeDecl ERROR_TYPE_DECL = TypeSpecifierParser.ERROR_TYPE_DECL;
6463
private static final VariableDecl ERROR_VARIABLE_DECL =
6564
VariableDecl.create(ERROR, ERROR_TYPE_DECL);
6665
private static final FunctionDecl ERROR_FUNCTION_DECL =
@@ -335,6 +334,7 @@ private ContextVariable parseContextVariable(ParserContext<Node> ctx, Node node)
335334
Node valueNode = nodeTuple.getValueNode();
336335
String keyName = ((ScalarNode) keyNode).getValue();
337336
switch (keyName) {
337+
case "type":
338338
case "type_name":
339339
typeName = newString(ctx, valueNode);
340340
break;
@@ -478,7 +478,7 @@ private FunctionDecl parseFunction(ParserContext<Node> ctx, Node node) {
478478
return builder.build();
479479
}
480480

481-
private static ImmutableSet<OverloadDecl> parseOverloads(ParserContext<Node> ctx, Node node) {
481+
private ImmutableSet<OverloadDecl> parseOverloads(ParserContext<Node> ctx, Node node) {
482482
long listId = ctx.collectMetadata(node);
483483
ImmutableSet.Builder<OverloadDecl> overloadSetBuilder = ImmutableSet.builder();
484484
if (!assertYamlType(ctx, listId, node, YamlNodeType.LIST)) {
@@ -553,8 +553,7 @@ private static ImmutableList<String> parseOverloadExamples(ParserContext<Node> c
553553
return builder.build();
554554
}
555555

556-
private static ImmutableList<TypeDecl> parseOverloadArguments(
557-
ParserContext<Node> ctx, Node node) {
556+
private ImmutableList<TypeDecl> parseOverloadArguments(ParserContext<Node> ctx, Node node) {
558557
long listValueId = ctx.collectMetadata(node);
559558
if (!assertYamlType(ctx, listValueId, node, YamlNodeType.LIST)) {
560559
return ImmutableList.of();
@@ -791,7 +790,7 @@ private static ImmutableSet<OverloadSelector> parseFunctionOverloadsSelector(
791790
}
792791

793792
@CanIgnoreReturnValue
794-
private static TypeDecl.Builder parseInlinedTypeDecl(
793+
private TypeDecl.Builder parseInlinedTypeDecl(
795794
ParserContext<Node> ctx, long keyId, Node keyNode, Node valueNode, TypeDecl.Builder builder) {
796795
if (!assertYamlType(ctx, keyId, keyNode, YamlNodeType.STRING, YamlNodeType.TEXT)) {
797796
return builder;
@@ -800,24 +799,28 @@ private static TypeDecl.Builder parseInlinedTypeDecl(
800799
// Create a synthetic node to make this behave as if a `type: ` parent node actually exists.
801800
MappingNode mapNode =
802801
new MappingNode(
803-
Tag.MAP, /* value= */ singletonList(new NodeTuple(keyNode, valueNode)), FlowStyle.AUTO);
802+
Tag.MAP,
803+
/* value= */ ImmutableList.of(new NodeTuple(keyNode, valueNode)),
804+
FlowStyle.AUTO);
804805

805806
return parseTypeDeclFields(ctx, mapNode, builder);
806807
}
807808

808-
private static TypeDecl parseTypeDecl(ParserContext<Node> ctx, Node node) {
809-
TypeDecl.Builder builder = TypeDecl.newBuilder();
809+
private TypeDecl parseTypeDecl(ParserContext<Node> ctx, Node node) {
810810
long id = ctx.collectMetadata(node);
811-
if (!assertYamlType(ctx, id, node, YamlNodeType.MAP)) {
812-
return ERROR_TYPE_DECL;
811+
if (validateYamlType(node, YamlNodeType.STRING, YamlNodeType.TEXT)) {
812+
return TypeSpecifierParser.parse(ctx, id, newString(ctx, node));
813813
}
814-
815-
MappingNode mapNode = (MappingNode) node;
816-
return parseTypeDeclFields(ctx, mapNode, builder).build();
814+
if (validateYamlType(node, YamlNodeType.MAP)) {
815+
TypeDecl.Builder builder = TypeDecl.newBuilder();
816+
return parseTypeDeclFields(ctx, (MappingNode) node, builder).build();
817+
}
818+
assertYamlType(ctx, id, node, YamlNodeType.STRING, YamlNodeType.TEXT, YamlNodeType.MAP);
819+
return ERROR_TYPE_DECL;
817820
}
818821

819822
@CanIgnoreReturnValue
820-
private static TypeDecl.Builder parseTypeDeclFields(
823+
private TypeDecl.Builder parseTypeDeclFields(
821824
ParserContext<Node> ctx, MappingNode mapNode, TypeDecl.Builder builder) {
822825
for (NodeTuple nodeTuple : mapNode.getValue()) {
823826
Node keyNode = nodeTuple.getKeyNode();
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright 2026 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)