Skip to content

Commit 50aabc7

Browse files
committed
Fix PathSerializer support for non-strings
1 parent c8f9d8e commit 50aabc7

2 files changed

Lines changed: 162 additions & 10 deletions

File tree

http/http-binding/src/main/java/software/amazon/smithy/java/http/binding/PathSerializer.java

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
import java.util.List;
1111
import software.amazon.smithy.java.core.schema.Schema;
1212
import software.amazon.smithy.java.core.schema.SerializableStruct;
13+
import software.amazon.smithy.java.core.schema.SmithyEnum;
14+
import software.amazon.smithy.java.core.schema.SmithyIntEnum;
1315
import software.amazon.smithy.java.core.serde.SerializationException;
16+
import software.amazon.smithy.java.core.serde.document.Document;
1417
import software.amazon.smithy.java.io.uri.URLEncoding;
1518
import software.amazon.smithy.model.traits.HttpTrait;
1619

@@ -138,30 +141,53 @@ private static String formatLabelValue(SerializableStruct struct, Schema labelSc
138141
throw emptyLabel(labelSchema);
139142
}
140143

144+
// HTTP labels support string, enum, intEnum, boolean, byte/short/integer/long/float/double/bigInteger/
145+
// bigDecimal, and timestamp. The value can arrive in three shapes depending on the source struct:
146+
// - a raw Java value (String, Integer, Instant, ...) for generated shapes,
147+
// - a SmithyEnum/SmithyIntEnum for enum and intEnum members,
148+
// - a Document for any type when the value comes from a document-backed struct (e.g. DynamicClient).
149+
// Normalize all three down to the formatting below.
141150
return switch (labelSchema.type()) {
142151
case STRING, ENUM -> {
143-
var s = (String) value;
152+
var s = switch (value) {
153+
case String str -> str;
154+
case SmithyEnum e -> e.getValue();
155+
case Document d -> d.asString();
156+
default -> throw unsupportedLabelValue(labelSchema, value);
157+
};
144158
if (s.isEmpty()) {
145159
throw emptyLabel(labelSchema);
146160
}
147161
yield s;
148162
}
149-
case BOOLEAN -> Boolean.toString((boolean) value);
150-
case BYTE -> Byte.toString((byte) value);
151-
case SHORT -> Short.toString((short) value);
152-
case INTEGER, INT_ENUM -> Integer.toString((int) value);
153-
case LONG -> Long.toString((long) value);
154-
case FLOAT -> Float.toString((float) value);
155-
case DOUBLE -> Double.toString((double) value);
156-
case BIG_INTEGER, BIG_DECIMAL -> value.toString();
163+
case BOOLEAN -> Boolean.toString(value instanceof Document d ? d.asBoolean() : (boolean) value);
164+
case BYTE -> Byte.toString(value instanceof Document d ? d.asByte() : (byte) value);
165+
case SHORT -> Short.toString(value instanceof Document d ? d.asShort() : (short) value);
166+
case INTEGER -> Integer.toString(value instanceof Document d ? d.asInteger() : (int) value);
167+
case INT_ENUM -> Integer.toString(switch (value) {
168+
case Integer i -> i;
169+
case SmithyIntEnum e -> e.getValue();
170+
case Document d -> d.asInteger();
171+
default -> throw unsupportedLabelValue(labelSchema, value);
172+
});
173+
case LONG -> Long.toString(value instanceof Document d ? d.asLong() : (long) value);
174+
case FLOAT -> Float.toString(value instanceof Document d ? d.asFloat() : (float) value);
175+
case DOUBLE -> Double.toString(value instanceof Document d ? d.asDouble() : (double) value);
176+
case BIG_INTEGER -> (value instanceof Document d ? d.asBigInteger() : value).toString();
177+
case BIG_DECIMAL -> (value instanceof Document d ? d.asBigDecimal() : value).toString();
157178
case TIMESTAMP -> HttpBindingSchemaExtensions.memberBindingOf(labelSchema)
158179
.timestampFormatter()
159-
.writeString((Instant) value);
180+
.writeString(value instanceof Document d ? d.asTimestamp() : (Instant) value);
160181
default -> throw new SerializationException(
161182
"Unsupported HTTP label type " + labelSchema.type() + " for `" + labelSchema.id() + "`");
162183
};
163184
}
164185

