Skip to content

Commit

Permalink
Easier configuration of strategies for decorators (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattmook authored Oct 24, 2019
1 parent f8cbfc1 commit 5c21139
Show file tree
Hide file tree
Showing 24 changed files with 452 additions and 70 deletions.
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ val futureDate = fixture<Date> {
}
```

### property
#### property

Used to override constructor parameters or mutable properties when
generating instances of generic classes.
Expand Down Expand Up @@ -216,6 +216,55 @@ val aStaticValue = fixture<Int>() {
}
```

### recursionStrategy

When recursion is detected the library will, by default, throw a
`FixtureException` with the details of the circular reference. This strategy
can be changed to instead return `null` for the reference, however, if this
results in an invalid object an exception will still be thrown as the object
requested couldn't be resolved.

```kotlin
val fixture = kotlinFixture {
recursionStrategy(NullRecursionStrategy)
}

// You can also override at instance creation

fixture<AnObject> {
recursionStrategy(NullRecursionStrategy)
}
```

It is also possible to define and implement your own recursion strategy by
implementing `RecursionStrategy` and applying it as above.

### loggingStrategy

A basic logger can be applied using the built in `SysOutLoggingStrategy`. It is
also possible to define and implement your own logging strategy by implementing
`LoggingStrategy` and applying it as below.

```kotlin
val fixture = kotlinFixture {
loggingStrategy(SysOutLoggingStrategy)
}

fixture<String>() {
// You can also override at instance creation
loggingStrategy(SysOutLoggingStrategy)
}
```

This outputs:

```text
ktype kotlin.String →
class kotlin.String →
Success(5878ec34-c30f-40c7-ad52-c15a39b44ac1)
Success(5878ec34-c30f-40c7-ad52-c15a39b44ac1)
```

## Contributing

Please fork this repository and contribute back using
Expand Down
2 changes: 2 additions & 0 deletions fixture/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ tasks.whenTaskAdded {
from(sourceSets.main.get().allSource)
}
}

tasks.getByName("check").finalizedBy(rootProject.tasks.getByName("detekt"))
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ interface Context {

fun resolve(obj: Any) = resolver.resolve(this, obj)
}

inline fun <reified T> Context.strategyOrDefault(default: T): T = configuration.strategies[T::class] as? T ?: default
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@
* limitations under the License.
*/

@file:Suppress("MatchingDeclarationName")

package com.appmattus.kotlinfixture

import com.appmattus.kotlinfixture.config.Configuration
import com.appmattus.kotlinfixture.config.ConfigurationBuilder
import com.appmattus.kotlinfixture.decorator.logging.LoggingDecorator
import com.appmattus.kotlinfixture.decorator.logging.SysOutLoggingStrategy
import com.appmattus.kotlinfixture.decorator.recursion.NullRecursionStrategy
import com.appmattus.kotlinfixture.decorator.recursion.RecursionDecorator
import kotlin.reflect.KType

