Skip to content

Commit 807b3e7

Browse files
authored
Feat/custom readers (#13)
* feat: add custom readers * feat: version
1 parent 9b61daa commit 807b3e7

7 files changed

Lines changed: 1010 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ build/
44
!gradle/wrapper/gradle-wrapper.jar
55
!**/src/main/**/build/
66
!**/src/test/**/build/
7+
CLAUDE.md
78

89
### IntelliJ IDEA ###
910
.idea/modules.xml

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=1.1.1
1+
version=1.2.0

src/main/java/fr/traqueur/structura/conversion/ValueConverter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import fr.traqueur.structura.api.Loadable;
55
import fr.traqueur.structura.exceptions.StructuraException;
66
import fr.traqueur.structura.factory.RecordInstanceFactory;
7+
import fr.traqueur.structura.registries.CustomReaderRegistry;
78
import fr.traqueur.structura.registries.PolymorphicRegistry;
89

910
import java.lang.reflect.ParameterizedType;
@@ -35,6 +36,12 @@ public ValueConverter(RecordInstanceFactory recordFactory) {
3536
public Object convert(Object value, Type genericType, Class<?> rawType, String prefix) {
3637
if (value == null) return null;
3738

39+
// Try custom reader first
40+
Optional<?> customResult = CustomReaderRegistry.getInstance().convert(value, rawType);
41+
if (customResult.isPresent()) {
42+
return customResult.get();
43+
}
44+
3845
if (rawType.isAssignableFrom(value.getClass()) &&
3946
!needsSpecialConversion(rawType, genericType)) {
4047
return value;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package fr.traqueur.structura.readers;
2+
3+
import fr.traqueur.structura.exceptions.StructuraException;
4+
5+
/**
6+
* A functional interface for custom type conversion from String values.
7+
* Readers enable Structura to convert YAML string values into custom types
8+
* that are not natively supported by the library.
9+
*
10+
* <p>This is particularly useful for external libraries like Adventure API,
11+
* where YAML strings need to be converted to complex objects.</p>
12+
*
13+
* <p>Example usage with Adventure API:</p>
14+
* <pre>
15+
* CustomReaderRegistry.getInstance().register(
16+
* Component.class,
17+
* str -&gt; MiniMessage.miniMessage().deserialize(str)
18+
* );
19+
* </pre>
20+
*
21+
* @param <T> the target type to convert to
22+
*/
23+
@FunctionalInterface
24+
public interface Reader<T> {
25+
26+
/**
27+
* Reads a string value and converts it to the target type.
28+
*
29+
* @param value the string value to convert
30+
* @return the converted object of type T
31+
* @throws StructuraException if conversion fails for any reason
32+
*/
33+
T read(String value) throws StructuraException;
34+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package fr.traqueur.structura.registries;
2+
3+
import fr.traqueur.structura.exceptions.StructuraException;
4+
import fr.traqueur.structura.readers.Reader;
5+
6+
import java.util.Optional;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
import java.util.concurrent.ConcurrentMap;
9+
10+
/**
11+
* Registry for custom type readers that convert String values to custom types.
12+
* This registry enables Structura to support external libraries and custom types
13+
* by providing user-defined conversion logic.
14+
*
15+
* <p>This is a thread-safe singleton that manages custom readers for types
16+
* that require special conversion from YAML string values.</p>
17+
*
18+
* <p>Example usage with Adventure API:</p>
19+
* <pre>
20+
* // Registration (once, typically at application startup)
21+
* CustomReaderRegistry.getInstance().register(
22+
* Component.class,
23+
* str -&gt; MiniMessage.miniMessage().deserialize(str)
24+
* );
25+
*
26+
* // Configuration record
27+
* public record MessageConfig(
28+
* Component welcomeMessage,
29+
* Component errorMessage
30+
* ) implements Loadable {}
31+
*
32+
* // YAML content
33+
* String yaml = """
34+
* welcome-message: "&lt;green&gt;Welcome!&lt;/green&gt;"
35+
* error-message: "&lt;red&gt;Error!&lt;/red&gt;"
36+
* """;
37+
*
38+
* // Parsing - automatic conversion via registered reader
39+
* MessageConfig config = Structura.parse(yaml, MessageConfig.class);
40+
* </pre>
41+
*
42+
* @see Reader
43+
*/
44+
public class CustomReaderRegistry {
45+
46+
private static final CustomReaderRegistry INSTANCE = new CustomReaderRegistry();
47+
48+
private final ConcurrentMap<Class<?>, Reader<?>> readers;
49+
50+
/**
51+
* Private constructor to enforce singleton pattern.
52+
*/
53+
private CustomReaderRegistry() {
54+
this.readers = new ConcurrentHashMap<>();
55+
}
56+
57+
/**
58+
* Gets the singleton instance of CustomReaderRegistry.
59+
*
60+
* @return the singleton instance
61+
*/
62+
public static CustomReaderRegistry getInstance() {
63+
return INSTANCE;
64+
}
65+
66+
/**
67+
* Registers a custom reader for a specific target type.
68+
* The reader will be used to convert YAML string values to the target type.
69+
*
70+
* @param targetClass the class of the target type
71+
* @param reader the reader to convert strings to the target type
72+
* @param <T> the type to convert to
73+
* @throws StructuraException if targetClass or reader is null, or if a reader is already registered
74+
*/
75+
public <T> void register(Class<T> targetClass, Reader<T> reader) {
76+
if (targetClass == null) {
77+
throw new StructuraException("Cannot register reader for null class");
78+
}
79+
if (reader == null) {
80+
throw new StructuraException("Cannot register null reader for class " + targetClass.getName());
81+
}
82+
if (readers.containsKey(targetClass)) {
83+
throw new StructuraException("A reader is already registered for class " + targetClass.getName());
84+
}
85+
readers.put(targetClass, reader);
86+
}
87+
88+
/**
89+
* Unregisters the custom reader for a specific target type.
90+
*
91+
* @param targetClass the class to unregister
92+
* @return true if a reader was removed, false if no reader was registered
93+
* @throws StructuraException if targetClass is null
94+
*/
95+
public boolean unregister(Class<?> targetClass) {
96+
if (targetClass == null) {
97+
throw new StructuraException("Cannot unregister reader for null class");
98+
}
99+
return readers.remove(targetClass) != null;
100+
}
101+
102+
/**
103+
* Checks if a custom reader is registered for the given class.
104+
*
105+
* @param targetClass the class to check
106+
* @return true if a reader is registered, false otherwise
107+
*/
108+
public boolean hasReader(Class<?> targetClass) {
109+
if (targetClass == null) {
110+
return false;
111+
}
112+
return readers.containsKey(targetClass);
113+
}
114+
115+
/**
116+
* Attempts to convert a value to the target type using a registered reader.
117+
* This method only works if:
118+
* <ul>
119+
* <li>The value is a String</li>
120+
* <li>A reader is registered for the target class</li>
121+
* </ul>
122+
*
123+
* @param value the value to convert (must be a String)
124+
* @param targetClass the target class
125+
* @param <T> the type to convert to
126+
* @return an Optional containing the converted value, or empty if no conversion is possible
127+
* @throws StructuraException if the reader throws an exception during conversion
128+
*/
129+
public <T> Optional<T> convert(Object value, Class<T> targetClass) {
130+
if (!(value instanceof String)) {
131+
return Optional.empty();
132+
}
133+
if (targetClass == null) {
134+
return Optional.empty();
135+
}
136+
137+
Reader<?> reader = readers.get(targetClass);
138+
if (reader == null) {
139+
return Optional.empty();
140+
}
141+
142+
try {
143+
String stringValue = (String) value;
144+
Object result = reader.read(stringValue);
145+
return Optional.of(targetClass.cast(result));
146+
} catch (Exception e) {
147+
throw new StructuraException(
148+
"Failed to convert value '" + value + "' to type " + targetClass.getName() +
149+
" using custom reader", e
150+
);
151+
}
152+
}
153+
154+
/**
155+
* Removes all registered readers.
156+
* This is primarily useful for testing.
157+
*/
158+
public void clear() {
159+
readers.clear();
160+
}
161+
162+
/**
163+
* Returns the number of registered readers.
164+
*
165+
* @return the count of registered readers
166+
*/
167+
public int size() {
168+
return readers.size();
169+
}
170+
}

0 commit comments

Comments
 (0)