Skip to content

Commit 04f235f

Browse files
authored
Fix some formatting issues (#277)
This fixes a few issues noticed from failing conformance tests: * Quotes were being escaped in string values in lists * Durations had the word `duration` added to their formatting (along with escaped quotes) * Doubles were not formatting the same as CEL-Go. Namely, insignificant digits were not being removed (`1.0` should format as `1`, not `1.0`) In addition, this adds some additional unit tests for doubles to ensure the string formatting of doubles is adhering to the same behavior that CEL-Go uses.
1 parent 4801a39 commit 04f235f

File tree

4 files changed

+113
-74
lines changed

4 files changed

+113
-74
lines changed

build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ allprojects {
207207
}
208208
tasks.withType<Test>().configureEach {
209209
useJUnitPlatform()
210+
this.testLogging {
211+
events("failed")
212+
exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
213+
showExceptions = true
214+
showCauses = true
215+
showStackTraces = true
216+
}
210217
}
211218
}
212219

conformance/expected-failures.yaml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,3 @@ custom_rules:
1515
- field_expression/map/int64/invalid
1616
- field_expression/map/uint64/valid
1717
- field_expression/map/uint64/invalid
18-
kitchen_sink:
19-
- field/embedded/invalid
20-
- field/transitive/invalid
21-
- many/all-non-message-fields/invalid
22-
- field/invalid
23-
standard_rules/repeated:
24-
- items/in/invalid
25-
- items/not_in/invalid
26-
standard_rules/well_known_types/duration:
27-
- in/invalid
28-
- not in/invalid

src/main/java/build/buf/protovalidate/Format.java