class Fixture(val fixtureConfiguration: Configuration) {
Expand Down Expand Up @@ -50,34 +48,3 @@ class Fixture(val fixtureConfiguration: Configuration) {

fun kotlinFixture(init: ConfigurationBuilder.() -> Unit = {}) =
Fixture(ConfigurationBuilder().apply(init).build())

fun main() {

val fixture = kotlinFixture()

fixture<List<String>> {
decorators.add(LoggingDecorator(SysOutLoggingStrategy()))
}

println(fixture<A> {
decorators.removeIf { it is RecursionDecorator }
decorators.add(RecursionDecorator(NullRecursionStrategy()))
decorators.add(LoggingDecorator(SysOutLoggingStrategy()))
})
}

class A {
lateinit var b: B

override fun toString(): String {
return "A[${if (::b.isInitialized) b.toString() else "uninit"}]"
}
}

class B {
lateinit var a: A

override fun toString(): String {
return "B[${if (::a.isInitialized) a.toString() else "uninit"}]"
}
}
7 changes: 0 additions & 7 deletions fixture/src/main/kotlin/com/appmattus/kotlinfixture/ToDo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@ package com.appmattus.kotlinfixture

@Suppress("unused")
private object ToDo {
// TODO SynchronousQueue - adding elements to this makes little sense
// TODO ArrayBlockingQueue - needs a fixed capacity

// TODO handle & detect circular dependencies. See circularDependencyBehaviour

// TODO handle unresolvable, throw exception or return null. See noResolutionBehaviour

// TODO Check out competition projects
// - https://github.com/FlexTradeUKLtd/kfixture
// - https://github.com/marcellogalhardo/kotlin-fixture/tree/develop
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
package com.appmattus.kotlinfixture.config

import com.appmattus.kotlinfixture.decorator.Decorator
import com.appmattus.kotlinfixture.decorator.logging.LoggingDecorator
import com.appmattus.kotlinfixture.decorator.recursion.RecursionDecorator
import com.appmattus.kotlinfixture.decorator.recursion.ThrowingRecursionStrategy
import com.appmattus.kotlinfixture.resolver.AbstractClassResolver
import com.appmattus.kotlinfixture.resolver.ArrayResolver
import com.appmattus.kotlinfixture.resolver.BigDecimalResolver
Expand Down Expand Up @@ -62,15 +62,16 @@ data class Configuration(
val subTypes: Map<KClass<*>, KClass<*>> = emptyMap<KClass<*>, KClass<*>>().toUnmodifiableMap(),
val random: Random = defaultRandom,
val decorators: List<Decorator> = defaultDecorators.toUnmodifiableList(),
val resolvers: List<Resolver> = defaultResolvers.toUnmodifiableList()
val resolvers: List<Resolver> = defaultResolvers.toUnmodifiableList(),
val strategies: Map<KClass<*>, Any> = emptyMap<KClass<*>, Any>().toUnmodifiableMap()
) {

private companion object {
private val defaultRepeatCount: () -> Int = { 5 }

private val defaultRandom = Random

private val defaultDecorators = listOf(RecursionDecorator(ThrowingRecursionStrategy()))
private val defaultDecorators = listOf(RecursionDecorator(), LoggingDecorator())

private val defaultResolvers = listOf(
CharResolver(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class ConfigurationBuilder(configuration: Configuration = Configuration()) {
private val instances: MutableMap<KType, Generator<Any?>.() -> Any?> = configuration.instances.toMutableMap()
private val subTypes: MutableMap<KClass<*>, KClass<*>> = configuration.subTypes.toMutableMap()

internal val strategies: MutableMap<KClass<*>, Any> = configuration.strategies.toMutableMap()

@Suppress("UNCHECKED_CAST")
inline fun <reified T> instance(noinline generator: Generator<T>.() -> T) =
instance(typeOf<T>(), generator as Generator<Any?>.() -> Any?)
Expand Down Expand Up @@ -94,6 +96,7 @@ class ConfigurationBuilder(configuration: Configuration = Configuration()) {
subTypes = subTypes.toUnmodifiableMap(),
random = random,
decorators = decorators.toUnmodifiableList(),
resolvers = resolvers.toUnmodifiableList()
resolvers = resolvers.toUnmodifiableList(),
strategies = strategies.toUnmodifiableMap()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.appmattus.kotlinfixture.decorator.logging

import com.appmattus.kotlinfixture.config.ConfigurationBuilder

@Suppress("unused")
fun ConfigurationBuilder.loggingStrategy(strategy: LoggingStrategy) {
strategies[LoggingStrategy::class] = strategy
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,19 @@ package com.appmattus.kotlinfixture.decorator.logging
import com.appmattus.kotlinfixture.Context
import com.appmattus.kotlinfixture.decorator.Decorator
import com.appmattus.kotlinfixture.resolver.Resolver
import com.appmattus.kotlinfixture.strategyOrDefault

class LoggingDecorator(private val strategy: LoggingStrategy) : Decorator {
internal class LoggingDecorator : Decorator {

override fun decorate(resolver: Resolver): Resolver = LoggingResolver(resolver, strategy)
override fun decorate(resolver: Resolver): Resolver = LoggingResolver(resolver)

private class LoggingResolver(
private val resolver: Resolver,
private val strategy: LoggingStrategy
private val resolver: Resolver
) : Resolver {

override fun resolve(context: Context, obj: Any): Any? {
val strategy = context.strategyOrDefault<LoggingStrategy>(NoLoggingStrategy)

strategy.request(obj)

try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2019 Appmattus Limited
*
* 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.appmattus.kotlinfixture.decorator.logging

object NoLoggingStrategy : LoggingStrategy {
override fun request(obj: Any) = Unit

override fun response(obj: Any, result: Result<Any?>) = Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@
package com.appmattus.kotlinfixture.decorator.logging

import java.util.Stack
import kotlin.reflect.KType

class SysOutLoggingStrategy : LoggingStrategy {
object SysOutLoggingStrategy : LoggingStrategy {
private val stack = Stack<Any>()

override fun request(obj: Any) {
stack.push(obj)

val indent = " ".repeat(stack.size - 1)
println("$indent$obj")

val prefix = when (obj) {
is KType -> "ktype "
else -> ""
}
println("$indent$prefix$obj")
}

override fun response(obj: Any, result: Result<Any?>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ package com.appmattus.kotlinfixture.decorator.recursion

import kotlin.reflect.KType

class NullRecursionStrategy : RecursionStrategy {
object NullRecursionStrategy : RecursionStrategy {

override fun handleRecursion(type: KType, stack: Collection<KType>) = null
override fun handleRecursion(type: KType, stack: Collection<KType>): Any? {
check(stack.isNotEmpty()) { "Stack must be populated" }

return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.appmattus.kotlinfixture.decorator.recursion

import com.appmattus.kotlinfixture.config.ConfigurationBuilder

@Suppress("unused")
fun ConfigurationBuilder.recursionStrategy(strategy: RecursionStrategy) {
strategies[RecursionStrategy::class] = strategy
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,25 @@ package com.appmattus.kotlinfixture.decorator.recursion
import com.appmattus.kotlinfixture.Context
import com.appmattus.kotlinfixture.decorator.Decorator
import com.appmattus.kotlinfixture.resolver.Resolver
import com.appmattus.kotlinfixture.strategyOrDefault
import java.util.Stack
import kotlin.reflect.KType

class RecursionDecorator(
private val strategy: RecursionStrategy
) : Decorator {
internal class RecursionDecorator : Decorator {

override fun decorate(resolver: Resolver): Resolver = RecursionResolver(resolver, strategy)
override fun decorate(resolver: Resolver): Resolver = RecursionResolver(resolver)

private class RecursionResolver(
private val resolver: Resolver,
private val recursionStrategy: RecursionStrategy
) : Resolver {
private class RecursionResolver(private val resolver: Resolver) : Resolver {

private val stack = Stack<KType>()

@Suppress("ReturnCount")
override fun resolve(context: Context, obj: Any): Any? {
val strategy = context.strategyOrDefault<RecursionStrategy>(ThrowingRecursionStrategy)

if (obj is KType) {
if (stack.contains(obj)) {
return recursionStrategy.handleRecursion(obj, stack)
return strategy.handleRecursion(obj, stack.toList())
}

stack.push(obj)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ package com.appmattus.kotlinfixture.decorator.recursion
import com.appmattus.kotlinfixture.FixtureException
import kotlin.reflect.KType

class ThrowingRecursionStrategy : RecursionStrategy {
object ThrowingRecursionStrategy : RecursionStrategy {

override fun handleRecursion(type: KType, stack: Collection<KType>): Any? {
check(stack.isNotEmpty()) { "Stack must be populated" }

val errorMessage = "Unable to create ${stack.first()} with circular reference: ${stack.toStackString(type)}"
throw FixtureException(errorMessage)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class FixtureTest {

val missingResolvers = Classes.classGraph.getClassInfo(Resolver::class.java.name).classesImplementing
.map { it.simpleName }
.filterNot { it == "TestResolver" || it == "CompositeResolver" }
.filterNot { it.endsWith("TestResolver") || it == "CompositeResolver" }
.sorted()
.toMutableList().apply {
removeAll(actualResolvers)
Expand Down
Loading

0 comments on commit 5c21139

Please sign in to comment.