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

Get rid of reflection in FakerService #252

Merged
merged 10 commits into from
Oct 28, 2024
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
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