Skip to content

Commit

Permalink
Add ability to override the optional strategy used (#35)
Browse files Browse the repository at this point in the history
Fixes #33
  • Loading branch information
mattmook authored Nov 29, 2019
1 parent 91ba644 commit 716acf4
Show file tree
Hide file tree
Showing 16 changed files with 703 additions and 4 deletions.
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,21 +226,48 @@ nullability strategy.

```kotlin
val fixture = kotlinFixture {
// All optionals will be populated with a value
// All nullable types will be populated with a value
nullabilityStrategy(NeverNullStrategy)
}

// You can also override at instance creation

fixture<AnObject> {
// All optionals will be populated with null
// All nullable types will be populated with null
nullabilityStrategy(AlwaysNullStrategy)
}
```

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

#### optionalStrategy

By default when the library comes across an optional type, such as
`value: String = "default"` it will randomly return that default value
or a generated value. This can be overridden by setting an optional
strategy.

```kotlin
val fixture = kotlinFixture {
// All optionals will be populated with generated values
optionalStrategy(NeverOptionalStrategy)
}

// You can also override at instance creation

fixture<AnObject> {
// All optionals will be populated with their default value
optionalStrategy(AlwaysOptionalStrategy) {
// You can override the strategy for a particular class
classOverride<AnotherObject>(NeverOptionalStrategy)

// You can override the strategy for a property of a class
propertyOverride(AnotherObject:property, RandomlyOptionalStrategy)
}
}
```

#### recursionStrategy

When recursion is detected the library will, by default, throw a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.KType

@ConfigurationDsl
class ConfigurationBuilder(configuration: Configuration = Configuration()) {

var decorators: MutableList<Decorator> = configuration.decorators.toMutableList()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.config

@DslMarker
annotation class ConfigurationDsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.optional

import com.appmattus.kotlinfixture.Context
import kotlin.reflect.KClass

object AlwaysOptionalStrategy : OptionalStrategy {

override fun Context.generateAsOptional(callingClass: KClass<*>, parameterName: String) = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.optional

import com.appmattus.kotlinfixture.Context
import kotlin.reflect.KClass

object NeverOptionalStrategy : OptionalStrategy {

override fun Context.generateAsOptional(callingClass: KClass<*>, parameterName: String) = false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.optional

import com.appmattus.kotlinfixture.config.ConfigurationBuilder

@Suppress("unused")
fun ConfigurationBuilder.optionalStrategy(
defaultStrategy: OptionalStrategy = RandomlyOptionalStrategy,
init: OptionalStrategyBuilder.() -> Unit = {}
) {
strategies[OptionalStrategy::class] = OptionalStrategyBuilder(defaultStrategy).apply(init).build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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.optional

import com.appmattus.kotlinfixture.Context
import kotlin.reflect.KClass

interface OptionalStrategy {
fun Context.generateAsOptional(callingClass: KClass<*>, parameterName: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.optional

import com.appmattus.kotlinfixture.config.ConfigurationDsl
import com.appmattus.kotlinfixture.toUnmodifiableMap
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1

@ConfigurationDsl
class OptionalStrategyBuilder(private val defaultStrategy: OptionalStrategy) {

private val classes: MutableMap<KClass<*>, OptionalStrategy> = mutableMapOf()

private val properties: MutableMap<KClass<*>, MutableMap<String, OptionalStrategy>> = mutableMapOf()

inline fun <reified T> classOverride(strategy: OptionalStrategy) = classOverride(T::class, strategy)

fun classOverride(superType: KClass<*>, strategy: OptionalStrategy) {
classes[superType] = strategy
}

@Suppress("UNCHECKED_CAST")
inline fun <reified T> propertyOverride(propertyName: String, strategy: OptionalStrategy) =
propertyOverride(T::class, propertyName, strategy)

inline fun <reified T> propertyOverride(property: KProperty1<T, *>, strategy: OptionalStrategy) {
// Only allow read only properties in constructor(s)
if (property !is KMutableProperty1) {
val constructorParams = T::class.constructors.flatMap {
it.parameters.map(KParameter::name)
}

check(constructorParams.contains(property.name)) {
"No setter available for ${T::class.qualifiedName}.${property.name}"
}
}

@Suppress("UNCHECKED_CAST")
return propertyOverride(T::class, property.name, strategy)
}

fun propertyOverride(clazz: KClass<*>, propertyName: String, strategy: OptionalStrategy) {
val classProperties = properties.getOrElse(clazz) { mutableMapOf() }
classProperties[propertyName] = strategy

properties[clazz] = classProperties
}

fun build(): OptionalStrategy = OverrideOptionalStrategy(
defaultStrategy = defaultStrategy,
propertyOverrides = properties.mapValues { it.value.toUnmodifiableMap() }.toUnmodifiableMap(),
classOverrides = classes.toUnmodifiableMap()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.optional

import com.appmattus.kotlinfixture.Context
import kotlin.reflect.KClass

data class OverrideOptionalStrategy(
private val defaultStrategy: OptionalStrategy,
private val propertyOverrides: Map<KClass<*>, Map<String, OptionalStrategy>> = emptyMap(),
private val classOverrides: Map<KClass<*>, OptionalStrategy> = emptyMap()
) : OptionalStrategy {

@Suppress("ReturnCount")
override fun Context.generateAsOptional(callingClass: KClass<*>, parameterName: String): Boolean {
// first check property overrides
propertyOverrides[callingClass]?.get(parameterName)?.apply {
return generateAsOptional(callingClass, parameterName)
}

// second check class overrides
classOverrides[callingClass]?.apply {
return generateAsOptional(callingClass, parameterName)
}

// third use default
with(defaultStrategy) {
return generateAsOptional(callingClass, parameterName)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.optional

import com.appmattus.kotlinfixture.Context
import kotlin.reflect.KClass

object RandomlyOptionalStrategy : OptionalStrategy {

override fun Context.generateAsOptional(callingClass: KClass<*>, parameterName: String) = random.nextBoolean()
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import com.appmattus.kotlinfixture.Context
import com.appmattus.kotlinfixture.FixtureException
import com.appmattus.kotlinfixture.Unresolved
import com.appmattus.kotlinfixture.config.DefaultGenerator
import com.appmattus.kotlinfixture.decorator.optional.OptionalStrategy
import com.appmattus.kotlinfixture.decorator.optional.RandomlyOptionalStrategy
import com.appmattus.kotlinfixture.strategyOrDefault
import kotlin.reflect.KParameter
import kotlin.reflect.full.companionObjectInstance
import kotlin.reflect.jvm.isAccessible
Expand All @@ -46,8 +49,15 @@ internal class KFunctionResolver : Resolver {
throw IllegalStateException("Unsupported parameter type: $it")
}
}.filterKeys {
// Keep if the parameter has an override, is mandatory, or if optional at random
overrides.containsKey(it.name) || !it.isOptional || context.random.nextBoolean()
with(context) {
with(strategyOrDefault<OptionalStrategy>(RandomlyOptionalStrategy)) {
// Keep if the parameter has an override, is mandatory, or if optional using the strategy
overrides.containsKey(it.name) || !it.isOptional || !generateAsOptional(
obj.containingClass,
it.name!!
)
}
}
}

if (parameters.all { it.value != Unresolved }) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.optional

import com.appmattus.kotlinfixture.TestContext
import com.appmattus.kotlinfixture.config.ConfigurationBuilder
import com.appmattus.kotlinfixture.resolver.TestResolver
import kotlin.test.Test
import kotlin.test.assertTrue

class AlwaysOptionalStrategyTest {

private val testContext = TestContext(ConfigurationBuilder().build(), TestResolver())

data class DataClass(val optionalValue: String = "hello")

@Test
fun `Strategy AlwaysOptionalStrategy returns true`(): Unit = with(testContext) {
AlwaysOptionalStrategy.apply {
assertTrue {
generateAsOptional(DataClass::class, "optionalValue")
}
}
}
}
Loading

0 comments on commit 716acf4

Please sign in to comment.