186+
private static SerializationException unsupportedLabelValue(Schema labelSchema, Object value) {
187+
return new SerializationException(
188+
"Unsupported HTTP label value " + value.getClass() + " for `" + labelSchema.id() + "`");
189+
}
190+
165191
private static SerializationException emptyLabel(Schema labelSchema) {
166192
throw new SerializationException("HTTP label for `" + labelSchema.id() + "` cannot be empty");
167193
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.http.binding;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
10+
import java.math.BigDecimal;
11+
import java.math.BigInteger;
12+
import java.time.Instant;
13+
import java.util.List;
14+
import java.util.Map;
15+
import java.util.Set;
16+
import org.junit.jupiter.params.ParameterizedTest;
17+
import org.junit.jupiter.params.provider.Arguments;
18+
import org.junit.jupiter.params.provider.MethodSource;
19+
import software.amazon.smithy.java.core.schema.PreludeSchemas;
20+
import software.amazon.smithy.java.core.schema.Schema;
21+
import software.amazon.smithy.java.core.schema.SerializableStruct;
22+
import software.amazon.smithy.java.core.schema.SmithyEnum;
23+
import software.amazon.smithy.java.core.schema.SmithyIntEnum;
24+
import software.amazon.smithy.java.core.serde.ShapeSerializer;
25+
import software.amazon.smithy.java.core.serde.document.Document;
26+
import software.amazon.smithy.model.pattern.UriPattern;
27+
import software.amazon.smithy.model.shapes.ShapeId;
28+
import software.amazon.smithy.model.traits.HttpLabelTrait;
29+
import software.amazon.smithy.model.traits.HttpTrait;
30+
31+
public class PathSerializerTest {
32+
33+
private static final ShapeId INPUT_ID = ShapeId.from("smithy.example#Input");
34+
private static final ShapeId COLOR_ID = ShapeId.from("smithy.example#Color");
35+
private static final ShapeId COUNT_ID = ShapeId.from("smithy.example#Count");
36+
37+
private static HttpTrait httpTrait(String uri) {
38+
return HttpTrait.builder().method("GET").uri(UriPattern.parse(uri)).build();
39+
}
40+
41+
// Each case binds a single member `v` as a label and asserts the serialized path. Cases cover every label type
42+
// supported by HTTP bindings (string, enum, intEnum, boolean, the numerics, and timestamp) in each of the three
43+
// value shapes a struct can hand back: a raw Java value, a SmithyEnum/SmithyIntEnum, and a wrapping Document.
44+
static List<Arguments> labelCases() {
45+
var ts = Instant.parse("2020-01-02T03:04:05Z");
46+
return List.of(
47+
// string
48+
Arguments.of(PreludeSchemas.STRING, "abc", "/foo/abc"),
49+
Arguments.of(PreludeSchemas.STRING, Document.of("abc"), "/foo/abc"),
50+
// enum
51+
Arguments.of(enumSchema(), (SmithyEnum) () -> "RED", "/foo/RED"),
52+
Arguments.of(enumSchema(), Document.of("RED"), "/foo/RED"),
53+
// boolean
54+
Arguments.of(PreludeSchemas.BOOLEAN, true, "/foo/true"),
55+
Arguments.of(PreludeSchemas.BOOLEAN, Document.of(true), "/foo/true"),
56+
// byte
57+
Arguments.of(PreludeSchemas.BYTE, (byte) 7, "/foo/7"),
58+
Arguments.of(PreludeSchemas.BYTE, Document.of((byte) 7), "/foo/7"),
59+
// short
60+
Arguments.of(PreludeSchemas.SHORT, (short) 8, "/foo/8"),
61+
Arguments.of(PreludeSchemas.SHORT, Document.of((short) 8), "/foo/8"),
62+
// integer
63+
Arguments.of(PreludeSchemas.INTEGER, 9, "/foo/9"),
64+
Arguments.of(PreludeSchemas.INTEGER, Document.of(9), "/foo/9"),
65+
// intEnum
66+
Arguments.of(intEnumSchema(), (SmithyIntEnum) () -> 2, "/foo/2"),
67+
Arguments.of(intEnumSchema(), Document.of(2), "/foo/2"),
68+
// long
69+
Arguments.of(PreludeSchemas.LONG, 10L, "/foo/10"),
70+
Arguments.of(PreludeSchemas.LONG, Document.of(10L), "/foo/10"),
71+
// float
72+
Arguments.of(PreludeSchemas.FLOAT, 1.5f, "/foo/1.5"),
73+
Arguments.of(PreludeSchemas.FLOAT, Document.of(1.5f), "/foo/1.5"),
74+
// double
75+
Arguments.of(PreludeSchemas.DOUBLE, 2.5d, "/foo/2.5"),
76+
Arguments.of(PreludeSchemas.DOUBLE, Document.of(2.5d), "/foo/2.5"),
77+
// bigInteger
78+
Arguments.of(PreludeSchemas.BIG_INTEGER, BigInteger.valueOf(11), "/foo/11"),
79+
Arguments.of(PreludeSchemas.BIG_INTEGER, Document.of(BigInteger.valueOf(11)), "/foo/11"),
80+
// bigDecimal
81+
Arguments.of(PreludeSchemas.BIG_DECIMAL, new BigDecimal("12.5"), "/foo/12.5"),
82+
Arguments.of(PreludeSchemas.BIG_DECIMAL, Document.of(new BigDecimal("12.5")), "/foo/12.5"),
83+
// timestamp (default date-time / ISO-8601 format for a path label; ':' is percent-encoded)
84+
Arguments.of(PreludeSchemas.TIMESTAMP, ts, "/foo/2020-01-02T03%3A04%3A05Z"),
85+
Arguments.of(PreludeSchemas.TIMESTAMP, Document.of(ts), "/foo/2020-01-02T03%3A04%3A05Z"));
86+
}
87+
88+
@ParameterizedTest
89+
@MethodSource("labelCases")
90+
public void serializesLabel(Schema memberTarget, Object value, String expected) {
91+
var schema = Schema.structureBuilder(INPUT_ID)
92+
.putMember("v", memberTarget, new HttpLabelTrait())
93+
.build();
94+
var serializer = new PathSerializer(httpTrait("/foo/{v}"), schema);
95+
96+
assertEquals(expected, serializer.serialize(struct(schema, Map.of("v", value))));
97+
}
98+
99+
private static Schema enumSchema() {
100+
return Schema.createEnum(COLOR_ID, Set.of("RED", "GREEN"));
101+
}
102+
103+
private static Schema intEnumSchema() {
104+
return Schema.createIntEnum(COUNT_ID, Set.of(1, 2));
105+
}
106+
107+
private static SerializableStruct struct(Schema schema, Map<String, Object> values) {
108+
return new SerializableStruct() {
109+
@Override
110+
public Schema schema() {
111+
return schema;
112+
}
113+
114+
@Override
115+
public void serializeMembers(ShapeSerializer serializer) {
116+
// Not exercised by PathSerializer.
117+
}
118+
119+
@Override
120+
@SuppressWarnings("unchecked")
121+
public <T> T getMemberValue(Schema member) {
122+
return (T) values.get(member.memberName());
123+
}
124+
};
125+
}
126+
}

0 commit comments

Comments
 (0)