Skip to content

Commit f72723a

Browse files
Adds Duration Extension and Offset Function support (cedar-policy#331)
Signed-off-by: Mudit Chaudhary <[email protected]>
1 parent 998b25b commit f72723a

File tree

11 files changed

+910
-21
lines changed

11 files changed

+910
-21
lines changed

CedarJava/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ dependencies {
8383
implementation 'com.fizzed:jne:4.3.0'
8484
implementation 'com.google.guava:guava:33.4.0-jre'
8585
compileOnly 'com.github.spotbugs:spotbugs-annotations:4.8.6'
86+
compileOnly 'org.projectlombok:lombok:1.18.30'
87+
annotationProcessor 'org.projectlombok:lombok:1.18.30'
8688
testImplementation 'net.jqwik:jqwik:1.9.2'
8789
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.4'
8890
testImplementation 'org.skyscreamer:jsonassert:2.0-rc1'

CedarJava/src/main/java/com/cedarpolicy/serializer/ValueDeserializer.java

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.cedarpolicy.value.CedarMap;
2323
import com.cedarpolicy.value.DateTime;
2424
import com.cedarpolicy.value.Decimal;
25+
import com.cedarpolicy.value.Duration;
2526
import com.cedarpolicy.value.EntityIdentifier;
2627
import com.cedarpolicy.value.EntityTypeName;
2728
import com.cedarpolicy.value.EntityUID;
@@ -31,6 +32,7 @@
3132
import com.cedarpolicy.value.PrimString;
3233
import com.cedarpolicy.value.Unknown;
3334
import com.cedarpolicy.value.Value;
35+
import com.cedarpolicy.value.functions.Offset;
3436
import com.fasterxml.jackson.core.JsonParser;
3537
import com.fasterxml.jackson.databind.DeserializationContext;
3638
import com.fasterxml.jackson.databind.JsonDeserializer;
@@ -40,12 +42,22 @@
4042
import java.util.Iterator;
4143
import java.util.Map;
4244
import java.util.Optional;
45+
import java.util.Set;
4346

4447
/** Deserialize Json to Value. This is mostly an implementation detail, but you may need to modify it if you extend the
4548
* `Value` class. */
4649
public class ValueDeserializer extends JsonDeserializer<Value> {
4750
private static final String ENTITY_ESCAPE_SEQ = "__entity";
4851
private static final String EXTENSION_ESCAPE_SEQ = "__extn";
52+
private static final String FN_OFFSET = "offset";
53+
private static final String FN_IP = "ip";
54+
private static final String FN_DECIMAL = "decimal";
55+
private static final String FN_UNKNOWN = "unknown";
56+
private static final String FN_DATETIME = "datetime";
57+
private static final String FN_DURATION = "duration";
58+
59+
private static final Set<String> MULTI_ARG_FN = Set.of(FN_OFFSET);
60+
private static final Set<String> SINGLE_ARG_FN = Set.of(FN_IP, FN_DECIMAL, FN_UNKNOWN, FN_DATETIME, FN_DURATION);
4961

5062
private enum EscapeType {
5163
ENTITY,
@@ -116,19 +128,13 @@ public Value deserialize(JsonParser parser, DeserializationContext context) thro
116128
throw new InvalidValueDeserializationException(parser,
117129
"Not textual node: " + fn.toString(), node.asToken(), Map.class);
118130
}
119-
JsonNode arg = val.get("arg");
120-
if (!arg.isTextual()) {
121-
throw new InvalidValueDeserializationException(parser,
122-
"Not textual node: " + arg.toString(), node.asToken(), Map.class);
123-
}
124-
if (fn.textValue().equals("ip")) {
125-
return new IpAddress(arg.textValue());
126-
} else if (fn.textValue().equals("decimal")) {
127-
return new Decimal(arg.textValue());
128-
} else if (fn.textValue().equals("unknown")) {
129-
return new Unknown(arg.textValue());
130-
} else if (fn.textValue().equals("datetime")) {
131-
return new DateTime(arg.textValue());
131+
132+
String fnName = fn.textValue();
133+
134+
if (MULTI_ARG_FN.contains(fnName)) {
135+
return deserializeMultiArgFunction(fnName, val, mapper, parser, node);
136+
} else if (SINGLE_ARG_FN.contains(fnName)) {
137+
return deserializeSingleArgFunction(fnName, val, parser, node);
132138
} else {
133139
throw new InvalidValueDeserializationException(parser,
134140
"Invalid function type: " + fn.toString(), node.asToken(), Map.class);
@@ -153,4 +159,79 @@ public Value deserialize(JsonParser parser, DeserializationContext context) thro
153159
throw new DeserializationRecursionDepthException("Stack overflow while deserializing value. " + e.toString());
154160
}
155161
}
162+
163+
private Value deserializeMultiArgFunction(String fnName, JsonNode val, ObjectMapper mapper, JsonParser parser,
164+
JsonNode node) throws IOException {
165+
JsonNode args = val.get("args");
166+
if (args == null || !args.isArray()) {
167+
throw new InvalidValueDeserializationException(parser,
168+
"Expected args to be an array" + (args != null ? ", got: " + args.getNodeType() : ""),
169+
node.asToken(), Map.class);
170+
}
171+
172+
switch (fnName) {
173+
case FN_OFFSET:
174+
return deserializeOffset(args, mapper, parser, node);
175+
default:
176+
throw new InvalidValueDeserializationException(parser, "Invalid function type: " + fnName,
177+
node.asToken(), Map.class);
178+
}
179+
}
180+
181+
private Value deserializeSingleArgFunction(String fnName, JsonNode val, JsonParser parser, JsonNode node)
182+
throws IOException {
183+
JsonNode arg = val.get("arg");
184+
if (arg == null || !arg.isTextual()) {
185+
throw new InvalidValueDeserializationException(parser, "Not textual node: " + fnName, node.asToken(),
186+
Map.class);
187+
}
188+
189+
String argValue = arg.textValue();
190+
switch (fnName) {
191+
case FN_IP:
192+
return new IpAddress(argValue);
193+
case FN_DECIMAL:
194+
return new Decimal(argValue);
195+
case FN_UNKNOWN:
196+
return new Unknown(argValue);
197+
case FN_DATETIME:
198+
return new DateTime(argValue);
199+
case FN_DURATION:
200+
return new Duration(argValue);
201+
default:
202+
throw new InvalidValueDeserializationException(parser,
203+
"Invalid function type: " + fnName, node.asToken(), Map.class);
204+
}
205+
}
206+
207+
private Offset deserializeOffset(JsonNode args, ObjectMapper mapper, JsonParser parser, JsonNode node)
208+
throws IOException {
209+
if (args.size() != 2) {
210+
throw new InvalidValueDeserializationException(parser,
211+
"Offset requires exactly two arguments but got: " + args.size(), node.asToken(), Offset.class);
212+
}
213+
214+
try {
215+
Value dateTimeValue = mapper.treeToValue(args.get(0), Value.class);
216+
Value durationValue = mapper.treeToValue(args.get(1), Value.class);
217+
218+
if (!(dateTimeValue instanceof DateTime)) {
219+
throw new InvalidValueDeserializationException(parser,
220+
"Offset first argument must be DateTime but got: " + dateTimeValue.getClass().getSimpleName(),
221+
node.asToken(), Offset.class);
222+
}
223+
224+
if (!(durationValue instanceof Duration)) {
225+
throw new InvalidValueDeserializationException(parser,
226+
"Offset second argument must be Duration but got: " + durationValue.getClass().getSimpleName(),
227+
node.asToken(), Offset.class);
228+
}
229+
230+
return new Offset((DateTime) dateTimeValue, (Duration) durationValue);
231+
232+
} catch (IOException e) {
233+
throw new InvalidValueDeserializationException(parser,
234+
"Failed to deserialize Offset arguments: " + e.getMessage(), node.asToken(), Offset.class);
235+
}
236+
}
156237
}

CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@
2121
import com.cedarpolicy.value.CedarMap;
2222
import com.cedarpolicy.value.DateTime;
2323
import com.cedarpolicy.value.Decimal;
24+
import com.cedarpolicy.value.Duration;
2425
import com.cedarpolicy.value.EntityUID;
2526
import com.cedarpolicy.value.IpAddress;
2627
import com.cedarpolicy.value.PrimBool;
2728
import com.cedarpolicy.value.PrimLong;
2829
import com.cedarpolicy.value.PrimString;
2930
import com.cedarpolicy.value.Unknown;
3031
import com.cedarpolicy.value.Value;
32+
import com.cedarpolicy.value.functions.Offset;
3133
import com.fasterxml.jackson.core.JsonGenerator;
3234
import com.fasterxml.jackson.databind.JsonSerializer;
3335
import com.fasterxml.jackson.databind.SerializerProvider;
@@ -113,6 +115,30 @@ public void serialize(
113115
jsonGenerator.writeString(value.toString());
114116
jsonGenerator.writeEndObject();
115117
jsonGenerator.writeEndObject();
118+
} else if (value instanceof Duration) {
119+
jsonGenerator.writeStartObject();
120+
jsonGenerator.writeFieldName(EXTENSION_ESCAPE_SEQ);
121+
jsonGenerator.writeStartObject();
122+
jsonGenerator.writeFieldName("fn");
123+
jsonGenerator.writeString("duration");
124+
jsonGenerator.writeFieldName("arg");
125+
jsonGenerator.writeString(value.toString());
126+
jsonGenerator.writeEndObject();
127+
jsonGenerator.writeEndObject();
128+
} else if (value instanceof Offset) {
129+
Offset offsetValue = (Offset) value;
130+
jsonGenerator.writeStartObject();
131+
jsonGenerator.writeFieldName(EXTENSION_ESCAPE_SEQ);
132+
jsonGenerator.writeStartObject();
133+
jsonGenerator.writeFieldName("fn");
134+
jsonGenerator.writeString("offset");
135+
jsonGenerator.writeFieldName("args");
136+
CedarList args = new CedarList();
137+
args.add(offsetValue.getDateTime());
138+
args.add(offsetValue.getOffsetDuration());
139+
jsonGenerator.writeObject(args);
140+
jsonGenerator.writeEndObject();
141+
jsonGenerator.writeEndObject();
116142
} else {
117143
// It is recommended that you extend the Value classes in
118144
// main.java.com.cedarpolicy.model.value or that you convert your class to a CedarMap

CedarJava/src/main/java/com/cedarpolicy/value/Decimal.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
public class Decimal extends Value {
3030

3131
private static class DecimalValidator {
32-
private static final Pattern DECIMAL_PATTERN = Pattern.compile("^([0-9])*(\\.)([0-9]{0,4})$");
32+
private static final Pattern DECIMAL_PATTERN = Pattern.compile("^-?([0-9])*(\\.)([0-9]{0,4})$");
3333

3434
public static boolean validDecimal(String d) {
3535
if (d == null || d.isEmpty()) {

0 commit comments

Comments
 (0)