Skip to content

Commit 2c698c2

Browse files
committed
feat: strengthen mapper errors and add class reference support
1 parent 547e82f commit 2c698c2

30 files changed

Lines changed: 1328 additions & 62 deletions

modules/deserializer/src/main/java/org/msuo/config2java/AbstractScriptDeserializer.java

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,36 @@ protected abstract ConfigValue parse(
3232
private static Map<String, String> normalizeEnvironment(
3333
Map<String, String> environment
3434
) {
35-
return environment == null
36-
? Collections.emptyMap()
37-
: Collections.unmodifiableMap(new LinkedHashMap<>(environment));
35+
if (environment == null) return Collections.emptyMap();
36+
LinkedHashMap<String, String> normalized = new LinkedHashMap<>();
37+
for (Map.Entry<String, String> entry : environment.entrySet()) {
38+
String key = requireValidKey(entry.getKey(), "environment");
39+
normalized.put(key, entry.getValue());
40+
}
41+
return Collections.unmodifiableMap(normalized);
3842
}
3943

4044
private static Map<String, Object> normalizeGlobals(Map<String, ?> globals) {
41-
return globals == null
42-
? Collections.emptyMap()
43-
: Collections.unmodifiableMap(new LinkedHashMap<>(globals));
45+
if (globals == null) return Collections.emptyMap();
46+
LinkedHashMap<String, Object> normalized = new LinkedHashMap<>();
47+
for (Map.Entry<String, ?> entry : globals.entrySet()) {
48+
String key = requireValidKey(entry.getKey(), "globals");
49+
normalized.put(key, entry.getValue());
50+
}
51+
return Collections.unmodifiableMap(normalized);
52+
}
53+
54+
private static String requireValidKey(String key, String sourceName) {
55+
if (key == null) {
56+
throw new IllegalArgumentException(
57+
"Invalid " + sourceName + " key: key must not be null"
58+
);
59+
}
60+
if (key.trim().isEmpty()) {
61+
throw new IllegalArgumentException(
62+
"Invalid " + sourceName + " key: key must not be blank"
63+
);
64+
}
65+
return key;
4466
}
4567
}

modules/deserializer/src/main/java/org/msuo/config2java/ClassAdapter.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.msuo.config2java;
22

33
import java.lang.reflect.Constructor;
4-
import java.lang.reflect.InvocationTargetException;
54
import java.lang.reflect.Type;
65

