Skip to content

Commit 2a6cd23

Browse files
committed
refactor: Extract form-body schema coercion from FormUrlEncodedParser
1 parent ad51067 commit 2a6cd23

4 files changed

Lines changed: 56 additions & 38 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.retailsvc.http.internal;
2+
3+
import com.retailsvc.http.spec.schema.ArraySchema;
4+
import com.retailsvc.http.spec.schema.ObjectSchema;
5+
import com.retailsvc.http.spec.schema.Schema;
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
/**
11+
* Coerces string-typed values produced by {@link FormUrlEncodedParser} into the Java types
12+
* described by the body schema (numbers, booleans, arrays). Called by {@link
13+
* RequestPreparationFilter} after parsing, before validation.
14+
*/
15+
final class FormBodyCoercion {
16+
17+
private FormBodyCoercion() {}
18+
19+
static Map<String, Object> coerce(Map<String, Object> parsed, Schema schema) {
20+
if (!(schema instanceof ObjectSchema obj)) {
21+
return parsed;
22+
}
23+
Map<String, Schema> properties = obj.properties();
24+
for (Map.Entry<String, Object> e : parsed.entrySet()) {
25+
Schema propSchema = properties.get(e.getKey());
26+
if (propSchema == null) {
27+
continue;
28+
}
29+
String pointer = "/" + e.getKey();
30+
Object value = e.getValue();
31+
if (propSchema instanceof ArraySchema arr && value instanceof List<?> list) {
32+
List<Object> coerced = new ArrayList<>(list.size());
33+
for (int i = 0; i < list.size(); i++) {
34+
coerced.add(ValueCoercion.coerce((String) list.get(i), arr.items(), pointer + "/" + i));
35+
}
36+
e.setValue(coerced);
37+
} else if (propSchema instanceof ArraySchema arr && value instanceof String s) {
38+
e.setValue(List.of(ValueCoercion.coerce(s, arr.items(), pointer + "/0")));
39+
} else if (value instanceof String s) {
40+
e.setValue(ValueCoercion.coerce(s, propSchema, pointer));
41+
}
42+
}
43+
return parsed;
44+
}
45+
}

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

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package com.retailsvc.http.internal;
22

33
import com.retailsvc.http.ValidationException;
4-
import com.retailsvc.http.spec.schema.ArraySchema;
5-
import com.retailsvc.http.spec.schema.ObjectSchema;
6-
import com.retailsvc.http.spec.schema.Schema;
74
import com.retailsvc.http.validate.ValidationError;
85
import java.net.URLDecoder;
96
import java.nio.charset.Charset;
@@ -71,35 +68,6 @@ private static void addEntry(Map<String, Object> out, String key, String value)
7168
});
7269
}
7370

