Skip to content

Commit 54f659a

Browse files
l46kokcopybara-github
authored andcommitted
Evaluate CEL's timestamp and duration types to their native equivalent values
PiperOrigin-RevId: 796654800
1 parent fe46a63 commit 54f659a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1166
-840
lines changed

common/internal/BUILD.bazel

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,13 @@ cel_android_library(
137137
name = "proto_time_utils_android",
138138
exports = ["//common/src/main/java/dev/cel/common/internal:proto_time_utils_android"],
139139
)
140+
141+
java_library(
142+
name = "date_time_helpers",
143+
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers"],
144+
)
145+
146+
cel_android_library(
147+
name = "date_time_helpers_android",
148+
exports = ["//common/src/main/java/dev/cel/common/internal:date_time_helpers_android"],
149+
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ java_library(
205205
tags = [
206206
],
207207
deps = [
208+
"//common/internal:date_time_helpers",
208209
"//common/internal:proto_time_utils",
209210
"//common/values",
210211
"//common/values:cel_byte_string",

common/src/main/java/dev/cel/common/CelProtoJsonAdapter.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
import com.google.protobuf.Struct;
2727
import com.google.protobuf.Timestamp;
2828
import com.google.protobuf.Value;
29+
import dev.cel.common.internal.DateTimeHelpers;
2930
import dev.cel.common.internal.ProtoTimeUtils;
3031
import dev.cel.common.values.CelByteString;
32+
import java.time.Instant;
3133
import java.util.ArrayList;
3234
import java.util.Base64;
3335
import java.util.List;
@@ -118,6 +120,14 @@ public static Value adaptValueToJsonValue(Object value) {
118120
String duration = ProtoTimeUtils.toString((Duration) value);
119121
return json.setStringValue(duration).build();
120122
}
123+
if (value instanceof Instant) {
124+
// Instant's toString follows RFC 3339
125+
return json.setStringValue(value.toString()).build();
126+
}
127+
if (value instanceof java.time.Duration) {
128+
String duration = DateTimeHelpers.toString((java.time.Duration) value);
129+
return json.setStringValue(duration).build();
130+
}
121131
if (value instanceof FieldMask) {
122132
String fieldMaskStr = toJsonString((FieldMask) value);
123133
return json.setStringValue(fieldMaskStr).build();

common/src/main/java/dev/cel/common/internal/BUILD.bazel

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,14 @@ java_library(
178178
"//common:options",
179179
"//common:runtime_exception",
180180
"//common/annotations",
181+
"//common/internal:proto_time_utils",
181182
"//common/values",
182183
"//common/values:cel_byte_string",
183184
"@maven//:com_google_code_findbugs_annotations",
184185
"@maven//:com_google_errorprone_error_prone_annotations",
185186
"@maven//:com_google_guava_guava",
186187
"@maven//:com_google_protobuf_protobuf_java",
188+
"@maven_android//:com_google_protobuf_protobuf_javalite",
187189
],
188190
)
189191

@@ -195,14 +197,17 @@ java_library(
195197
deps = [
196198
":well_known_proto",
197199
"//common:error_codes",
200+
"//common:options",
198201
"//common:proto_json_adapter",
199202
"//common:runtime_exception",
200203
"//common/annotations",
204+
"//common/internal:proto_time_utils",
201205
"//common/values",
202206
"//common/values:cel_byte_string",
203207
"@maven//:com_google_errorprone_error_prone_annotations",
204208
"@maven//:com_google_guava_guava",
205209
"@maven//:com_google_protobuf_protobuf_java",
210+
"@maven_android//:com_google_protobuf_protobuf_javalite",
206211
],
207212
)
208213

@@ -429,3 +434,32 @@ cel_android_library(
429434
"@maven_android//:com_google_protobuf_protobuf_javalite",
430435
],
431436
)
437+
438+
java_library(
439+
name = "date_time_helpers",
440+
srcs = ["DateTimeHelpers.java"],
441+
tags = [
442+
],
443+
deps = [
444+
"//common:error_codes",
445+
"//common:runtime_exception",
446+
"//common/annotations",
447+
"@maven//:com_google_errorprone_error_prone_annotations",
448+
"@maven//:com_google_guava_guava",
449+
"@maven//:com_google_protobuf_protobuf_java",
450+
],
451+
)
452+
453+
cel_android_library(
454+
name = "date_time_helpers_android",
455+
srcs = ["DateTimeHelpers.java"],
456+
tags = [
457+
],
458+
deps = [
459+
"//common:error_codes",
460+
"//common:runtime_exception",
461+
"//common/annotations",
462+
"@maven_android//:com_google_guava_guava",
463+
"@maven_android//:com_google_protobuf_protobuf_javalite",
464+
],
465+
)
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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.common.internal;
16+
17+
import com.google.common.base.Strings;
18+
import com.google.protobuf.Timestamp;
19+
import dev.cel.common.CelErrorCode;
20+
import dev.cel.common.CelRuntimeException;
21+
import dev.cel.common.annotations.Internal;
22+
import java.time.DateTimeException;
23+
import java.time.Instant;
24+
import java.time.LocalDateTime;
25+
import java.time.ZoneId;
26+
import java.util.Locale;
27+
28+
/** Collection of utility methods for CEL datetime handlings. */
29+
@Internal
30+
@SuppressWarnings("JavaInstantGetSecondsGetNano") // Intended within CEL.
31+
public final class DateTimeHelpers {
32+
public static final String UTC = "UTC";
33+
34+
// Timestamp for "0001-01-01T00:00:00Z"
35+
private static final long TIMESTAMP_SECONDS_MIN = -62135596800L;
36+
// Timestamp for "9999-12-31T23:59:59Z"
37+
private static final long TIMESTAMP_SECONDS_MAX = 253402300799L;
38+
39+
private static final long DURATION_SECONDS_MIN = -315576000000L;
40+
private static final long DURATION_SECONDS_MAX = 315576000000L;
41+
private static final int NANOS_PER_SECOND = 1000000000;
42+
43+
/**
44+
* Constructs a new {@link LocalDateTime} instance
45+
*
46+
* @param ts Timestamp protobuf object
47+
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
48+
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
49+
* <ul>
50+
* <li>UTC
51+
* <li>America/Los_Angeles
52+
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
53+
* </ul>
54+
*
55+
* @return If an Invalid timezone is supplied.
56+
*/
57+
public static LocalDateTime newLocalDateTime(Timestamp ts, String tz) {
58+
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos())
59+
.atZone(timeZone(tz))
60+
.toLocalDateTime();
61+
}
62+
63+
/**
64+
* Constructs a new {@link LocalDateTime} instance from a Java Instant.
65+
*
66+
* @param instant Instant object
67+
* @param tz Timezone based on the CEL specification. This is either the canonical name from tz
68+
* database or a standard offset represented in (+/-)HH:MM. Few valid examples are:
69+
* <ul>
70+
* <li>UTC
71+
* <li>America/Los_Angeles
72+
* <li>-09:30 or -9:30 (Leading zeroes can be omitted though not allowed by spec)
73+
* </ul>
74+
*
75+
* @return A new {@link LocalDateTime} instance.
76+
*/
77+
public static LocalDateTime newLocalDateTime(Instant instant, String tz) {
78+
return instant.atZone(timeZone(tz)).toLocalDateTime();
79+
}
80+
81+
/**
82+
* Parse from RFC 3339 date string to {@link java.time.Instant}.
83+
*
84+
* <p>Example of accepted format: "1972-01-01T10:00:20.021-05:00"
85+
*/
86+
public static Instant parse(String text) {
87+
Instant instant = Instant.parse(text);
88+
checkValid(instant);
89+
90+
return instant;
91+
}
92+
93+
/** Adds a duration to an instant. */
94+
public static Instant add(Instant ts, java.time.Duration dur) {
95+
Instant newInstant = ts.plus(dur);
96+
checkValid(newInstant);
97+
98+
return newInstant;
99+
}
100+
101+
/** Adds two durations */
102+
public static java.time.Duration add(java.time.Duration d1, java.time.Duration d2) {
103+
java.time.Duration newDuration = d1.plus(d2);
104+
checkValid(newDuration);
105+
106+
return newDuration;
107+
}
108+
109+
/** Subtracts a duration to an instant. */
110+
public static Instant subtract(Instant ts, java.time.Duration dur) {
111+
Instant newInstant = ts.minus(dur);
112+
checkValid(newInstant);
113+
114+
return newInstant;
115+
}
116+
117+
/** Subtract a duration from another. */
118+
public static java.time.Duration subtract(java.time.Duration d1, java.time.Duration d2) {
119+
java.time.Duration newDuration = d1.minus(d2);
120+
checkValid(newDuration);
121+
122+
return newDuration;
123+
}
124+
125+
/**
126+
* Formats a {@link java.time.Duration} into a minimal seconds-based representation.
127+
*
128+
* <p>Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation
129+
*/
130+
public static String toString(java.time.Duration duration) {
131+
if (duration.isZero()) {
132+
return "0s";
133+
}
134+
135+
long totalNanos = duration.toNanos();
136+
StringBuilder sb = new StringBuilder();
137+
138+
if (totalNanos < 0) {
139+
sb.append('-');
140+
totalNanos = -totalNanos;
141+
}
142+
143+
long seconds = totalNanos / 1_000_000_000;
144+
int nanos = (int) (totalNanos % 1_000_000_000);
145+
146+
sb.append(seconds);
147+
148+
// Follows ProtoTimeUtils.toString(Duration) implementation
149+
if (nanos > 0) {
150+
sb.append('.');
151+
if (nanos % 1_000_000 == 0) {
152+
// Millisecond precision (3 digits)
153+
int millis = nanos / 1_000_000;
154+
sb.append(String.format(Locale.getDefault(), "%03d", millis));
155+
} else if (nanos % 1_000 == 0) {
156+
// Microsecond precision (6 digits)
157+
int micros = nanos / 1_000;
158+
sb.append(String.format(Locale.getDefault(), "%06d", micros));
159+
} else {
160+
// Nanosecond precision (9 digits)
161+
sb.append(String.format(Locale.getDefault(), "%09d", nanos));
162+
}
163+
}
164+
165+
sb.append('s');
166+
return sb.toString();
167+
}
168+
169+
/**
170+
* Get the DateTimeZone Instance.
171+
*
172+
* @param tz the ID of the datetime zone
173+
* @return the ZoneId object
174+
*/
175+
private static ZoneId timeZone(String tz) {
176+
try {
177+
return ZoneId.of(tz);
178+
} catch (DateTimeException e) {
179+
// If timezone is not a string name (for example, 'US/Central'), it should be a numerical
180+
// offset from UTC in the format [+/-]HH:MM.
181+
try {
182+
int ind = tz.indexOf(":");
183+
if (ind == -1) {
184+
throw new CelRuntimeException(e, CelErrorCode.BAD_FORMAT);
185+
}
186+
187+
int hourOffset = Integer.parseInt(tz.substring(0, ind));
188+
int minOffset = Integer.parseInt(tz.substring(ind + 1));
189+
// Ensures that the offset are properly formatted in [+/-]HH:MM to conform with
190+
// ZoneOffset's format requirements.
191+
// Example: "-9:30" -> "-09:30" and "9:30" -> "+09:30"
192+
String formattedOffset =
193+
((hourOffset < 0) ? "-" : "+")
194+
+ String.format(Locale.getDefault(), "%02d:%02d", Math.abs(hourOffset), minOffset);
195+
196+
return ZoneId.of(formattedOffset);
197+
198+
} catch (DateTimeException e2) {
199+
throw new CelRuntimeException(e2, CelErrorCode.BAD_FORMAT);
200+
}
201+
}
202+
}
203+
204+
/** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */
205+
private static void checkValid(Instant instant) {
206+
long seconds = instant.getEpochSecond();
207+
208+
if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) {
209+
throw new IllegalArgumentException(
210+
Strings.lenientFormat(
211+
"Timestamp is not valid. "
212+
+ "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. "
213+
+ "Nanos (%s) must be in range [0, +999,999,999].",
214+
seconds, instant.getNano()));
215+
}
216+
}
217+
218+
/** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */
219+
private static java.time.Duration checkValid(java.time.Duration duration) {
220+
long seconds = duration.getSeconds();
221+
int nanos = duration.getNano();
222+
if (!isDurationValid(seconds, nanos)) {
223+
throw new IllegalArgumentException(
224+
Strings.lenientFormat(
225+
"Duration is not valid. "
226+
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
227+
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
228+
+ "Nanos must have the same sign as seconds",
229+
seconds, nanos));
230+
}
231+
return duration;
232+
}
233+
234+
/**
235+
* Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code
236+
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
237+
* value must be in the range [-999,999,999, +999,999,999].
238+
*
239+
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
240+
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
241+
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
242+
*/
243+
private static boolean isDurationValid(long seconds, int nanos) {
244+
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
245+
return false;
246+
}
247+
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
248+
return false;
249+
}
250+
if (seconds < 0 || nanos < 0) {
251+
if (seconds > 0 || nanos > 0) {
252+
return false;
253+
}
254+
}
255+
return true;
256+
}
257+
258+
private DateTimeHelpers() {}
259+
}

common/src/main/java/dev/cel/common/internal/ProtoAdapter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public final class ProtoAdapter {
125125

126126
public ProtoAdapter(DynamicProto dynamicProto, CelOptions celOptions) {
127127
this.dynamicProto = checkNotNull(dynamicProto);
128-
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions.enableUnsignedLongs());
128+
this.protoLiteAdapter = new ProtoLiteAdapter(celOptions);
129129
this.celOptions = celOptions;
130130
}
131131

0 commit comments

Comments
 (0)