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 -> 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: "<green>Welcome!</green>"
35+ * error-message: "<red>Error!</red>"
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