diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index f020887a6..a64041ee2 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -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 diff --git a/core/api/core.api b/core/api/core.api index 9b8e10aae..a8c72006c 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -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; diff --git a/core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt b/core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt index 25c692975..cdb23553b 100644 --- a/core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt +++ b/core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt @@ -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. @@ -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 @@ -296,18 +290,28 @@ class FakerService { return fakerData[category.lowercase()] as Map? } + 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) } @@ -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) { @@ -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) { @@ -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)) + } + } + } + } } /** @@ -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.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.getFunctionName(rawString: String): Pair> { - 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). * @@ -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( @@ -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() } diff --git a/core/src/main/kotlin/io/github/serpro69/kfaker/dictionary/YamlCategory.kt b/core/src/main/kotlin/io/github/serpro69/kfaker/dictionary/YamlCategory.kt index 076ae1de4..62d386a50 100644 --- a/core/src/main/kotlin/io/github/serpro69/kfaker/dictionary/YamlCategory.kt +++ b/core/src/main/kotlin/io/github/serpro69/kfaker/dictionary/YamlCategory.kt @@ -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 = emptySet(), + internal val children: Set = emptySet(), +) : Category { /** * [YamlCategory] for custom yml-based data providers */ @@ -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, @@ -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, @@ -75,7 +82,7 @@ enum class YamlCategory : Category { DEVICE, DND, DORAEMON, - GAMES, + GAMES(children = games), DRAGON_BALL, DRIVING_LICENSE, DRONE, @@ -96,7 +103,7 @@ enum class YamlCategory : Category { FRIENDS, FUNNY_NAME, FUTURAMA, - GAME, + GAME(children = setOf("title")), GAME_OF_THRONES, GENDER, GHOSTBUSTERS, @@ -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, @@ -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", +) diff --git a/core/src/main/kotlin/io/github/serpro69/kfaker/provider/Address.kt b/core/src/main/kotlin/io/github/serpro69/kfaker/provider/Address.kt index 097efbbc2..ccd5a97e6 100644 --- a/core/src/main/kotlin/io/github/serpro69/kfaker/provider/Address.kt +++ b/core/src/main/kotlin/io/github/serpro69/kfaker/provider/Address.kt @@ -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 diff --git a/faker/edu/src/main/kotlin/io/github/serpro69/kfaker/edu/provider/Educator.kt b/faker/edu/src/main/kotlin/io/github/serpro69/kfaker/edu/provider/Educator.kt index 8f60aa80a..888adeb1c 100644 --- a/faker/edu/src/main/kotlin/io/github/serpro69/kfaker/edu/provider/Educator.kt +++ b/faker/edu/src/main/kotlin/io/github/serpro69/kfaker/edu/provider/Educator.kt @@ -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")