74-
/** Returns the parsed map after coercing field values against the given body schema. */
75-
public Map<String, Object> parseAndCoerce(byte[] body, String contentTypeHeader, Schema schema) {
76-
Map<String, Object> parsed = parse(body, contentTypeHeader);
77-
if (!(schema instanceof ObjectSchema obj)) {
78-
return parsed;
79-
}
80-
Map<String, Schema> properties = obj.properties();
81-
for (Map.Entry<String, Object> e : parsed.entrySet()) {
82-
Schema propSchema = properties.get(e.getKey());
83-
if (propSchema == null) {
84-
continue;
85-
}
86-
String pointer = "/" + e.getKey();
87-
Object value = e.getValue();
88-
if (propSchema instanceof ArraySchema arr && value instanceof List<?> list) {
89-
List<Object> coerced = new ArrayList<>(list.size());
90-
for (int i = 0; i < list.size(); i++) {
91-
coerced.add(ValueCoercion.coerce((String) list.get(i), arr.items(), pointer + "/" + i));
92-
}
93-
e.setValue(coerced);
94-
} else if (propSchema instanceof ArraySchema arr && value instanceof String s) {
95-
e.setValue(List.of(ValueCoercion.coerce(s, arr.items(), pointer + "/0")));
96-
} else if (value instanceof String s) {
97-
e.setValue(ValueCoercion.coerce(s, propSchema, pointer));
98-
}
99-
}
100-
return parsed;
101-
}
102-
10371
private static Charset resolveCharset(String header) {
10472
return ContentTypeHeader.parameter(header, "charset")
10573
.map(FormUrlEncodedParser::safeCharset)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ private Object validateAndParseBody(HttpExchange exchange, Operation op, byte[]
153153
Object parsed =
154154
switch (mediaType) {
155155
case "application/x-www-form-urlencoded" ->
156-
formParser.parseAndCoerce(body, header, mt.schema());
156+
FormBodyCoercion.coerce(formParser.parse(body, header), mt.schema());
157157
case "text/plain" -> textParser.parse(body, header);
158158
default -> jsonMapper.mapFrom(body);
159159
};

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ void coercesIntegerProperty() {
8484
ObjectSchema bodySchema = anObjectSchema(Map.of("age", intSchema));
8585

8686
Map<String, Object> out =
87-
parser.parseAndCoerce("age=30".getBytes(StandardCharsets.UTF_8), null, bodySchema);
87+
FormBodyCoercion.coerce(
88+
parser.parse("age=30".getBytes(StandardCharsets.UTF_8), null), bodySchema);
8889

8990
assertThat(out).containsExactly(Map.entry("age", 30L));
9091
}
@@ -96,7 +97,8 @@ void coercesArrayOfIntegersProperty() {
9697
ObjectSchema bodySchema = anObjectSchema(Map.of("ids", arrSchema));
9798

9899
Map<String, Object> out =
99-
parser.parseAndCoerce("ids=1&ids=2".getBytes(StandardCharsets.UTF_8), null, bodySchema);
100+
FormBodyCoercion.coerce(
101+
parser.parse("ids=1&ids=2".getBytes(StandardCharsets.UTF_8), null), bodySchema);
100102

101103
assertThat(out).containsExactly(Map.entry("ids", List.of(1L, 2L)));
102104
}
@@ -108,7 +110,8 @@ void coercionFailureThrowsValidationExceptionAtPropertyPointer() {
108110

109111
org.assertj.core.api.Assertions.assertThatThrownBy(
110112
() ->
111-
parser.parseAndCoerce("age=abc".getBytes(StandardCharsets.UTF_8), null, bodySchema))
113+
FormBodyCoercion.coerce(
114+
parser.parse("age=abc".getBytes(StandardCharsets.UTF_8), null), bodySchema))
112115
.isInstanceOf(com.retailsvc.http.ValidationException.class)
113116
.extracting("error.pointer", "error.keyword")
114117
.containsExactly("/age", "type");
@@ -119,7 +122,8 @@ void unknownPropertyPassesThroughUnchanged() {
119122
ObjectSchema bodySchema = anObjectSchema(Map.of());
120123

121124
Map<String, Object> out =
122-
parser.parseAndCoerce("anything=v".getBytes(StandardCharsets.UTF_8), null, bodySchema);
125+
FormBodyCoercion.coerce(
126+
parser.parse("anything=v".getBytes(StandardCharsets.UTF_8), null), bodySchema);
123127

124128
assertThat(out).containsExactly(Map.entry("anything", "v"));
125129
}
@@ -129,7 +133,8 @@ void nonObjectSchemaReturnsRawMap() {
129133
StringSchema strSchema = aStringSchema();
130134

131135
Map<String, Object> out =
132-
parser.parseAndCoerce("a=1".getBytes(StandardCharsets.UTF_8), null, strSchema);
136+
FormBodyCoercion.coerce(
137+
parser.parse("a=1".getBytes(StandardCharsets.UTF_8), null), strSchema);
133138

134139
assertThat(out).containsExactly(Map.entry("a", "1"));
135140
}

0 commit comments

Comments
 (0)