Skip to content

Commit

Permalink
Get rid of reflection in FakerService (#252)
Browse files Browse the repository at this point in the history
Partially does what #23 wants. Over-engineering is evil...

There are still some parts in :core that user reflection, e.g. unique data generation bits. This PR doesn't attempt to address those.

At the very least, this should fix most issues with #250
  • Loading branch information
serpro69 authored Oct 28, 2024
1 parent caf10e6 commit 8e77afd
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 88 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
* https://github.com/serpro69/kotlin-faker/issues/222[#222] (:faker:databases) Create new Databases faker module
* https://github.com/serpro69/kotlin-faker/issues/218[#218] (:core) Allow creating custom fakers / generators

[discrete]
=== Changed

* https://github.com/serpro69/kotlin-faker/pull/252[#252] (:core) Get rid of reflection in `FakerService`

[discrete]
=== Fixed

Expand Down
1 change: 1 addition & 0 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public final class io/github/serpro69/kfaker/FakerService {
public final fun getLetterify (Ljava/lang/String;)Lkotlin/jvm/functions/Function1;
public final fun getNumerify (Ljava/lang/String;)Lkotlin/jvm/functions/Function0;
public final fun getRawValue-DOu9s8A (Lio/github/serpro69/kfaker/dictionary/YamlCategory;Ljava/lang/String;)Ljava/lang/String;
public final fun getRawValue-DOu9s8A (Lio/github/serpro69/kfaker/dictionary/YamlCategory;[Ljava/lang/String;)Ljava/lang/String;
public final fun getRawValue-EpprjwY (Lio/github/serpro69/kfaker/dictionary/YamlCategory;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
public final fun getRawValue-f6CUTBQ (Lio/github/serpro69/kfaker/dictionary/YamlCategory;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
public final fun getRegexify (Ljava/lang/String;)Lkotlin/jvm/functions/Function0;
Expand Down
133 changes: 55 additions & 78 deletions core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,14 @@ import io.github.serpro69.kfaker.dictionary.YamlCategory.SEPARATOR
import io.github.serpro69.kfaker.dictionary.YamlCategoryData
import io.github.serpro69.kfaker.dictionary.lowercase
import io.github.serpro69.kfaker.exception.DictionaryKeyNotFoundException
import io.github.serpro69.kfaker.provider.Address
import io.github.serpro69.kfaker.provider.FakeDataProvider
import io.github.serpro69.kfaker.provider.Name
import io.github.serpro69.kfaker.provider.YamlFakeDataProvider
import java.io.InputStream
import java.util.*
import java.util.regex.Matcher
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
import kotlin.reflect.KFunction
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties

/**
* Internal class used for resolving yaml expressions into values.
Expand All @@ -33,7 +28,6 @@ import kotlin.reflect.full.declaredMemberProperties
*/
class FakerService {
@Suppress("RegExpRedundantEscape")
private val curlyBraceRegex = Regex("""#\{(?!\d)(\p{L}+\.)?(.*?)\}""")
private val locale: String
internal val faker: AbstractFaker
internal val randomService: RandomService
Expand Down Expand Up @@ -296,18 +290,28 @@ class FakerService {
return fakerData[category.lowercase()] as Map<String, Any>?
}

fun getRawValue(category: YamlCategory, vararg keys: String): RawExpression {
return when (keys.size) {
1 -> getRawValue(category, keys.first())
2 -> getRawValue(category, keys.first(), keys.last())
3 -> getRawValue(category, keys[0], keys[1], keys[2])
else -> throw UnsupportedOperationException("Unsupported keys length of ${keys.size}")
}
}

/**
* Returns raw value as [RawExpression] from a given [category] fetched by its [key]
*
* @throws DictionaryKeyNotFoundException IF the [dictionary] [category] does not contain the [key]
*/
fun getRawValue(category: YamlCategory, key: String): RawExpression {
val paramValue = dictionary[category]?.get(key)
val paramValue = getProviderData(category)[key]
?: throw DictionaryKeyNotFoundException("Parameter '$key' not found in '$category' category")

return when (paramValue) {
is List<*> -> {
if (paramValue.isEmpty()) RawExpression("") else when (val value = randomService.randomValue(paramValue)) {
if (paramValue.isEmpty()) RawExpression("") else when (val value =
randomService.randomValue(paramValue)) {
is List<*> -> {
if (value.isEmpty()) RawExpression("") else RawExpression(randomService.randomValue(value) as String)
}
Expand All @@ -328,7 +332,7 @@ class FakerService {
* OR the primary [key] does not contain the [secondaryKey]
*/
fun getRawValue(category: YamlCategory, key: String, secondaryKey: String): RawExpression {
val parameterValue = dictionary[category]?.get(key)
val parameterValue = getProviderData(category)[key]
?: throw DictionaryKeyNotFoundException("Parameter '$key' not found in '$category' category")

return when (parameterValue) {
Expand Down Expand Up @@ -369,7 +373,7 @@ class FakerService {
secondaryKey: String,
thirdKey: String,
): RawExpression {
val parameterValue = dictionary[category]?.get(key)
val parameterValue = getProviderData(category)[key]
?: throw DictionaryKeyNotFoundException("Parameter '$key' not found in '$category' category")

return when (parameterValue) {
Expand Down Expand Up @@ -485,30 +489,51 @@ class FakerService {
*/
@Suppress("KDocUnresolvedReference")
private tailrec fun resolveExpression(category: YamlCategory, rawExpression: RawExpression): String {
val cc = category
.lowercase()
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
val primary = category.names.toMutableSet().plus(cc).joinToString("|")
val secondary = category.children.toMutableSet().joinToString("|")
// https://regex101.com/r/KIvagc/1
// #\{(?!\d)(?:(Creature|Games|(Bird|Cat|Dog))\.)?((?![A-Z]\p{L}*\.).*?)\}
val lexpr = Regex("""#\{(?!\d)(?i:($primary|($secondary))\.)?((?![A-Z]\p{L}*\.).*?)\}""")
// https://regex101.com/r/I8gG7M/1
// #\{(?!\d)(?!(?i:Creature|(?i:Bird|Cat|Dog))\.)(?:([A-Z]\p{L}+())\.)?(.*?)\}
val cexpr = Regex("""#\{(?!\d)(?!(?i:$primary|(?i:$secondary))\.)(?:([A-Z]\p{L}+())\.)?(.*?)\}""")
val sb = StringBuffer()
val pc: (Matcher) -> YamlCategory? = { it.group(1)?.let { c -> YamlCategory.findByName(c) } }
val sc: (Matcher) -> Category? = { it.group(2)?.let { c -> Category.ofName(c.uppercase()) } }

val resolvedExpression = when {
curlyBraceRegex.containsMatchIn(rawExpression.value) -> {
findMatchesAndAppendTail(rawExpression.value, sb, curlyBraceRegex) {
val simpleClassName = it.group(1)?.trimEnd('.')

val replacement = when (simpleClassName != null) {
true -> {
val (providerType, propertyName) = getProvider(simpleClassName).getFunctionName(it.group(2))
providerType.callFunction(propertyName)
}
false -> getRawValue(category, it.group(2)).value
}

lexpr.containsMatchIn(rawExpression.value) -> {
findMatchesAndAppendTail(rawExpression.value, sb, lexpr) {
val args = sc(it)?.let { c -> "${c.name.lowercase()}.${it.group(3)}".split(".").toTypedArray() }
?: it.group(3).split(".").toTypedArray()
val replacement = getRawValue(category, *args).value
it.appendReplacement(sb, replacement)
}
}
else -> rawExpression.value
}

return if (!curlyBraceRegex.containsMatchIn(resolvedExpression)) {
resolvedExpression
} else resolveExpression(category, RawExpression(resolvedExpression))
return when {
!lexpr.containsMatchIn(resolvedExpression)
&& !cexpr.containsMatchIn(resolvedExpression) -> resolvedExpression
else -> {
if (lexpr.containsMatchIn(resolvedExpression)) {
resolveExpression(category, RawExpression(resolvedExpression))
} else {
val cm = cexpr.toPattern().matcher(resolvedExpression)
when { // resolve expression from another category, rinse and repeat
cm.find() -> {
val cat = pc(cm) ?: category
resolveExpression(cat, RawExpression(resolvedExpression.replace(cc, "")))
}
else -> resolveExpression(category, RawExpression(resolvedExpression))
}
}
}
}
}

/**
Expand Down Expand Up @@ -549,44 +574,6 @@ class FakerService {
val String.regexify: () -> String
get() = { RgxGen.parse(this).generate(faker.config.random) }

/**
* Calls the property of this [FakeDataProvider] receiver and returns the result as [String].
*
* @param T instance of [FakeDataProvider]
* @param kFunction the [KFunction] of [T]
*/
private fun <T : FakeDataProvider> T.callFunction(kFunction: KFunction<*>): String {
return kFunction.call(this) as String
}

/**
* Gets the [KFunction] of this [FakeDataProvider] receiver from the [rawString].
*
* Examples:
*
* - 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]
*/
@Suppress("KDocUnresolvedReference")
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.declaredMemberFunctions.firstOrNull { it.name == funcName }
?.let { this to it }
?: run {
this::class.declaredMemberProperties.firstOrNull { it.name == funcName.substringBefore(".") }?.let {
(it.getter.call(this) as YamlFakeDataProvider<*>)
.getFunctionName(funcName.substringAfter("."))
}
}
?: throw NoSuchElementException("Function $funcName not found in $this")
}

/**
* Returns an instance of [FakeDataProvider] fetched by its [simpleClassName] (case-insensitive).
*
Expand All @@ -596,19 +583,11 @@ class FakerService {
* @throws NoSuchElementException if neither this [faker] nor the core [Faker] implementation
* has declared a provider that matches the [simpleClassName] parameter.
*/
private fun getProvider(simpleClassName: String): FakeDataProvider {
val kProp = faker::class.declaredMemberProperties.firstOrNull {
it.name.lowercase() == simpleClassName.lowercase()
}

return kProp?.let { it.call(faker) as FakeDataProvider } ?: run {
val core = Faker(faker.config)
val prop = core::class.declaredMemberProperties.firstOrNull { p ->
p.name.lowercase() == simpleClassName.lowercase()
}
prop?.let { p -> p.call(core) as FakeDataProvider }
?: throw NoSuchElementException("Faker provider '$simpleClassName' not found in $core or $faker")
}
private fun getProviderData(primary: YamlCategory, secondary: Category? = null): YamlCategoryData {
return dictionary[primary]
?: secondary?.let { load(primary, secondary)[primary] }
?: load(primary)[primary]
?: throw NoSuchElementException("Category $primary not found in $this")
}

private fun findMatchesAndAppendTail(
Expand All @@ -618,9 +597,7 @@ class FakerService {
invoke: (Matcher) -> Unit
): String {
val matcher = regex.toPattern().matcher(string)

while (matcher.find()) invoke(matcher)

matcher.appendTail(stringBuffer)
return stringBuffer.toString()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ package io.github.serpro69.kfaker.dictionary
* This enum contains all default categories and matches with the names of the .yml files for 'en' locale.
*
* If any new category is added to .yml file(s) a new class has to be added to this enum as well.
*
* @property children an optional set of children category names that are not part of this enum (e.g. Creature -> Animal)
* @property names alternative names that may be used to refer to this category in yml expressions, e.g.
* `#{PhoneNumber.area_code}` is used in en-US.yml:6932 instead of `#{Phone_Number.area_code}`
*/
enum class YamlCategory : Category {
enum class YamlCategory(
internal val names: Set<String> = emptySet(),
internal val children: Set<String> = emptySet(),
) : Category {
/**
* [YamlCategory] for custom yml-based data providers
*/
Expand Down Expand Up @@ -36,8 +43,8 @@ enum class YamlCategory : Category {
BIG_BANG_THEORY,
BLOOD,
BOJACK_HORSEMAN,
BOOK,
BOOKS,
BOOK(children = setOf("title")),
BOOKS(children = setOf("the_kingkiller_chronicle")),
BOSSA_NOVA,
BREAKING_BAD,
BROOKLYN_NINE_NINE,
Expand All @@ -62,7 +69,7 @@ enum class YamlCategory : Category {
CONSTRUCTION,
COSMERE,
COWBOY_BEBOP,
CREATURE,
CREATURE(children = setOf("animal", "bird", "cat", "dog", "horse")),
CROSSFIT,
CRYPTO_COIN,
CULTURE_SERIES,
Expand All @@ -75,7 +82,7 @@ enum class YamlCategory : Category {
DEVICE,
DND,
DORAEMON,
GAMES,
GAMES(children = games),
DRAGON_BALL,
DRIVING_LICENSE,
DRONE,
Expand All @@ -96,7 +103,7 @@ enum class YamlCategory : Category {
FRIENDS,
FUNNY_NAME,
FUTURAMA,
GAME,
GAME(children = setOf("title")),
GAME_OF_THRONES,
GENDER,
GHOSTBUSTERS,
Expand Down Expand Up @@ -146,7 +153,7 @@ enum class YamlCategory : Category {
PARKS_AND_REC,
PEARL_JAM,
PHISH,
PHONE_NUMBER,
PHONE_NUMBER(names = setOf("PhoneNumber")),
PRINCE,
PRINCESS_BRIDE,
PROGRAMMING_LANGUAGE,
Expand Down Expand Up @@ -212,8 +219,34 @@ enum class YamlCategory : Category {
* Returns [YamlCategory] by [name] string (case-insensitive).
*/
internal fun findByName(name: String): YamlCategory {
return values().firstOrNull { it.lowercase() == name.lowercase() }
?: throw NoSuchElementException("Category with name '$name' not found.")
return values().firstOrNull {
it.lowercase() == name.lowercase() || it.names.any { n -> it.lowercase() == n.lowercase() }
} ?: throw NoSuchElementException("Category with name '$name' not found.")
}
}
}

private val games = setOf(
"dota",
"clash_of_clan",
"control",
"elder_scrolls",
"fallout",
"final_fantasy_xiv",
"half_life",
"league_of_legends",
"minecraft",
"myst",
"overwatch",
"pokemon",
"sonic_the_hedgehog",
"street_fighter",
"super_mario",
"super_smash_bros",
"touhou",
"tron",
"warhammer_fantasy",
"witcher",
"world_of_warcraft",
"zelda",
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.serpro69.kfaker.provider
import io.github.serpro69.kfaker.FakerService
import io.github.serpro69.kfaker.dictionary.YamlCategory
import io.github.serpro69.kfaker.extension.or
import io.github.serpro69.kfaker.faker
import io.github.serpro69.kfaker.provider.unique.LocalUniqueDataProvider
import io.github.serpro69.kfaker.provider.unique.UniqueProviderDelegate

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Educator internal constructor(fakerService: FakerService) : YamlFakeDataPr
fun campus() = resolve("campus")
fun subject() = resolve("subject")
fun degree() = resolve("degree")
fun courseName() = resolve("course_name")
fun courseName() = with(fakerService) { resolve("course_name").numerify() }
}

@Suppress("unused")
Expand Down

0 comments on commit 8e77afd

Please sign in to comment.