Skip to content

Commit 23bb028

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 23bb028

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

+1159
-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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ java_library(
184184
"@maven//:com_google_errorprone_error_prone_annotations",
185185
"@maven//:com_google_guava_guava",
186186
"@maven//:com_google_protobuf_protobuf_java",
187+
"@maven_android//:com_google_protobuf_protobuf_javalite",
187188
],
188189
)
189190

@@ -195,14 +196,17 @@ java_library(
195196
deps = [
196197
":well_known_proto",
197198
"//common:error_codes",
199+
"//common:options",
198200
"//common:proto_json_adapter",
199201
"//common:runtime_exception",
200202
"//common/annotations",
203+
"//common/internal:proto_time_utils",
201204
"//common/values",
202205
"//common/values:cel_byte_string",
203206
"@maven//:com_google_errorprone_error_prone_annotations",
204207
"@maven//:com_google_guava_guava",
205208
"@maven//:com_google_protobuf_protobuf_java",
209+
"@maven_android//:com_google_protobuf_protobuf_javalite",
206210
],
207211
)
208212

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

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)