@@ -7,20 +7,30 @@ import dev.appoutlet.some.config.StringStrategy
77import dev.appoutlet.some.core.FixtureContext
88import dev.appoutlet.some.core.ResolverChain
99import dev.appoutlet.some.core.TypeResolver
10+ import dev.appoutlet.some.exception.SomeCircularReferenceException
11+ import dev.appoutlet.some.exception.SomeInstantiationException
12+ import java.lang.reflect.Modifier
1013import kotlin.random.Random
1114import kotlin.reflect.KClass
15+ import kotlin.reflect.KFunction
16+ import kotlin.reflect.KParameter
1217import kotlin.reflect.KType
1318import kotlin.reflect.full.createType
1419import kotlin.reflect.full.isSubclassOf
15- import kotlin.reflect.full.primaryConstructor
20+ import kotlin.reflect.jvm.isAccessible
1621
1722/* *
18- * Resolves constructable Kotlin classes by calling their primary constructor with generated arguments.
23+ * Resolves constructable Kotlin classes by calling their constructors with generated arguments.
1924 *
2025 * This resolver is the fallback for user-defined model types. A type is supported when its classifier is a
21- * [KClass] with a primary constructor and is not one of the built-in scalar or collection types handled by earlier
22- * resolvers in the chain. Although the name refers to data classes, any class with a primary constructor can be
23- * resolved when it reaches this resolver.
26+ * [KClass] with at least one constructor (primary or secondary), is not abstract, and is not one of the
27+ * built-in scalar or collection types handled by earlier resolvers in the chain.
28+ *
29+ * Constructors are tried in declaration order. The first constructor whose arguments can be fully resolved
30+ * wins. If all constructors fail, a [SomeInstantiationException] is thrown summarizing each failure.
31+ *
32+ * [SomeCircularReferenceException] is never caught by this resolver; it always propagates so callers receive
33+ * accurate circular-reference diagnostics.
2434 *
2535 * Constructor parameters are resolved as follows:
2636 * - If a property-specific factory exists for the parameter name, the factory value is used.
@@ -37,7 +47,7 @@ import kotlin.reflect.full.primaryConstructor
3747 * @param collectionStrategy Collection sizing strategy exposed to property factories through [FixtureContext].
3848 * @param defaultValueStrategy Strategy for handling constructor defaults.
3949 */
40- class DataClassResolver (
50+ class ClassResolver (
4151 private val propertyFactories : Map <Pair <KClass <* >, String >, FixtureContext .() -> Any? > = emptyMap(),
4252 private val random : Random = Random .Default ,
4353 private val nullableStrategy : NullableStrategy = NullableStrategy .NullOnCircularReference ,
@@ -48,9 +58,9 @@ class DataClassResolver(
4858 /* *
4959 * Returns whether [type] can be instantiated by this resolver.
5060 *
51- * The resolver requires a [KClass] classifier with a primary constructor. It deliberately rejects collection,
52- * string, numeric, boolean, and character types because those are handled by more specific resolvers with
53- * generation rules tailored to each type.
61+ * The resolver requires a [KClass] classifier with at least one constructor that is not abstract.
62+ * It deliberately rejects collection, string, numeric, boolean, and character types because those are
63+ * handled by more specific resolvers with generation rules tailored to each type.
5464 *
5565 * @param type Type being checked by the resolver chain.
5666 * @return `true` when [type] is a constructable class that should be handled as a model object.
@@ -59,9 +69,13 @@ class DataClassResolver(
5969 val kClass = type.classifier as ? KClass <* > ? : return false
6070
6171 return when {
62- kClass.primaryConstructor == null -> false
72+ kClass.constructors.isEmpty() -> false
73+
74+ Modifier .isAbstract(kClass.java.modifiers) -> false
6375
64- kClass.isSubclassOf(List ::class ) || kClass.isSubclassOf(Set ::class ) || kClass.isSubclassOf(Map ::class ) -> {
76+ kClass.isSubclassOf(List ::class ) || kClass.isSubclassOf(Set ::class ) || kClass.isSubclassOf(
77+ Map ::class
78+ ) -> {
6579 false
6680 }
6781
@@ -78,25 +92,84 @@ class DataClassResolver(
7892 }
7993
8094 /* *
81- * Creates an instance of [type] by resolving values for its primary constructor parameters .
95+ * Creates an instance of [type] by trying each constructor in order .
8296 *
8397 * Property factories take precedence over generated values and receive a [FixtureContext] containing the current
8498 * random source, resolution stack, and configured generation strategies. Required parameters without custom
8599 * property factories are delegated back to [chain], while optional parameters are left out of the argument map so
86100 * their Kotlin default values are preserved.
87101 *
102+ * If a constructor succeeds, its result is returned immediately. If all constructors fail, a
103+ * [SomeInstantiationException] is thrown with details about each failure.
104+ *
105+ * [SomeCircularReferenceException] is re-thrown immediately and is never absorbed into the failure summary,
106+ * ensuring that circular-reference diagnostics propagate to the caller.
107+ *
88108 * @param type Class type to instantiate.
89109 * @param chain Resolver chain used to generate required constructor parameter values.
90110 * @return A new instance of [type].
91- * @throws IllegalStateException when [type] does not expose a primary constructor .
111+ * @throws SomeInstantiationException when all constructors fail to instantiate [type] .
92112 */
113+ @Suppress(" TooGenericExceptionCaught" )
93114 override fun resolve (type : KType , chain : ResolverChain ): Any {
94115 val kClass = type.classifier as KClass <* >
95- val constructor = kClass.primaryConstructor
96- ? : error(" No primary constructor found for ${kClass.simpleName} " )
116+ val failures = mutableListOf<String >()
117+
118+ for (constructor in kClass.constructors) {
119+ try {
120+ constructor .isAccessible = true
121+ val result = tryConstructor(constructor , kClass, type, chain)
122+ return requireNotNull(result) { " Constructor returned null" }
123+ } catch (e: SomeCircularReferenceException ) {
124+ throw e
125+ } catch (e: Exception ) {
126+ failures.add(
127+ " Constructor ${constructor .parameters.map { it.name }} : ${e.message ? : e::class .simpleName} "
128+ )
129+ }
130+ }
131+
132+ throw SomeInstantiationException (kClass, failures)
133+ }
97134
135+ private fun tryConstructor (
136+ constructor : KFunction <* >,
137+ kClass : KClass <* >,
138+ type : KType ,
139+ chain : ResolverChain ,
140+ ): Any? {
141+ val args = constructor .parameters.mapNotNull { param ->
142+ val propertyFactory = propertyFactories[kClass to param.name]
143+ val shouldGenerate = ! param.isOptional ||
144+ defaultValueStrategy == DefaultValueStrategy .Generate
145+
146+ when {
147+ propertyFactory != null -> resolveByPropertyFactory(chain, param, propertyFactory)
148+ shouldGenerate -> resolveByResolvers(kClass, type, param, chain)
149+ else -> null
150+ }
151+ }.toMap()
152+
153+ return constructor .callBy(args)
154+ }
155+
156+ private fun resolveByResolvers (
157+ kClass : KClass <* >,
158+ type : KType ,
159+ param : KParameter ,
160+ chain : ResolverChain
161+ ): Pair <KParameter , Any ?> {
98162 val typeArgMap = buildTypeArgMap(kClass, type)
163+ val paramType = param.type
164+ val resolvedType = typeArgMap[paramType.toString()] ? : paramType
165+ return param to chain.resolve(resolvedType)
166+ }
99167
168+ private fun resolveByPropertyFactory (
169+ chain : ResolverChain ,
170+ param : KParameter ,
171+ propertyFactory : FixtureContext .() -> Any?
172+ ): Pair <KParameter , Any ?> {
100173 val context = FixtureContext (
101174 random = random,
102175 resolutionStack = chain.stack,
@@ -106,23 +179,7 @@ class DataClassResolver(
106179 defaultValueStrategy = defaultValueStrategy,
107180 )
108181
109- val args = constructor .parameters
110- .mapNotNull { param ->
111- val propertyFactory = propertyFactories[kClass to param.name]
112- val shouldGenerate = ! param.isOptional || defaultValueStrategy == DefaultValueStrategy .Generate
113-
114- if (propertyFactory != null ) {
115- param to propertyFactory(context)
116- } else if (shouldGenerate) {
117- val paramType = param.type
118- val resolvedType = typeArgMap[paramType.toString()] ? : paramType
119- param to chain.resolve(resolvedType)
120- } else {
121- null
122- }
123- }.toMap()
124-
125- return constructor .callBy(args)
182+ return param to propertyFactory(context)
126183 }
127184
128185 /* *
0 commit comments