Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for chained parameter expressions in yml dicts #118

Merged
merged 4 commits into from
Feb 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and the project versioning adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [1.10.0] - UNRELEASED
### Added
- [#115](https://github.com/serpro69/kotlin-faker/pull/115) [core] Add Crossfit® provider to Faker
- [#117](https://github.com/serpro69/kotlin-faker/pull/117) [core] Add namedParameterGenerator for RandomProvider#randomClassInstance
- [#118](https://github.com/serpro69/kotlin-faker/pull/118) [core] Add support for chained parameter expressions in yml dicts
- [#55](https://github.com/serpro69/kotlin-faker/pull/55) [core] Add missing 'Educator' functionality

## [1.9.0] - 2021-11-19
### Added
- [#103](https://github.com/serpro69/kotlin-faker/issues/103) [core] Add support for `Collection` types in `RandomProvider#randomClassInstance`
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ subprojects {
testRuntimeOnly("org.codehaus.groovy:groovy:3.0.9")
}

configure<JavaPluginConvention> {
configure<JavaPluginExtension> {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,15 @@
"name":"io.github.serpro69.kfaker.provider.CurrencySymbol",
"methods":[{"name":"symbol","parameterTypes":[] }]
},
{
"name":"io.github.serpro69.kfaker.provider.Degree",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{
"name":"io.github.serpro69.kfaker.provider.Degree[]"
},
{
"name":"io.github.serpro69.kfaker.provider.DcComics",
"methods":[
Expand Down Expand Up @@ -1557,6 +1566,15 @@
{"name":"sport","parameterTypes":[] }
]
},
{
"name":"io.github.serpro69.kfaker.provider.Tertiary",
"allDeclaredFields":true,
"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
{
"name":"io.github.serpro69.kfaker.provider.Tertiary[]"
},
{
"name":"io.github.serpro69.kfaker.provider.TheExpanse",
"methods":[
Expand Down
36 changes: 25 additions & 11 deletions core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ import io.github.serpro69.kfaker.dictionary.Dictionary
import io.github.serpro69.kfaker.dictionary.RawExpression
import io.github.serpro69.kfaker.dictionary.getCategoryName
import io.github.serpro69.kfaker.dictionary.toLowerCase
import io.github.serpro69.kfaker.provider.AbstractFakeDataProvider
import io.github.serpro69.kfaker.provider.Address
import io.github.serpro69.kfaker.provider.Educator
import io.github.serpro69.kfaker.provider.Degree
import io.github.serpro69.kfaker.provider.Tertiary
import io.github.serpro69.kfaker.provider.FakeDataProvider
import io.github.serpro69.kfaker.provider.Name
import java.io.InputStream
import java.util.*
import java.util.regex.Matcher
import kotlin.collections.set
import kotlin.reflect.KFunction
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties

/**
Expand Down Expand Up @@ -301,6 +305,10 @@ internal class FakerService @JvmOverloads internal constructor(
* For yaml expressions:
* - `#{city_prefix}` from `en: faker: address` would be resolved to getting value from `address: city_prefix`
* - `#{Name.first_name} from `en: faker: address` would be resolved to calling [Name.name] function.
* - `#{Educator.tertiary.degree.type}` from `en: faker: educator: degree` would be resolved to calling [Degree.type] function.
* In this case the chained call needs to be implemented as a "class->property" hierarchy,
* i.e. [Educator] class must declare a `tertiary` property of [Tertiary] type,
* which in turn must declare a `degree` property of [Degree] type, and so on.
*
* Recursive expressions are also supported:
* - `#{Name.name}` from `en: faker: book: author` that could be resolved to `#{first_name} #{last_name}` from `en: faker: name: name`
Expand Down Expand Up @@ -354,9 +362,7 @@ internal class FakerService @JvmOverloads internal constructor(

val replacement = when (simpleClassName != null) {
true -> {
val providerType = getProvider(simpleClassName)
val propertyName = providerType.getFunctionName(it.group(2))

val (providerType, propertyName) = getProvider(simpleClassName).getFunctionName(it.group(2))
providerType.callFunction(propertyName)
}
false -> getRawValue(category, it.group(2)).value
Expand Down Expand Up @@ -407,22 +413,30 @@ internal class FakerService @JvmOverloads internal constructor(
*
* Examples:
*
* Yaml expression in the form of `Name.first_name` would be translated to [Name.firstName] function.
*
* Yaml expression in the form of `Address.country` would be translated to [Address.country] function.
* - Yaml expression in the form of `Name.first_name` would return the [Name.firstName] function.
* - Yaml expression in the form of `Address.country` would return the [Address.country] function.
* - Yaml expression in the form of `Educator.tertiary.degree.course_number` would return the [Educator.tertiary.degree.courseNumber] function.
*
* @param T instance of [FakeDataProvider]
*/
private fun <T : FakeDataProvider> T.getFunctionName(rawString: String): KFunction<*> {
val propertyName = rawString.split("_").mapIndexed { i: Int, s: String ->
private fun <T : FakeDataProvider> T.getFunctionName(rawString: String): Pair<FakeDataProvider, KFunction<*>> {
val funcName = rawString.split("_").mapIndexed { i: Int, s: String ->
if (i == 0) s else s.substring(0, 1).uppercase() + s.substring(1)
}.joinToString("")

return this::class.declaredFunctions.first { it.name == propertyName }
return this::class.declaredMemberFunctions.firstOrNull { it.name == funcName }
?.let { this to it }
?: run {
this::class.declaredMemberProperties.firstOrNull { it.name == funcName.substringBefore(".") }?.let {
(it.getter.call(this) as AbstractFakeDataProvider<*>)
.getFunctionName(funcName.substringAfter("."))
}
}
?: throw NoSuchElementException("Function $funcName not found in $this")
}

/**
* Returns an instance of [FakeDataProvider] fetched by it's [simpleClassName] (case-insensitive).
* Returns an instance of [FakeDataProvider] fetched by its [simpleClassName] (case-insensitive).
*/
private fun getProvider(simpleClassName: String): FakeDataProvider {
val kProp = faker::class.declaredMemberProperties.first {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,46 @@ class Educator internal constructor(fakerService: FakerService) : AbstractFakeDa
override val localUniqueDataProvider = LocalUniqueDataProvider<Educator>()
override val unique by UniqueProviderDelegate(localUniqueDataProvider)

val tertiary = Tertiary(fakerService)

fun schoolName() = resolve("school_name")
fun secondary() = resolve("secondary")

@Deprecated(level = DeprecationLevel.ERROR, message = "Not fully implemented")
fun university() = resolve("university")

fun secondarySchool() = resolve("secondary_school")
fun campus() = resolve("campus")
fun subject() = resolve("subject")

@Deprecated(level = DeprecationLevel.ERROR, message = "Not fully implemented")
fun degree() = resolve("degree")

@Deprecated(level = DeprecationLevel.ERROR, message = "Not fully implemented")
fun courseName() = resolve("course_name")

@Deprecated(
message = "This is deprecated and will be removed in future releases",
replaceWith = ReplaceWith("tertiary.universityType()"),
level = DeprecationLevel.WARNING
)
fun universityType() = resolve("tertiary", "university_type")
fun tertiaryDegreeType() = resolve("tertiary", "degree", "type")
fun tertiaryDegreeCourseNumber() = with(fakerService) {
}

class Tertiary internal constructor(fakerService: FakerService) : AbstractFakeDataProvider<Tertiary>(fakerService) {
override val categoryName = CategoryName.EDUCATOR
override val localUniqueDataProvider = LocalUniqueDataProvider<Tertiary>()
override val unique by UniqueProviderDelegate(localUniqueDataProvider)

val degree = Degree(fakerService)

fun universityType() = resolve("tertiary", "university_type")
}

class Degree internal constructor(fakerService: FakerService) : AbstractFakeDataProvider<Degree>(fakerService) {
override val categoryName = CategoryName.EDUCATOR
override val localUniqueDataProvider = LocalUniqueDataProvider<Degree>()
override val unique by UniqueProviderDelegate(localUniqueDataProvider)

fun type() = resolve("tertiary", "degree", "type")
fun courseNumber() = with(fakerService) {
resolve("tertiary", "degree", "course_number")
.numerify()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import io.github.serpro69.kfaker.dictionary.toLowerCase
import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.collections.shouldBeIn
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.collections.shouldContainAll
import io.kotest.matchers.collections.shouldContainAnyOf
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.collections.shouldHaveAtLeastSize
import io.kotest.matchers.collections.shouldHaveAtMostSize
Expand Down Expand Up @@ -457,13 +459,26 @@ internal class FakerServiceTest : DescribeSpec({
val peru = fakerService.resolve(address, "country_by_code", "PE")
val norway = fakerService.resolve(address, "country_by_code", "NO")

context("expression is resolved using secondary key") {
it("expression is resolved using secondary key") {
assertSoftly {
peru shouldBe "Peru"
norway shouldBe "Norway"
}
}
}

context("expression calls are chained with a dot '.' char") {
val educator = fakerService.fetchCategory(CategoryName.EDUCATOR)
val degreeType = fakerService.resolve(educator, "degree")

it("is resolved by functionName") {
degreeType.split(" ").take(2).joinToString(" ") shouldBeIn listOf(
"Associate Degree",
"Bachelor of",
"Master of",
)
}
}
}
}
}
Expand Down