Skip to content

Commit

Permalink
Refactor separate Kotlin property and Java property (#1040)
Browse files Browse the repository at this point in the history
  • Loading branch information
seongahjo committed Aug 31, 2024
1 parent 5c3deca commit d3831dc
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,17 @@ Fixture Monkey 는 Kotlin 클래스를 생성하기 위한 추가적인 introspe
## PrimaryConstructorArbitraryIntrospector

`PrimaryConstructorArbitraryIntrospector` 는 코틀린 플러그인이 추가되면 자동으로 기본 introspector 로 설정됩니다.
이 introspector 는 주 생성자를 기반으로 Kotlin 클래스를 생성합니다.
이 introspector 는 주 생성자를 기반으로 Kotlin 클래스를 생성합니다.

`PrimaryConstructorArbitraryIntrospector`를 사용하면 `코틀린 생성자의 파라미터` 정보만 생성합니다. `ArbitraryBuilder` API를 사용하면 `코틀린 생성자의 파라미터`만 변경할 수 있습니다.

`pushArbitraryIntrospector` 옵션을 사용해서 `PrimaryConstructorArbitraryIntrospector`를 사용하지 않게 되면 `코틀린 생성자의 파라미터`는 물론 `필드``게터` 정보를 같이 생성합니다.
따라서 부모 클래스의 `필드`, `게터` 정보도 모두 가지고 있습니다. 이때는 `ArbitraryBuilder` API를 사용하면 가지고 있는 정보를 모두 변경 가능합니다.

예를 들어, `KotlinPlugin`을 적용한 후에 `JacksonPlugin`을 적용하면 Jackson으로 코틀린 객체를 생성할 수 있습니다. (순서 의존성이 있습니다.)
Jackson으로 생성하는 경우에는 부모의 필드도 변경이 가능합니다.



**예제 Kotlin 클래스:**
```kotlin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ Fixture Monkey provides some additional introspectors that support the generatio
The `PrimaryConstructorArbitraryIntrospector` becomes the default introspector when the Kotlin plugin is added.
It creates a Kotlin class with its primary constructor.

In the case of using `PrimaryConstructorArbitraryIntrospector`, it only contains the properties of the `Kotlin constructor parameter`.

If you use your own `ArbitraryIntrospector` instead of `PrimaryConstructorArbitraryIntrospector`, it will contain the properties of the `Kotlin constructor parameter`, `Field`, `Getter`. So it contains the properties of the parent `Field` and `Getter`.
You can customize the all properties by `ArbitraryBuilder` APIs.

For example, if you apply the `JacksonPlugin` after applying the `KotlinPlugin`, you can generate an instance of the Kotlin type by Jackson. In this case, you can customize the parent fields.

**Example Kotlin Class :**
```kotlin
data class Product (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Fixture Monkey
*
* Copyright (c) 2021-present NAVER Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.navercorp.fixturemonkey.api.generator;

import java.util.List;

import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;

import com.navercorp.fixturemonkey.api.matcher.MatcherOperator;
import com.navercorp.fixturemonkey.api.property.CompositeProperty;
import com.navercorp.fixturemonkey.api.property.CompositePropertyGenerator;
import com.navercorp.fixturemonkey.api.property.Property;
import com.navercorp.fixturemonkey.api.property.PropertyGenerator;

/**
* Generates the properties by a matched {@link PropertyGenerator}.
* <p>
* It is different from the {@link CompositePropertyGenerator}, which uses all the {@link PropertyGenerator}s
* and combines all the generated properties as a {@link CompositeProperty}.
* <p>
* It only uses a matching {@link PropertyGenerator}, not throws Exception if no matched {@link PropertyGenerator}.
*/
@API(since = "1.1.0", status = Status.EXPERIMENTAL)
public final class MatchPropertyGenerator implements PropertyGenerator {
private final List<MatcherOperator<PropertyGenerator>> propertyGenerators;

public MatchPropertyGenerator(List<MatcherOperator<PropertyGenerator>> propertyGenerators) {
this.propertyGenerators = propertyGenerators;
}

@Override
public List<Property> generateChildProperties(Property property) {
for (MatcherOperator<PropertyGenerator> propertyGenerator : propertyGenerators) {
if (propertyGenerator.getMatcher().match(property)) {
return propertyGenerator.getOperator().generateChildProperties(property);
}
}

throw new IllegalArgumentException("Type " + property.getType() + " has no matching PropertyGenerator.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
package com.navercorp.fixturemonkey.kotlin

import com.navercorp.fixturemonkey.api.generator.FunctionalInterfaceContainerPropertyGenerator
import com.navercorp.fixturemonkey.api.generator.MatchPropertyGenerator
import com.navercorp.fixturemonkey.api.introspector.FunctionalInterfaceArbitraryIntrospector
import com.navercorp.fixturemonkey.api.introspector.MatchArbitraryIntrospector
import com.navercorp.fixturemonkey.api.matcher.MatcherOperator
import com.navercorp.fixturemonkey.api.option.FixtureMonkeyOptionsBuilder
import com.navercorp.fixturemonkey.api.plugin.Plugin
import com.navercorp.fixturemonkey.api.property.CandidateConcretePropertyResolver
import com.navercorp.fixturemonkey.api.property.ConcreteTypeCandidateConcretePropertyResolver
import com.navercorp.fixturemonkey.api.property.DefaultPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.generator.InterfaceKFunctionPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.generator.PairContainerPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.generator.PairDecomposedContainerValueFactory
Expand All @@ -43,6 +45,7 @@ import com.navercorp.fixturemonkey.kotlin.property.KotlinPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.type.actualType
import com.navercorp.fixturemonkey.kotlin.type.cachedKotlin
import com.navercorp.fixturemonkey.kotlin.type.isKotlinLambda
import com.navercorp.fixturemonkey.kotlin.type.isKotlinType
import org.apiguardian.api.API
import org.apiguardian.api.API.Status.MAINTAINED
import java.lang.reflect.Modifier
Expand All @@ -58,7 +61,17 @@ class KotlinPlugin : Plugin {
)
)
}
.defaultPropertyGenerator(KotlinPropertyGenerator())
.defaultPropertyGenerator(
MatchPropertyGenerator(
listOf(
MatcherOperator(
{ property -> property.type.actualType().isKotlinType() },
KotlinPropertyGenerator()
),
MatcherOperator({ true }, DefaultPropertyGenerator())
)
)
)
.insertFirstArbitraryContainerPropertyGenerator(
{ property -> property.type.actualType().cachedKotlin().isKotlinLambda() }
) { FunctionalInterfaceContainerPropertyGenerator.INSTANCE.generate(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,13 @@ import com.navercorp.fixturemonkey.api.type.TypeReference
import com.navercorp.fixturemonkey.api.type.Types
import com.navercorp.fixturemonkey.kotlin.introspector.CompanionObjectFactoryMethodIntrospector
import com.navercorp.fixturemonkey.kotlin.introspector.KotlinPropertyArbitraryIntrospector
import com.navercorp.fixturemonkey.kotlin.property.KotlinConstructorParameterProperty
import com.navercorp.fixturemonkey.kotlin.property.KotlinPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.type.actualType
import com.navercorp.fixturemonkey.kotlin.type.cachedKotlin
import com.navercorp.fixturemonkey.kotlin.type.cachedMemberFunctions
import com.navercorp.fixturemonkey.kotlin.type.declaredConstructor
import com.navercorp.fixturemonkey.kotlin.type.toTypeReference
import java.lang.reflect.AnnotatedType
import java.lang.reflect.Type
import java.util.Optional
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty
Expand Down Expand Up @@ -242,32 +240,6 @@ class KotlinInstantiatorProcessor :
inputParameterTypes.isEmpty() || Types.isAssignableTypes(it.toTypedArray(), inputParameterTypes)
}

internal data class KotlinConstructorParameterProperty(
private val annotatedType: AnnotatedType,
val kParameter: KParameter,
private val parameterName: String,
private val constructor: KFunction<*>,
) : Property {
override fun getType(): Type = annotatedType.type

override fun getAnnotatedType(): AnnotatedType = annotatedType

override fun getName(): String = parameterName

override fun getAnnotations(): List<Annotation> = kParameter.annotations

override fun getValue(obj: Any?): Any? {
throw UnsupportedOperationException("Interface method should not be called.")
}

@Suppress("UNCHECKED_CAST")
override fun <T : Annotation> getAnnotation(annotationClass: Class<T>): Optional<T> = annotations
.find { it.annotationClass.java == annotationClass }
.let { Optional.of(it as T) }

override fun isNullable(): Boolean = kParameter.type.isMarkedNullable
}

internal class KotlinConstructorArbitraryIntrospector(private val kotlinConstructor: KFunction<*>) :
ArbitraryIntrospector {
override fun introspect(context: ArbitraryGeneratorContext): ArbitraryIntrospectorResult =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import com.navercorp.fixturemonkey.api.generator.ArbitraryGeneratorContext
import com.navercorp.fixturemonkey.api.introspector.ArbitraryIntrospector
import com.navercorp.fixturemonkey.api.introspector.ArbitraryIntrospectorResult
import com.navercorp.fixturemonkey.api.introspector.BeanArbitraryIntrospector
import com.navercorp.fixturemonkey.api.property.Property
import com.navercorp.fixturemonkey.api.property.PropertyGenerator
import com.navercorp.fixturemonkey.kotlin.property.KotlinPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.type.actualType
import com.navercorp.fixturemonkey.kotlin.type.isKotlinType
import org.slf4j.LoggerFactory
Expand All @@ -44,7 +47,10 @@ class KotlinAndJavaCompositeArbitraryIntrospector(
}
}

override fun getRequiredPropertyGenerator(property: Property?): PropertyGenerator = PROPERTY_GENERATOR

companion object {
private val LOGGER = LoggerFactory.getLogger(KotlinAndJavaCompositeArbitraryIntrospector::class.java)
private val PROPERTY_GENERATOR = KotlinPropertyGenerator()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,42 +24,59 @@ import com.navercorp.fixturemonkey.api.introspector.ArbitraryIntrospector
import com.navercorp.fixturemonkey.api.introspector.ArbitraryIntrospectorResult
import com.navercorp.fixturemonkey.api.matcher.Matcher
import com.navercorp.fixturemonkey.api.property.Property
import com.navercorp.fixturemonkey.api.property.PropertyGenerator
import com.navercorp.fixturemonkey.kotlin.matcher.Matchers.DURATION_TYPE_MATCHER
import com.navercorp.fixturemonkey.kotlin.property.KotlinConstructorParameterPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.type.kotlinPrimaryConstructor
import org.apiguardian.api.API
import kotlin.reflect.full.primaryConstructor
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.DurationUnit.NANOSECONDS
import kotlin.time.toDuration

private const val STORAGE_UNIT = "storageUnit"
private const val IN_WHOLE_NANOSECONDS = "inWholeNanoseconds"
private const val IN_WHOLE_MILLISECONDS = "inWholeMilliseconds"

@API(since = "1.0.15", status = API.Status.EXPERIMENTAL)
class KotlinDurationIntrospector : ArbitraryIntrospector, Matcher {
override fun match(property: Property) = DURATION_TYPE_MATCHER.match(property)

override fun introspect(context: ArbitraryGeneratorContext): ArbitraryIntrospectorResult {
val kClass = Duration::class
val primaryConstructor = kClass.primaryConstructor!!

require(primaryConstructor.parameters.size == 1) { "Duration class must have only one parameter" }
val rawValueArbitraryByArbitraryProperty = context.combinableArbitrariesByArbitraryProperty
.mapValues { entry -> entry.value.filter(::isValidDurationRawValue) }

return ArbitraryIntrospectorResult(
CombinableArbitrary.objectBuilder()
.properties(context.combinableArbitrariesByArbitraryProperty)
.properties(rawValueArbitraryByArbitraryProperty)
.build {
val arbitrariesByPropertyName = it.mapKeys { map -> map.key.objectProperty.property.name }
val durationUnit = arbitrariesByPropertyName[STORAGE_UNIT] as DurationUnit

val value = when (durationUnit) {
NANOSECONDS -> arbitrariesByPropertyName[IN_WHOLE_NANOSECONDS] as Long
else -> arbitrariesByPropertyName[IN_WHOLE_MILLISECONDS] as Long
}
val rawValue = it.values.first() as Long

value.toDuration(durationUnit)
PRIMARY_CONSTRUCTOR.callBy(mapOf(RAW_VALUE_PARAMETER to rawValue))
}
)
}

private fun isValidDurationRawValue(it: Any?): Boolean {
val rawValue = it as Long
val value = rawValue shr 1
val isInNanos = (rawValue.toInt() and 1) == 0

return if (isInNanos) {
value in -MAX_NANOS..MAX_NANOS
} else {
(value in -MAX_MILLIS..MAX_MILLIS) &&
(value !in -MAX_NANOS_IN_MILLIS..MAX_NANOS_IN_MILLIS)
}
}

override fun getRequiredPropertyGenerator(property: Property): PropertyGenerator = PROPERTY_GENERATOR

companion object {
internal val PRIMARY_CONSTRUCTOR = Duration::class.java.kotlinPrimaryConstructor()
internal val PROPERTY_GENERATOR = KotlinConstructorParameterPropertyGenerator({ PRIMARY_CONSTRUCTOR })
internal val RAW_VALUE_PARAMETER =
PRIMARY_CONSTRUCTOR.parameters.first { parameter -> parameter.name == "rawValue" }
private const val NANOS_IN_MILLIS = 1_000_000
private const val MAX_NANOS = Long.MAX_VALUE / 2 / NANOS_IN_MILLIS * NANOS_IN_MILLIS - 1 // ends in ..._999_999

// maximum number duration can store in millisecond range, also encodes an infinite value
private const val MAX_MILLIS = Long.MAX_VALUE / 2

// MAX_NANOS expressed in milliseconds
private const val MAX_NANOS_IN_MILLIS = MAX_NANOS / NANOS_IN_MILLIS
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,31 @@
package com.navercorp.fixturemonkey.kotlin.introspector

import com.navercorp.fixturemonkey.api.arbitrary.CombinableArbitrary
import com.navercorp.fixturemonkey.api.container.ConcurrentLruCache
import com.navercorp.fixturemonkey.api.generator.ArbitraryGeneratorContext
import com.navercorp.fixturemonkey.api.introspector.ArbitraryIntrospector
import com.navercorp.fixturemonkey.api.introspector.ArbitraryIntrospectorResult
import com.navercorp.fixturemonkey.api.matcher.Matcher
import com.navercorp.fixturemonkey.api.property.Property
import com.navercorp.fixturemonkey.api.property.PropertyGenerator
import com.navercorp.fixturemonkey.api.type.Types
import com.navercorp.fixturemonkey.kotlin.property.KotlinPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.property.KotlinConstructorParameterPropertyGenerator
import com.navercorp.fixturemonkey.kotlin.type.actualType
import com.navercorp.fixturemonkey.kotlin.type.cachedKotlin
import com.navercorp.fixturemonkey.kotlin.type.isKotlinLambda
import com.navercorp.fixturemonkey.kotlin.type.isKotlinType
import com.navercorp.fixturemonkey.kotlin.type.kotlinPrimaryConstructor
import org.apiguardian.api.API
import org.apiguardian.api.API.Status.MAINTAINED
import org.slf4j.LoggerFactory
import java.lang.reflect.Modifier
import kotlin.jvm.internal.Reflection
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible

@API(since = "0.4.0", status = MAINTAINED)
class PrimaryConstructorArbitraryIntrospector : ArbitraryIntrospector, Matcher {
override fun match(property: Property): Boolean = property.type.actualType().isKotlinType()
override fun match(property: Property): Boolean =
property.type.actualType().isKotlinType() &&
!property.type.actualType().cachedKotlin().isKotlinLambda() &&
property.type.actualType().cachedKotlin() != Unit::class

override fun introspect(context: ArbitraryGeneratorContext): ArbitraryIntrospectorResult {
val type = Types.getActualType(context.resolvedType)
Expand All @@ -52,10 +52,7 @@ class PrimaryConstructorArbitraryIntrospector : ArbitraryIntrospector, Matcher {
}

val constructor = try {
CONSTRUCTOR_CACHE.computeIfAbsent(type) {
val kotlinClass = Reflection.createKotlinClass(type) as KClass<*>
requireNotNull(kotlinClass.primaryConstructor) { "No kotlin primary constructor provided for $kotlinClass" }
}.apply { isAccessible = true }
type.kotlinPrimaryConstructor()
} catch (ex: Exception) {
LOGGER.warn("Given type $type is failed to generated due to the exception. It may be null.", ex)
return ArbitraryIntrospectorResult.NOT_INTROSPECTED
Expand All @@ -81,12 +78,13 @@ class PrimaryConstructorArbitraryIntrospector : ArbitraryIntrospector, Matcher {
)
}

override fun getRequiredPropertyGenerator(property: Property): PropertyGenerator = KOTLIN_PROPERTY_GENERATOR
override fun getRequiredPropertyGenerator(p: Property): PropertyGenerator = PROPERTY_GENERATOR

companion object {
val INSTANCE = PrimaryConstructorArbitraryIntrospector()
private val LOGGER = LoggerFactory.getLogger(PrimaryConstructorArbitraryIntrospector::class.java)
private val KOTLIN_PROPERTY_GENERATOR = KotlinPropertyGenerator()
private val CONSTRUCTOR_CACHE = ConcurrentLruCache<Class<*>, KFunction<*>>(2048)
internal val PROPERTY_GENERATOR = KotlinConstructorParameterPropertyGenerator({ property ->
property.type.actualType().kotlinPrimaryConstructor()
})
}
}
Loading

0 comments on commit d3831dc

Please sign in to comment.