Skip to content

Commit f174988

Browse files
l46kokcopybara-github
authored andcommitted
Evaluate CEL's timestamp and duration types to their native equivalent values
PiperOrigin-RevId: 808664776
1 parent 66fde12 commit f174988

File tree

51 files changed

+1200
-841
lines changed

Some content is hidden

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

51 files changed

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

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)