Lines changed: 16 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -139,52 +139,25 @@ private static String bytesToHex(byte[] bytes, char[] digits) {
139139
* @param val the value to format.
140140
*/
141141
private static void formatString(StringBuilder builder, Val val) {
142-
if (val.type().typeEnum() == TypeEnum.String) {
143-
builder.append(val.value());
144-
} else if (val.type().typeEnum() == TypeEnum.Bytes) {
145-
builder.append(new String((byte[]) val.value(), StandardCharsets.UTF_8));
146-
} else if (val.type().typeEnum() == TypeEnum.Double) {
147-
DecimalFormat format = new DecimalFormat();
148-
builder.append(format.format(val.value()));
149-
} else {
150-
formatStringSafe(builder, val, false);
151-
}
152-
}
153-
154-
/**
155-
* Formats a string value safely for other value types.
156-
*
157-
* @param builder the StringBuilder to append the formatted string to.
158-
* @param val the value to format.
159-
* @param listType indicates if the value type is a list.
160-
*/
161-
private static void formatStringSafe(StringBuilder builder, Val val, boolean listType) {
162142
TypeEnum type = val.type().typeEnum();
163143
if (type == TypeEnum.Bool) {
164144
builder.append(val.booleanValue());
165-
} else if (type == TypeEnum.Int || type == TypeEnum.Uint) {
166-
formatDecimal(builder, val);
167-
} else if (type == TypeEnum.Double) {
168-
// When a double is nested in another type (e.g. a list) it will have a minimum of 6 decimal
169-
// digits. This is to maintain consistency with the Go CEL runtime.
170-
DecimalFormat format = new DecimalFormat();
171-
format.setMaximumFractionDigits(Integer.MAX_VALUE);
172-
format.setMinimumFractionDigits(6);
173-
builder.append(format.format(val.value()));
174-
} else if (type == TypeEnum.String) {
175-
builder.append("\"").append(val.value().toString()).append("\"");
145+
} else if (type == TypeEnum.String || type == TypeEnum.Int || type == TypeEnum.Uint) {
146+
builder.append(val.value());
176147
} else if (type == TypeEnum.Bytes) {
177-
formatBytes(builder, val);
148+
builder.append(new String((byte[]) val.value(), StandardCharsets.UTF_8));
149+
} else if (type == TypeEnum.Double) {
150+
formatDecimal(builder, val);
178151
} else if (type == TypeEnum.Duration) {
179-
formatDuration(builder, val, listType);
152+
formatDuration(builder, val);
180153
} else if (type == TypeEnum.Timestamp) {
181154
formatTimestamp(builder, val);
182155
} else if (type == TypeEnum.List) {
183156
formatList(builder, val);
184157
} else if (type == TypeEnum.Map) {
185-
throw new ErrException("unimplemented stringSafe map type");
158+
throw new ErrException("unimplemented string map type");
186159
} else if (type == TypeEnum.Null) {
187-
throw new ErrException("unimplemented stringSafe null type");
160+
throw new ErrException("unimplemented string null type");
188161
}
189162
}
190163

@@ -200,7 +173,7 @@ private static void formatList(StringBuilder builder, Val val) {
200173
List list = val.convertToNative(List.class);
201174
for (int i = 0; i < list.size(); i++) {
202175
Object obj = list.get(i);
203-
formatStringSafe(builder, DefaultTypeAdapter.nativeToValue(Db.newDb(), null, obj), true);
176+
formatString(builder, DefaultTypeAdapter.nativeToValue(Db.newDb(), null, obj));
204177
if (i != list.size() - 1) {
205178
builder.append(", ");
206179
}
@@ -225,35 +198,15 @@ private static void formatTimestamp(StringBuilder builder, Val val) {
225198
*
226199
* @param builder the StringBuilder to append the formatted duration value to.
227200
* @param val the value to format.
228-
* @param listType indicates if the value type is a list.
229201
*/
230-
private static void formatDuration(StringBuilder builder, Val val, boolean listType) {
231-
if (listType) {
232-
builder.append("duration(\"");
233-
}
202+
private static void formatDuration(StringBuilder builder, Val val) {
234203
Duration duration = val.convertToNative(Duration.class);
235204

236205
double totalSeconds = duration.getSeconds() + (duration.getNanos() / 1_000_000_000.0);
237206

238-
DecimalFormat format = new DecimalFormat("0.#########");
239-
builder.append(format.format(totalSeconds));
207+
DecimalFormat formatter = new DecimalFormat("0.#########");
208+
builder.append(formatter.format(totalSeconds));
240209
builder.append("s");
241-
if (listType) {
242-
builder.append("\")");
243-
}
244-
}
245-
246-
/**
247-
* Formats a byte array value.
248-
*
249-
* @param builder the StringBuilder to append the formatted byte array value to.
250-
* @param val the value to format.
251-
*/
252-
private static void formatBytes(StringBuilder builder, Val val) {
253-
builder
254-
.append("\"")
255-
.append(new String((byte[]) val.value(), StandardCharsets.UTF_8))
256-
.append("\"");
257210
}
258211

259212
/**
@@ -283,9 +236,10 @@ private static void formatHex(StringBuilder builder, Val val, char[] digits) {
283236
* Formats a decimal value.
284237
*
285238
* @param builder the StringBuilder to append the formatted decimal value to.
286-
* @param arg the value to format.
239+
* @param val the value to format.
287240
*/
288-
private static void formatDecimal(StringBuilder builder, Val arg) {
289-
builder.append(arg.value());
241+
private static void formatDecimal(StringBuilder builder, Val val) {
242+
DecimalFormat formatter = new DecimalFormat("0.#########");
243+
builder.append(formatter.format(val.value()));
290244
}
291245
}

src/test/java/build/buf/protovalidate/FormatTest.java

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,107 @@
1515
package build.buf.protovalidate;
1616

1717
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1819

20+
import com.google.protobuf.Duration;
1921
import org.junit.jupiter.api.Test;
22+
import org.projectnessie.cel.common.types.DoubleT;
23+
import org.projectnessie.cel.common.types.Err.ErrException;
2024
import org.projectnessie.cel.common.types.ListT;
25+
import org.projectnessie.cel.common.types.StringT;
2126
import org.projectnessie.cel.common.types.UintT;
27+
import org.projectnessie.cel.common.types.pb.DefaultTypeAdapter;
2228
import org.projectnessie.cel.common.types.ref.Val;
2329

2430
class FormatTest {
2531
@Test
26-
void largeDecimalValuesAreProperlyFormatted() {
32+
void testNotEnoughArgumentsThrows() {
33+
StringT one = StringT.stringOf("one");
34+
ListT val = (ListT) ListT.newValArrayList(null, new Val[] {one});
35+
36+
assertThatThrownBy(
37+
() -> {
38+
Format.format("first value: %s and %s", val);
39+
})
40+
.isInstanceOf(ErrException.class)
41+
.hasMessageContaining("format: not enough arguments");
42+
}
43+
44+
@Test
45+
void testDouble() {
46+
ListT val =
47+
(ListT)
48+
ListT.newValArrayList(
49+
null,
50+
new Val[] {
51+
DoubleT.doubleOf(-1.20000000000),
52+
DoubleT.doubleOf(-1.2),
53+
DoubleT.doubleOf(-1.230),
54+
DoubleT.doubleOf(-1.002),
55+
DoubleT.doubleOf(-0.1),
56+
DoubleT.doubleOf(-.1),
57+
DoubleT.doubleOf(-1),
58+
DoubleT.doubleOf(-0.0),
59+
DoubleT.doubleOf(0),
60+
DoubleT.doubleOf(0.0),
61+
DoubleT.doubleOf(1),
62+
DoubleT.doubleOf(0.1),
63+
DoubleT.doubleOf(.1),
64+
DoubleT.doubleOf(1.002),
65+
DoubleT.doubleOf(1.230),
66+
DoubleT.doubleOf(1.20000000000)
67+
});
68+
String formatted =
69+
Format.format("%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d", val);
70+
assertThat(formatted)
71+
.isEqualTo(
72+
"-1.2, -1.2, -1.23, -1.002, -0.1, -0.1, -1, -0, 0, 0, 1, 0.1, 0.1, 1.002, 1.23, 1.2");
73+
}
74+
75+
@Test
76+
void testLargeDecimalValuesAreProperlyFormatted() {
2777
UintT largeDecimal = UintT.uintOf(999999999999L);
2878
ListT val = (ListT) ListT.newValArrayList(null, new Val[] {largeDecimal});
2979
String formatted = Format.format("%s", val);
3080
assertThat(formatted).isEqualTo("999999999999");
3181
}
82+
83+
@Test
84+
void testDuration() {
85+
Duration duration = Duration.newBuilder().setSeconds(123).setNanos(45678).build();
86+
87+
ListT val =
88+
(ListT) ListT.newGenericArrayList(DefaultTypeAdapter.Instance, new Duration[] {duration});
89+
String formatted = Format.format("%s", val);
90+
assertThat(formatted).isEqualTo("123.000045678s");
91+
}
92+
93+
@Test
94+
void testEmptyDuration() {
95+
Duration duration = Duration.newBuilder().build();
96+
ListT val =
97+
(ListT) ListT.newGenericArrayList(DefaultTypeAdapter.Instance, new Duration[] {duration});
98+
String formatted = Format.format("%s", val);
99+
assertThat(formatted).isEqualTo("0s");
100+
}
101+
102+
@Test
103+
void testDurationSecondsOnly() {
104+
Duration duration = Duration.newBuilder().setSeconds(123).build();
105+
106+
ListT val =
107+
(ListT) ListT.newGenericArrayList(DefaultTypeAdapter.Instance, new Duration[] {duration});
108+
String formatted = Format.format("%s", val);
109+
assertThat(formatted).isEqualTo("123s");
110+
}
111+
112+
@Test
113+
void testDurationNanosOnly() {
114+
Duration duration = Duration.newBuilder().setNanos(42).build();
115+
116+
ListT val =
117+
(ListT) ListT.newGenericArrayList(DefaultTypeAdapter.Instance, new Duration[] {duration});
118+
String formatted = Format.format("%s", val);
119+
assertThat(formatted).isEqualTo("0.000000042s");
120+
}
32121
}

0 commit comments

Comments
 (0)