-
Notifications
You must be signed in to change notification settings - Fork 37
Expand file tree
/
Copy pathCelContainer.java
More file actions
349 lines (304 loc) · 12.7 KB
/
CelContainer.java
File metadata and controls
349 lines (304 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// Copyright 2025 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.
package dev.cel.common;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.errorprone.annotations.Immutable;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
/** CelContainer holds a reference to an optional qualified container name and set of aliases. */
@AutoValue
@Immutable
public abstract class CelContainer {
public abstract String name();
abstract ImmutableMap<String, AliasEntry> aliasMap();
/**
* Returns the aliases configured in the container.
*
* <p>The key of the map is the alias and the value is the corresponding fully qualified name.
*/
public ImmutableMap<String, String> aliases() {
return aliasMap().entrySet().stream()
.filter(e -> e.getValue().kind().equals(AliasKind.ALIAS))
.collect(toImmutableMap(Map.Entry::getKey, e -> e.getValue().qualifiedName()));
}
public ImmutableList<String> abbreviations() {
return aliasMap().entrySet().stream()
.filter(e -> e.getValue().kind().equals(AliasKind.ABBREVIATION))
.map(e -> e.getValue().qualifiedName())
.collect(toImmutableList());
}
/** Builder for {@link CelContainer} */
@AutoValue.Builder
public abstract static class Builder {
private final LinkedHashMap<String, AliasEntry> aliasMap = new LinkedHashMap<>();
abstract String name();
/** Sets the fully-qualified name of the container. */
public abstract Builder setName(String name);
abstract Builder setAliasMap(ImmutableMap<String, AliasEntry> aliasMap);
/** See {@link #addAbbreviations(ImmutableSet)} for documentation. */
@CanIgnoreReturnValue
public Builder addAbbreviations(String... qualifiedNames) {
Preconditions.checkNotNull(qualifiedNames);
return addAbbreviations(ImmutableSet.copyOf(qualifiedNames));
}
/**
* Configures a set of simple names as abbreviations for fully-qualified names.
*
* <p>An abbreviation is a simple name that expands to a fully-qualified name. Abbreviations can
* be useful when working with variables, functions, and especially types from multiple
* namespaces:
*
* <pre>{@code
* // CEL object construction
* qual.pkg.version.ObjTypeName{
* field: alt.container.ver.FieldTypeName{value: ...}
* }
* }</pre>
*
* <p>Only one the qualified names above may be used as the CEL container, so at least one of
* these references must be a long qualified name within an otherwise short CEL program. Using
* the following abbreviations, the program becomes much simpler:
*
* <pre>{@code
* // CEL Java option
* CelContainer.newBuilder().addAbbreviations("qual.pkg.version.ObjTypeName", "alt.container.ver.FieldTypeName").build()
* }
* {@code
* // Simplified Object construction
* ObjTypeName{field: FieldTypeName{value: ...}}
* }</pre>
*
* <p>There are a few rules for the qualified names and the simple abbreviations generated from
* them:
*
* <ul>
* <li>Qualified names must be dot-delimited, e.g. `package.subpkg.name`.
* <li>The last element in the qualified name is the abbreviation.
* <li>Abbreviations must not collide with each other.
* <li>The abbreviation must not collide with unqualified names in use.
* </ul>
*
* <p>Abbreviations are distinct from container-based references in the following important
* ways:
*
* <ul>
* <li>Abbreviations must expand to a fully-qualified name.
* <li>Expanded abbreviations do not participate in namespace resolution.
* <li>Abbreviation expansion is done instead of the container search for a matching
* identifier.
* <li>Containers follow C++ namespace resolution rules with searches from the most qualified
* name to the least qualified name.
* <li>Container references within the CEL program may be relative, and are resolved to fully
* qualified names at either type-check time or program plan time, whichever comes first.
* </ul>
*
* <p>If there is ever a case where an identifier could be in both the container and as an
* abbreviation, the abbreviation wins as this will ensure that the meaning of a program is
* preserved between compilations even as the container evolves.
*
* @throws IllegalArgumentException If qualifiedName is invalid per above specification.
*/
@CanIgnoreReturnValue
public Builder addAbbreviations(ImmutableSet<String> qualifiedNames) {
for (String qualifiedName : qualifiedNames) {
qualifiedName = qualifiedName.trim();
for (int i = 0; i < qualifiedName.length(); i++) {
if (!isIdentifierChar(qualifiedName.charAt(i))) {
throw new IllegalArgumentException(
String.format(
"invalid qualified name: %s, wanted name of the form 'qualified.name'",
qualifiedName));
}
}
int index = qualifiedName.lastIndexOf(".");
if (index <= 0 || index >= qualifiedName.length() - 1) {
throw new IllegalArgumentException(
String.format(
"invalid qualified name: %s, wanted name of the form 'qualified.name'",
qualifiedName));
}
String alias = qualifiedName.substring(index + 1);
aliasAs(AliasKind.ABBREVIATION, qualifiedName, alias);
}
return this;
}
/**
* Alias associates a fully-qualified name with a user-defined alias.
*
* <p>In general, {@link #addAbbreviations} is preferred to aliasing since the names generated
* from the Abbrevs option are more easily traced back to source code. Aliasing is useful for
* propagating alias configuration from one container instance to another, and may also be
* useful for remapping poorly chosen protobuf message / package names.
*
* <p>Note: all the rules that apply to abbreviations also apply to aliasing.
*
* <p>Note: It is also possible to alias a top-level package or a name that does not contain a
* period. When resolving an identifier, CEL checks for variables and functions before
* attempting to expand aliases for type resolution. Therefore, if an expression consists solely
* of an identifier that matches both an alias and a declared variable (e.g., {@code
* short_alias}), the variable will take precedence and the compilation will succeed. The alias
* expansion will only be used when the alias is a prefix to a longer name (e.g., {@code
* short_alias.TestRequest}) or if no variable with the same name exists, in which case using
* the alias as a standalone identifier will likely result in a compilation error.
*
* @param alias Simple name to be expanded. Must be a valid identifier.
* @param qualifiedName The fully qualified name to expand to. This may be a simple name (e.g. a
* package name) but it must be a valid identifier.
*/
@CanIgnoreReturnValue
public Builder addAlias(String alias, String qualifiedName) {
aliasAs(AliasKind.ALIAS, qualifiedName, alias);
return this;
}
private void aliasAs(AliasKind kind, String qualifiedName, String alias) {
validateAliasOrThrow(kind, qualifiedName, alias);
aliasMap.put(alias, AliasEntry.create(kind, qualifiedName));
}
private void validateAliasOrThrow(AliasKind kind, String qualifiedName, String alias) {
if (alias.isEmpty() || alias.contains(".")) {
throw new IllegalArgumentException(
String.format(
"%s must be non-empty and simple (not qualified): %s=%s", kind, kind, alias));
}
if (qualifiedName.charAt(0) == '.') {
throw new IllegalArgumentException(
String.format("qualified name must not begin with a leading '.': %s", qualifiedName));
}
AliasEntry aliasRef = aliasMap.get(alias);
if (aliasRef != null) {
throw new IllegalArgumentException(
String.format(
"%s collides with existing reference: name=%s, %s=%s, existing=%s",
kind, qualifiedName, kind, alias, aliasRef.qualifiedName()));
}
String containerName = name();
if (containerName.startsWith(alias + ".") || containerName.equals(alias)) {
throw new IllegalArgumentException(
String.format(
"%s collides with container name: name=%s, %s=%s, container=%s",
kind, qualifiedName, kind, alias, containerName));
}
}
abstract CelContainer autoBuild();
@CheckReturnValue
public CelContainer build() {
setAliasMap(ImmutableMap.copyOf(aliasMap));
return autoBuild();
}
}
/**
* Returns the candidates name of namespaced identifiers in C++ resolution order.
*
* <p>Names which shadow other names are returned first. If a name includes a leading dot ('.'),
* the name is treated as an absolute identifier which cannot be shadowed.
*
* <p>Given a container name a.b.c.M.N and a type name R.s, this will deliver in order:
*
* <ul>
* <li>a.b.c.M.N.R.s
* <li>a.b.c.M.R.s
* <li>a.b.c.R.s
* <li>a.b.R.s
* <li>a.R.s
* <li>R.s
* </ul>
*
* <p>If aliases or abbreviations are configured for the container, then alias names will take
* precedence over containerized names.
*/
public ImmutableSet<String> resolveCandidateNames(String typeName) {
if (typeName.startsWith(".")) {
String qualifiedName = typeName.substring(1);
String alias = findAlias(qualifiedName).orElse(qualifiedName);
return ImmutableSet.of(alias);
}
String alias = findAlias(typeName).orElse(null);
if (alias != null) {
return ImmutableSet.of(alias);
}
if (name().isEmpty()) {
return ImmutableSet.of(typeName);
}
String nextContainer = name();
ImmutableSet.Builder<String> candidates =
ImmutableSet.<String>builder().add(nextContainer + "." + typeName);
for (int i = nextContainer.lastIndexOf("."); i >= 0; i = nextContainer.lastIndexOf(".")) {
nextContainer = nextContainer.substring(0, i);
candidates.add(nextContainer + "." + typeName);
}
return candidates.add(typeName).build();
}
abstract Builder autoToBuilder();
public Builder toBuilder() {
Builder builder = autoToBuilder();
builder.aliasMap.putAll(aliasMap());
return builder;
}
public static Builder newBuilder() {
return new AutoValue_CelContainer.Builder().setName("");
}
public static CelContainer ofName(String containerName) {
return newBuilder().setName(containerName).build();
}
private Optional<String> findAlias(String name) {
// If an alias exists for the name, ensure it is searched last.
String simple = name;
String qualifier = "";
int dot = name.indexOf(".");
if (dot > 0) {
simple = name.substring(0, dot);
qualifier = name.substring(dot);
}
AliasEntry alias = aliasMap().get(simple);
if (alias == null) {
return Optional.empty();
}
return Optional.of(alias.qualifiedName() + qualifier);
}
private static boolean isIdentifierChar(int r) {
if (r > 127) {
// Not ASCII
return false;
}
return r == '.' || r == '_' || Character.isLetter(r) || Character.isDigit(r);
}
enum AliasKind {
ALIAS,
ABBREVIATION;
@Override
public String toString() {
return this.name().toLowerCase(Locale.getDefault());
}
}
/** Represents an alias or abbreviation. */
@AutoValue
@Immutable
abstract static class AliasEntry {
static AliasEntry create(AliasKind kind, String qualifiedName) {
return new AutoValue_CelContainer_AliasEntry(kind, qualifiedName);
}
abstract AliasKind kind();
abstract String qualifiedName();
}
}