76
final class ClassAdapter implements TypeAdapter {
@@ -79,11 +78,8 @@ private static Object instantiateNoArg(Path path, Class<?> cls, ErrorCollector e
7978
return c.newInstance();
8079
} catch (NoSuchMethodException e) {
8180
errors.add(path, Errors.noNoArgCtor(cls));
82-
} catch (InvocationTargetException e) {
83-
Throwable cause = (e.getCause() != null) ? e.getCause() : e;
84-
errors.add(path, Errors.ctorFailed(cls, cause));
8581
} catch (ReflectiveOperationException e) {
86-
errors.add(path, Errors.instantiateFailed(cls, e));
82+
errors.add(path, ReflectionErrorMapper.instantiateError(cls, e));
8783
}
8884
return null;
8985
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.msuo.config2java;
2+
3+
import java.lang.reflect.Type;
4+
5+
final class ClassReferenceAdapter implements TypeAdapter {
6+
7+
private final Type expectedType;
8+
9+
ClassReferenceAdapter(Type expectedType) {
10+
this.expectedType = expectedType;
11+
}
12+
13+
@Override
14+
public ReadResult read(Path path, ConfigValue value, ErrorCollector errors) {
15+
String className = ValueCoerce.stringOrError(
16+
path,
17+
value,
18+
errors,
19+
Errors::classRefExpectedString
20+
);
21+
if (className == null) return ReadResult.fail();
22+
23+
if (!(expectedType instanceof Class<?>)) {
24+
errors.add(path, Errors.unsupportedType(expectedType));
25+
return ReadResult.fail();
26+
}
27+
Class<?> expectedBaseType = (Class<?>) expectedType;
28+
29+
Class<?> resolved;
30+
try {
31+
resolved = loadClass(className);
32+
} catch (ClassNotFoundException e) {
33+
errors.add(path, Errors.classRefNotFound(className));
34+
return ReadResult.fail();
35+
}
36+
37+
if (!expectedBaseType.isAssignableFrom(resolved)) {
38+
errors.add(path, Errors.classRefNotAssignable(expectedBaseType, resolved));
39+
return ReadResult.fail();
40+
}
41+
42+
return ReadResult.ok(resolved);
43+
}
44+
45+
private static Class<?> loadClass(String className)
46+
throws ClassNotFoundException {
47+
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
48+
if (contextLoader != null) {
49+
return Class.forName(className, false, contextLoader);
50+
}
51+
return Class.forName(className);
52+
}
53+
}

modules/deserializer/src/main/java/org/msuo/config2java/ConfigDeserializationException.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.util.ArrayList;
44
import java.util.Collections;
55
import java.util.List;
6+
import java.util.Collection;
67
import java.util.Objects;
78
import java.util.function.BiConsumer;
89

@@ -12,7 +13,17 @@ public final class ConfigDeserializationException extends RuntimeException {
1213

1314
public ConfigDeserializationException(List<ConfigError> errors) {
1415
super("Config deserialization failed");
15-
this.errors = Collections.unmodifiableList(new ArrayList<ConfigError>(errors));
16+
this.errors = Collections.unmodifiableList(
17+
new ArrayList<ConfigError>(requireNonEmpty(errors, "errors"))
18+
);
19+
}
20+
21+
private static <T extends Collection<?>> T requireNonEmpty(T value, String name) {
22+
Objects.requireNonNull(value, name);
23+
if (value.isEmpty()) {
24+
throw new IllegalArgumentException(name + " must not be empty");
25+
}
26+
return value;
1627
}
1728

1829
public List<ConfigError> getErrors() {

modules/deserializer/src/main/java/org/msuo/config2java/ConfigErrorTypes.java

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.msuo.config2java;
22

33
import java.lang.reflect.Type;
4+
import java.lang.reflect.TypeVariable;
5+
import java.lang.reflect.WildcardType;
46
import java.util.Arrays;
57
import java.util.stream.Collectors;
68

@@ -14,12 +16,69 @@ public static final class UnsupportedType implements ConfigErrorType {
1416
@Override public String message() { return "Unsupported Type: " + type; }
1517
}
1618

19+
public static final class UnresolvedTypeVariable implements ConfigErrorType {
20+
private final TypeVariable<?> typeVariable;
21+
UnresolvedTypeVariable(TypeVariable<?> typeVariable) {
22+
this.typeVariable = typeVariable;
23+
}
24+
@Override
25+
public String message() {
26+
return "Unresolved generic type variable: " + typeVariable.getName();
27+
}
28+
}
29+
30+
public static final class WildcardTypeNotSupported implements ConfigErrorType {
31+
private final WildcardType wildcardType;
32+
WildcardTypeNotSupported(WildcardType wildcardType) {
33+
this.wildcardType = wildcardType;
34+
}
35+
@Override
36+
public String message() {
37+
return "Wildcard generic types are not supported here: " + wildcardType;
38+
}
39+
}
40+
1741
public static final class UnsupportedParameterizedRaw implements ConfigErrorType {
1842
private final Type raw;
1943
UnsupportedParameterizedRaw(Type raw) { this.raw = raw; }
2044
@Override public String message() { return "Unsupported parameterized raw type: " + raw; }
2145
}
2246

47+
public static final class ClassRefExpectedString implements ConfigErrorType {
48+
private final String gotTypeName;
49+
ClassRefExpectedString(String gotTypeName) {
50+
this.gotTypeName = gotTypeName;
51+
}
52+
@Override
53+
public String message() {
54+
return "Class reference expects string class name, got: " + gotTypeName;
55+
}
56+
}
57+
58+
public static final class ClassRefNotFound implements ConfigErrorType {
59+
private final String className;
60+
ClassRefNotFound(String className) {
61+
this.className = className;
62+
}
63+
@Override
64+
public String message() {
65+
return "Class not found: " + className;
66+
}
67+
}
68+
69+
public static final class ClassRefNotAssignable implements ConfigErrorType {
70+
private final Class<?> expectedBaseType;
71+
private final Class<?> actualType;
72+
ClassRefNotAssignable(Class<?> expectedBaseType, Class<?> actualType) {
73+
this.expectedBaseType = expectedBaseType;
74+
this.actualType = actualType;
75+
}
76+
@Override
77+
public String message() {
78+
return "Class " + actualType.getName() + " is not assignable to " + expectedBaseType.getName();
79+
}
80+
}
81+
2382
public static final class PrimitiveNotSupported implements ConfigErrorType {
2483
private final Class<?> primitive;
2584
PrimitiveNotSupported(Class<?> primitive) { this.primitive = primitive; }

modules/deserializer/src/main/java/org/msuo/config2java/EnumAdapter.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ final class EnumAdapter implements TypeAdapter {
1010

1111
@Override
1212
public ReadResult read(Path path, ConfigValue value, ErrorCollector errors) {
13-
ScalarValue scalar = value.asScalar();
14-
if (scalar == null || scalar.boxedType != String.class) {
15-
errors.add(path, Errors.enumExpectedString(value));
16-
return ReadResult.fail();
17-
}
18-
19-
String name = (String) scalar.value;
13+
String name = ValueCoerce.stringOrError(
14+
path,
15+
value,
16+
errors,
17+
Errors::enumExpectedString
18+
);
19+
if (name == null) return ReadResult.fail();
2020
try {
2121
@SuppressWarnings({ "unchecked", "rawtypes" })
2222
Object e = Enum.valueOf((Class<? extends Enum>) enumClass, name);

modules/deserializer/src/main/java/org/msuo/config2java/Errors.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.msuo.config2java;
22

33
import java.lang.reflect.Type;
4+
import java.lang.reflect.TypeVariable;
5+
import java.lang.reflect.WildcardType;
46

57
final class Errors {
68

@@ -10,10 +12,33 @@ static ConfigErrorType unsupportedType(Type t) {
1012
return new ConfigErrorTypes.UnsupportedType(t);
1113
}
1214

15+
static ConfigErrorType unresolvedTypeVariable(TypeVariable<?> tv) {
16+
return new ConfigErrorTypes.UnresolvedTypeVariable(tv);
17+
}
18+
19+
static ConfigErrorType wildcardTypeNotSupported(WildcardType wt) {
20+
return new ConfigErrorTypes.WildcardTypeNotSupported(wt);
21+
}
22+
1323
static ConfigErrorType unsupportedParameterizedRaw(Type raw) {
1424
return new ConfigErrorTypes.UnsupportedParameterizedRaw(raw);
1525
}
1626

27+
static ConfigErrorType classRefExpectedString(ConfigValue got) {
28+
return new ConfigErrorTypes.ClassRefExpectedString(got.typename());
29+
}
30+
31+
static ConfigErrorType classRefNotFound(String className) {
32+
return new ConfigErrorTypes.ClassRefNotFound(className);
33+
}
34+
35+
static ConfigErrorType classRefNotAssignable(
36+
Class<?> expectedBaseType,
37+
Class<?> actualType
38+
) {
39+
return new ConfigErrorTypes.ClassRefNotAssignable(expectedBaseType, actualType);
40+
}
41+
1742
static ConfigErrorType primitiveNotSupported(Class<?> prim) {
1843
return new ConfigErrorTypes.PrimitiveNotSupported(prim);
1944
}

modules/deserializer/src/main/java/org/msuo/config2java/FieldAccess.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Object readDefault(Object instance, Path path, ErrorCollector errors) {
2626
try {
2727
return field.get(instance);
2828
} catch (IllegalAccessException | RuntimeException e) {
29-
errors.add(path, Errors.fieldReadAccess(e));
29+
errors.add(path, ReflectionErrorMapper.fieldReadError(e));
3030
return READ_FAILED;
3131
}
3232
}
@@ -38,10 +38,8 @@ boolean isReadFailed(Object value) {
3838
void write(Object instance, Object value, Path path, ErrorCollector errors) {
3939
try {
4040
field.set(instance, value);
41-
} catch (IllegalAccessException e) {
42-
errors.add(path, Errors.fieldSetAccess(e));
43-
} catch (IllegalArgumentException e) {
44-
errors.add(path, Errors.fieldSetTypeMismatch(e));
41+
} catch (IllegalAccessException | IllegalArgumentException e) {
42+
errors.add(path, ReflectionErrorMapper.fieldWriteError(e));
4543
}
4644
}
4745
}

modules/deserializer/src/main/java/org/msuo/config2java/LeafReader.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.msuo.config2java;
22

33
import java.lang.reflect.Constructor;
4-
import java.lang.reflect.InvocationTargetException;
54

65
final class LeafReader {
76

@@ -24,12 +23,8 @@ static ReadResult readLeaf(Path path, Class<?> target, ConfigValue value, ErrorC
2423
try {
2524
ctor.setAccessible(true);
2625
return ReadResult.ok(ctor.newInstance(scalar.value));
27-
} catch (InvocationTargetException e) {
28-
Throwable cause = (e.getCause() != null) ? e.getCause() : e;
29-
errors.add(path, Errors.ctorRejected(target, cause, scalar.value));
30-
return ReadResult.fail();
3126
} catch (ReflectiveOperationException e) {
32-
errors.add(path, Errors.ctorCallFailed(target, e));
27+
errors.add(path, ReflectionErrorMapper.leafCtorError(target, scalar.value, e));
3328
return ReadResult.fail();
3429
}
3530
}

modules/deserializer/src/main/java/org/msuo/config2java/MapListConfigValue.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@ abstract class MapListConfigValue implements ConfigValue {
1515

1616
@Override
1717
public String typename() {
18-
if (value == null) return "nil";
1918
if (value instanceof Map || value instanceof List) return "table";
20-
if (value instanceof CharSequence) return "string";
21-
if (value instanceof Boolean) return "boolean";
22-
if (value instanceof Number) return "number";
23-
return "userdata";
19+
return JavaScalarConfigValue.typenameOf(value);
2420
}
2521

2622
@Override
@@ -46,10 +42,6 @@ public ConfigTable asTable() {
4642

4743
@Override
4844
public ScalarValue asScalar() {
49-
if (value == null) return null;
50-
if (value instanceof CharSequence) return ScalarValue.ofString(value.toString());
51-
if (value instanceof Boolean) return ScalarValue.ofBoolean((Boolean) value);
52-
if (value instanceof Number) return ScalarNumbers.fromNumber((Number) value);
53-
return null;
45+
return JavaScalarConfigValue.scalarOf(value);
5446
}
5547
}

0 commit comments

Comments
 (0)