Skip to content

Commit 777d7a5

Browse files
committed
feat: Form body field coercion against ObjectSchema property types
1 parent 5c496bd commit 777d7a5

2 files changed

Lines changed: 109 additions & 5 deletions

File tree

src/main/java/com/retailsvc/http/internal/FormUrlEncodedParser.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.retailsvc.http.internal;
22

3+
import com.retailsvc.http.spec.schema.ArraySchema;
4+
import com.retailsvc.http.spec.schema.ObjectSchema;
35
import com.retailsvc.http.spec.schema.Schema;
46
import java.net.URLDecoder;
57
import java.nio.charset.Charset;
@@ -54,12 +56,33 @@ private static void addEntry(Map<String, Object> out, String key, String value)
5456
out.put(key, list);
5557
}
5658

57-
/**
58-
* Returns the parsed map after coercing field values against the given body schema. Coercion is
59-
* added in a subsequent task; for now this delegates to {@link #parse}.
60-
*/
59+
/** Returns the parsed map after coercing field values against the given body schema. */
6160
public Map<String, Object> parseAndCoerce(byte[] body, String contentTypeHeader, Schema schema) {
62-
return parse(body, contentTypeHeader);
61+
Map<String, Object> parsed = parse(body, contentTypeHeader);
62+
if (!(schema instanceof ObjectSchema obj)) {
63+
return parsed;
64+
}
65+
Map<String, Schema> properties = obj.properties();
66+
for (Map.Entry<String, Object> e : parsed.entrySet()) {
67+
Schema propSchema = properties.get(e.getKey());
68+
if (propSchema == null) {
69+
continue;
70+
}
71+
String pointer = "/" + e.getKey();
72+
Object value = e.getValue();
73+
if (propSchema instanceof ArraySchema arr && value instanceof List<?> list) {
74+
List<Object> coerced = new ArrayList<>(list.size());
75+
for (int i = 0; i < list.size(); i++) {
76+
coerced.add(ValueCoercion.coerce((String) list.get(i), arr.items(), pointer + "/" + i));
77+
}
78+
e.setValue(coerced);
79+
} else if (propSchema instanceof ArraySchema arr && value instanceof String s) {
80+
e.setValue(List.of(ValueCoercion.coerce(s, arr.items(), pointer + "/0")));
81+
} else if (value instanceof String s) {
82+
e.setValue(ValueCoercion.coerce(s, propSchema, pointer));
83+
}
84+
}
85+
return parsed;
6386
}
6487

6588
private static Charset resolveCharset(String header) {

src/test/java/com/retailsvc/http/internal/FormUrlEncodedParserTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44

5+
import com.retailsvc.http.spec.schema.ArraySchema;
6+
import com.retailsvc.http.spec.schema.IntegerSchema;
7+
import com.retailsvc.http.spec.schema.ObjectSchema;
8+
import com.retailsvc.http.spec.schema.Schema;
9+
import com.retailsvc.http.spec.schema.StringSchema;
10+
import com.retailsvc.http.spec.schema.TypeName;
511
import java.nio.charset.StandardCharsets;
612
import java.util.List;
713
import java.util.Map;
14+
import java.util.Set;
815
import org.junit.jupiter.api.Test;
916

1017
class FormUrlEncodedParserTest {
@@ -70,4 +77,78 @@ void charsetFromHeader() {
7077
assertThat(parser.parse(iso, "application/x-www-form-urlencoded; charset=iso-8859-1"))
7178
.containsExactly(Map.entry("x", "räka"));
7279
}
80+
81+
@Test
82+
void coercesIntegerProperty() {
83+
IntegerSchema intSchema = anIntegerSchema();
84+
ObjectSchema bodySchema = anObjectSchema(Map.of("age", intSchema));
85+
86+
Map<String, Object> out =
87+
parser.parseAndCoerce("age=30".getBytes(StandardCharsets.UTF_8), null, bodySchema);
88+
89+
assertThat(out).containsExactly(Map.entry("age", 30L));
90+
}
91+
92+
@Test
93+
void coercesArrayOfIntegersProperty() {
94+
IntegerSchema intItems = anIntegerSchema();
95+
ArraySchema arrSchema = anArraySchemaOf(intItems);
96+
ObjectSchema bodySchema = anObjectSchema(Map.of("ids", arrSchema));
97+
98+
Map<String, Object> out =
99+
parser.parseAndCoerce("ids=1&ids=2".getBytes(StandardCharsets.UTF_8), null, bodySchema);
100+
101+
assertThat(out).containsExactly(Map.entry("ids", List.of(1L, 2L)));
102+
}
103+
104+
@Test
105+
void coercionFailureThrowsValidationExceptionAtPropertyPointer() {
106+
IntegerSchema intSchema = anIntegerSchema();
107+
ObjectSchema bodySchema = anObjectSchema(Map.of("age", intSchema));
108+
109+
org.assertj.core.api.Assertions.assertThatThrownBy(
110+
() ->
111+
parser.parseAndCoerce("age=abc".getBytes(StandardCharsets.UTF_8), null, bodySchema))
112+
.isInstanceOf(com.retailsvc.http.ValidationException.class)
113+
.extracting("error.pointer", "error.keyword")
114+
.containsExactly("/age", "type");
115+
}
116+
117+
@Test
118+
void unknownPropertyPassesThroughUnchanged() {
119+
ObjectSchema bodySchema = anObjectSchema(Map.of());
120+
121+
Map<String, Object> out =
122+
parser.parseAndCoerce("anything=v".getBytes(StandardCharsets.UTF_8), null, bodySchema);
123+
124+
assertThat(out).containsExactly(Map.entry("anything", "v"));
125+
}
126+
127+
@Test
128+
void nonObjectSchemaReturnsRawMap() {
129+
StringSchema strSchema = aStringSchema();
130+
131+
Map<String, Object> out =
132+
parser.parseAndCoerce("a=1".getBytes(StandardCharsets.UTF_8), null, strSchema);
133+
134+
assertThat(out).containsExactly(Map.entry("a", "1"));
135+
}
136+
137+
private static IntegerSchema anIntegerSchema() {
138+
return new IntegerSchema(
139+
Set.of(TypeName.INTEGER), null, null, null, null, null, null, Map.of());
140+
}
141+
142+
private static StringSchema aStringSchema() {
143+
return new StringSchema(Set.of(TypeName.STRING), null, null, null, null, null, Map.of());
144+
}
145+
146+
private static ArraySchema anArraySchemaOf(Schema items) {
147+
return new ArraySchema(Set.of(TypeName.ARRAY), items, null, null, false, Map.of());
148+
}
149+
150+
private static ObjectSchema anObjectSchema(Map<String, Schema> properties) {
151+
return new ObjectSchema(
152+
Set.of(TypeName.OBJECT), properties, List.of(), null, null, null, Map.of());
153+
}
73154
}

0 commit comments

Comments
 (0)