@@ -15,6 +15,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
1515import com.fasterxml.jackson.annotation.JsonProperty
1616import com.fasterxml.jackson.databind.ObjectMapper
1717import com.github.jasminb.jsonapi.StringIdHandler
18+ import com.github.jasminb.jsonapi.ResourceConverter
1819import com.github.jasminb.jsonapi.annotations.Id
1920import com.github.jasminb.jsonapi.annotations.Meta
2021import com.github.jasminb.jsonapi.annotations.Relationship
@@ -83,73 +84,90 @@ class FormSubmissionVersion @JsonCreator constructor(
8384 private fun resourceMapper (): ObjectMapper = JsonConfig .getMapper()
8485
8586 /* *
86- * Convert the raw resources list (List<Map<...>>) into typed JsonApiEnvelope objects.
87- * This keeps the original raw structure but provides a typed view over it.
87+ * Create a configured Jasminb ResourceConverter for the given target class and included classes.
88+ * Also include any classes registered in the companion type registry so the converter knows
89+ * about all registered resource types.
8890 */
89- fun resourcesAsEnvelopes (): List <JsonApiEnvelope > = resources?.mapNotNull { res ->
91+ private fun resourceConverter (targetClass : Class <* >, vararg includedClasses : Class <* >): ResourceConverter {
92+ // Use the registered classes plus the explicit target and included classes.
93+ val registryClasses: List <Class <* >> = companionTypeRegistryClasses()
94+
95+ val classesList: MutableList <Class <* >> = ArrayList ()
96+ classesList.add(targetClass)
97+ // add registry classes (skip duplicates)
98+ for (c in registryClasses) {
99+ if (! classesList.contains(c)) classesList.add(c)
100+ }
101+ // add explicitly passed included classes
102+ for (c in includedClasses) {
103+ if (! classesList.contains(c)) classesList.add(c)
104+ }
105+
106+ val classesArray: Array <Class <* >> = classesList.toTypedArray()
107+ val rc = ResourceConverter (resourceMapper(), * classesArray)
90108 try {
91- resourceMapper().convertValue(res, JsonApiEnvelope :: class .java )
92- } catch (e : Exception ) {
93- null
109+ rc.enableSerializationOption(com.github.jasminb.jsonapi. SerializationFeature . INCLUDE_RELATIONSHIP_ATTRIBUTES )
110+ } catch (_ : Throwable ) {
111+ // ignore
94112 }
95- } ? : emptyList()
113+ return rc
114+ }
96115
97- /* *
98- * Find the full envelope for a resource by id.
99- */
100- fun findResourceEnvelopeById ( id : String ): JsonApiEnvelope ? = resourcesAsEnvelopes().firstOrNull { it.data?.id == id }
116+ // Helper to access companion's registry as a list (keeps the converter creation tidy)
117+ private fun companionTypeRegistryClasses (): List < Class < * >> = synchronized( Companion ) {
118+ getAllRegisteredClasses()
119+ }
101120
102121 /* *
103- * Find the inner resource data object by id.
122+ * Find the raw JSON:API envelope Map for a resource by id.
104123 */
105- fun findResourceDataById (id : String ): JsonApiResourceData ? = findResourceEnvelopeById(id)?.data
124+ private fun findRawResourceEnvelopeById (id : String ): Map <String , Any >? {
125+ resources?.forEach { res ->
126+ val data = res[" data" ] as ? Map <* , * > ? : return @forEach
127+ val dataId = data[" id" ] as ? String
128+ if (dataId == id) {
129+ @Suppress(" UNCHECKED_CAST" )
130+ return res
131+ }
132+ }
133+ return null
134+ }
106135
107136 /* *
108- * Hydrate the attributes of a resource (by id) into a target class using Jackson.
109- * Example: hydrateResourceAttributesById("...", Image::class.java)
110- * Returns null if resource or attributes are missing or conversion fails.
137+ * Hydrate a resource by using Jasminb's ResourceConverter. Serialises the raw envelope Map
138+ * to bytes using the configured ObjectMapper and passes it to ResourceConverter.readDocument.
111139 */
112- fun <T > hydrateResourceAttributesById (id : String , clazz : Class <T >): T ? {
113- val attrs = findResourceDataById(id)?.attributes ? : return null
140+ private fun <T > hydrateResourceUsingConverter (id : String , clazz : Class <T >, vararg includedClasses : Class <* >): T ? {
141+ val envelope = findRawResourceEnvelopeById(id) ? : return null
142+
114143 return try {
115- resourceMapper().convertValue(attrs, clazz)
144+ val bytes = resourceMapper().writeValueAsBytes(envelope)
145+ val rc = resourceConverter(clazz, * includedClasses)
146+ val document = rc.readDocument(bytes, clazz)
147+ document.get()
116148 } catch (e: Exception ) {
117- null
149+ throw e
118150 }
119151 }
120152
121153 /* *
122- * Auto-hydrate a resource by looking up its JSON:API type and using the registered class for that type .
123- * Returns the hydrated object or null if not registered or conversion fails .
154+ * Hydrate a resource ( by id) into a target class using Jasminb ResourceConverter .
155+ * This is the primary public method for hydration .
124156 */
125- fun autoHydrateById (id : String ): Any? {
126- val env = findResourceEnvelopeById(id) ? : return null
127- val type = env.data?.type ? : return null
128- val clazz = getRegisteredClass(type) ? : return null
129- return hydrateResourceAttributesById(id, clazz)
157+ fun <T > hydrateResourceById (id : String , clazz : Class <T >): T ? {
158+ return hydrateResourceUsingConverter(id, clazz)
130159 }
131160
132161 /* *
133- * Reified convenience that attempts to auto-hydrate and cast to the expected type.
162+ * Auto-hydrate a resource by looking up its JSON:API type and using the registered class for that type.
134163 */
135- inline fun <reified T > autoHydrateByIdAs (id : String ): T ? {
136- val any = autoHydrateById(id) ? : return null
137- return any as ? T
138- }
164+ fun autoHydrateById (id : String ): Any? {
165+ val envelope = findRawResourceEnvelopeById(id) ? : return null
139166
140- /* *
141- * Backwards-compatible helper: original simple lookup that returns the inner "data" map.
142- */
143- fun findResourceById (id : String ): Map <String , Any >? {
144- resources?.forEach { res ->
145- val data = res[" data" ] as ? Map <* , * > ? : return @forEach
146- val dataId = data[" id" ] as ? String
147- if (dataId == id) {
148- @Suppress(" UNCHECKED_CAST" )
149- return data as Map <String , Any >
150- }
151- }
152- return null
167+ val data = envelope[" data" ] as ? Map <* , * > ? : return null
168+ val type = data[" type" ] as ? String ? : return null
169+ val clazz = getRegisteredClass(type) ? : return null
170+ return hydrateResourceUsingConverter(id, clazz)
153171 }
154172
155173 companion object {
@@ -161,6 +179,11 @@ class FormSubmissionVersion @JsonCreator constructor(
161179 }
162180
163181 fun getRegisteredClass (type : String ): Class <* >? = typeRegistry[type]
182+
183+ // expose a synchronized way to get the registry contents as a List
184+ fun getAllRegisteredClasses (): List <Class <* >> = synchronized(typeRegistry) {
185+ typeRegistry.values.toList()
186+ }
164187 }
165188}
166189
@@ -170,21 +193,3 @@ class FormSubmissionVersionMeta(
170193 @JsonProperty(" latest" ) val latest : String? = null ,
171194 @JsonProperty(" is_latest" ) val isLatest : Boolean? = null ,
172195)
173-
174- /* *
175- * Lightweight typed representations for JSON:API resource envelopes and resource data.
176- * These are intentionally simple (attributes are a Map) to support arbitrary resource types.
177- */
178- @JsonIgnoreProperties(ignoreUnknown = true )
179- data class JsonApiResourceData (
180- @JsonProperty(" id" ) val id : String? = null ,
181- @JsonProperty(" type" ) val type : String? = null ,
182- @JsonProperty(" attributes" ) val attributes : Map <String , Any >? = null ,
183- @JsonProperty(" relationships" ) val relationships : Map <String , Any >? = null ,
184- )
185-
186- @JsonIgnoreProperties(ignoreUnknown = true )
187- data class JsonApiEnvelope (
188- @JsonProperty(" data" ) val data : JsonApiResourceData ? = null ,
189- @JsonProperty(" jsonapi" ) val jsonapi : Map <String , Any >? = null ,
190- )
0 commit comments