diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e0b18b72f..3076ce50e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,7 +79,7 @@ jobs: run: chmod +x gradlew - name: Compile native image - run: ./gradlew nativeCompile + run: ./gradlew nativeCompile -x test -x integrationTest - name: Test native image run: | _app_path=$(find ./cli-bot/build/native/nativeCompile/ -type f -name faker-bot\* -not -name \*.txt) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index f1bbb1f17..b36f704fb 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,6 +9,7 @@ [discrete] === Added +* https://github.com/serpro69/kotlin-faker/pull/232[#232] (:core) Add support for alternative primary key when resolving values * https://github.com/serpro69/kotlin-faker/pull/227[#227] Add BOM to manage faker versions * 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 diff --git a/core/api/core.api b/core/api/core.api index 6b22491c0..75cb0cb26 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -408,6 +408,12 @@ public final class io/github/serpro69/kfaker/dictionary/YamlCategory : java/lang public final class io/github/serpro69/kfaker/dictionary/YamlCategory$Companion { } +public final class io/github/serpro69/kfaker/exception/DictionaryKeyNotFoundException : java/lang/Exception { + public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V + public fun (Ljava/lang/Throwable;)V +} + public final class io/github/serpro69/kfaker/exception/RetryLimitException : java/lang/Exception { public fun (Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/lang/Throwable;)V @@ -620,6 +626,7 @@ public abstract class io/github/serpro69/kfaker/provider/YamlFakeDataProvider : protected final fun resolve (Ljava/lang/String;)Ljava/lang/String; protected final fun resolve (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; protected final fun resolve (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + protected final fun resolve (Lkotlin/Pair;)Ljava/lang/String; } public final class io/github/serpro69/kfaker/provider/misc/ConstructorFilterStrategy : java/lang/Enum { diff --git a/core/src/integration/kotlin/io/github/serpro69/kfaker/provider/AddressIT.kt b/core/src/integration/kotlin/io/github/serpro69/kfaker/provider/AddressIT.kt index 40cfddf29..0239d826d 100644 --- a/core/src/integration/kotlin/io/github/serpro69/kfaker/provider/AddressIT.kt +++ b/core/src/integration/kotlin/io/github/serpro69/kfaker/provider/AddressIT.kt @@ -77,5 +77,13 @@ class AddressIT : DescribeSpec({ address("en-CA").postcode() shouldMatch Regex("""[A-CEGHJ-NPR-TVXY][0-9][A-CEJ-NPR-TV-Z] ?[0-9][A-CEJ-NPR-TV-Z][0-9]""") } } + + context("default country code") { + listOf("en-US", "en-GB", "en-CA").forEach { locale -> + it("should generate a default country code for $locale") { + address(locale).countryCode() shouldBe locale.replaceFirst("en-", "") + } + } + } } }) 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 cc5f54dc5..a31ff3ddf 100644 --- a/core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt +++ b/core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt @@ -11,6 +11,7 @@ import io.github.serpro69.kfaker.dictionary.YamlCategory.PHONE_NUMBER 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 @@ -297,10 +298,12 @@ class FakerService { /** * 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) - ?: throw NoSuchElementException("Parameter '$key' not found in '$category' category") + ?: throw DictionaryKeyNotFoundException("Parameter '$key' not found in '$category' category") return when (paramValue) { is List<*> -> { @@ -320,10 +323,13 @@ class FakerService { /** * Returns raw value as [RawExpression] from a given [category] fetched by its [key] and [secondaryKey] + * + * @throws DictionaryKeyNotFoundException IF the [dictionary] [category] does not contain the [key], + * OR the primary [key] does not contain the [secondaryKey] */ fun getRawValue(category: YamlCategory, key: String, secondaryKey: String): RawExpression { val parameterValue = dictionary[category]?.get(key) - ?: throw NoSuchElementException("Parameter '$key' not found in '$category' category") + ?: throw DictionaryKeyNotFoundException("Parameter '$key' not found in '$category' category") return when (parameterValue) { is Map<*, *> -> { @@ -343,7 +349,7 @@ class FakerService { is Map<*, *> -> RawExpression(secondaryValue.toString()) else -> throw UnsupportedOperationException("Unsupported type of raw value: ${parameterValue::class.simpleName}") } - } ?: throw NoSuchElementException("Secondary key '$secondaryKey' not found.") + } ?: throw DictionaryKeyNotFoundException("Secondary key '$secondaryKey' not found.") } } else -> throw UnsupportedOperationException("Unsupported type of raw value: ${parameterValue::class.simpleName}") @@ -352,6 +358,10 @@ class FakerService { /** * Returns raw value as [RawExpression] for a given [category] fetched from the [dictionary] by its [key], [secondaryKey], and [thirdKey]. + * + * @throws DictionaryKeyNotFoundException IF the [dictionary] [category] does not contain the [key], + * OR the primary [key] does not contain the [secondaryKey], + * OR the [secondaryKey] does not contain the [thirdKey] */ fun getRawValue( category: YamlCategory, @@ -360,7 +370,7 @@ class FakerService { thirdKey: String, ): RawExpression { val parameterValue = dictionary[category]?.get(key) - ?: throw NoSuchElementException("Parameter '$key' not found in '$category' category") + ?: throw DictionaryKeyNotFoundException("Parameter '$key' not found in '$category' category") return when (parameterValue) { is Map<*, *> -> { @@ -382,12 +392,12 @@ class FakerService { is String -> RawExpression(thirdValue) else -> throw UnsupportedOperationException("Unsupported type of raw value: ${parameterValue::class.simpleName}") } - } ?: throw NoSuchElementException("Third key '$thirdKey' not found.") + } ?: throw DictionaryKeyNotFoundException("Third key '$thirdKey' not found.") } } else -> throw UnsupportedOperationException("Unsupported type of raw value: ${parameterValue::class.simpleName}") } - } ?: throw NoSuchElementException("Secondary key '$secondaryKey' not found.") + } ?: throw DictionaryKeyNotFoundException("Secondary key '$secondaryKey' not found.") } else { throw IllegalArgumentException("Secondary key can not be empty string.") } diff --git a/core/src/main/kotlin/io/github/serpro69/kfaker/exception/DictionaryKeyNotFoundException.kt b/core/src/main/kotlin/io/github/serpro69/kfaker/exception/DictionaryKeyNotFoundException.kt new file mode 100644 index 000000000..9298ad86e --- /dev/null +++ b/core/src/main/kotlin/io/github/serpro69/kfaker/exception/DictionaryKeyNotFoundException.kt @@ -0,0 +1,11 @@ +package io.github.serpro69.kfaker.exception + +@Suppress("unused") +class DictionaryKeyNotFoundException : Exception { + + constructor(message: String) : super(message) + + constructor(message: String, throwable: Throwable) : super(message, throwable) + + constructor(throwable: Throwable) : super(throwable) +} diff --git a/core/src/main/kotlin/io/github/serpro69/kfaker/extension/Pair.kt b/core/src/main/kotlin/io/github/serpro69/kfaker/extension/Pair.kt new file mode 100644 index 000000000..5164eda38 --- /dev/null +++ b/core/src/main/kotlin/io/github/serpro69/kfaker/extension/Pair.kt @@ -0,0 +1,5 @@ +package io.github.serpro69.kfaker.extension + +typealias AltKey = Pair + +internal infix fun A.or(second: B): AltKey = AltKey(this, second) 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 e1e63ff17..1fad9bdb5 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 @@ -1,7 +1,8 @@ package io.github.serpro69.kfaker.provider -import io.github.serpro69.kfaker.* -import io.github.serpro69.kfaker.dictionary.* +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.provider.unique.LocalUniqueDataProvider import io.github.serpro69.kfaker.provider.unique.UniqueProviderDelegate @@ -23,7 +24,7 @@ class Address internal constructor(fakerService: FakerService) : YamlFakeDataPro fun country() = resolve("country") fun countryByCode(countryCode: String) = resolve("country_by_code", countryCode) fun countryByName(countryName: String) = resolve("country_by_name", countryName) - fun countryCode() = resolve("country_code") + fun countryCode() = resolve("default_country_code" or "country_code") fun countryCodeLong() = resolve("country_code_long") fun buildingNumber() = with(fakerService) { resolve("building_number").numerify() } fun communityPrefix() = resolve("community_prefix") diff --git a/core/src/main/kotlin/io/github/serpro69/kfaker/provider/YamlFakeDataProvider.kt b/core/src/main/kotlin/io/github/serpro69/kfaker/provider/YamlFakeDataProvider.kt index 5d51ae824..0ab213a1f 100644 --- a/core/src/main/kotlin/io/github/serpro69/kfaker/provider/YamlFakeDataProvider.kt +++ b/core/src/main/kotlin/io/github/serpro69/kfaker/provider/YamlFakeDataProvider.kt @@ -4,7 +4,9 @@ import io.github.serpro69.kfaker.AbstractFaker import io.github.serpro69.kfaker.FakerService import io.github.serpro69.kfaker.dictionary.Category import io.github.serpro69.kfaker.dictionary.YamlCategory +import io.github.serpro69.kfaker.exception.DictionaryKeyNotFoundException import io.github.serpro69.kfaker.exception.RetryLimitException +import io.github.serpro69.kfaker.extension.AltKey /** * Abstract class for all concrete [FakeDataProvider]'s that use yml files as data source. @@ -66,8 +68,7 @@ abstract class YamlFakeDataProvider( /** * Returns resolved (unique) value for the parameter with the specified [key]. * - * Will return a unique value if the call to the function is prefixed with `unique` property. - * Example: + * Will return a unique value if the call to the function is prefixed with `unique` property. Example: * ``` * faker.address.unique.city() => will return a unique value for the `city` parameter * ``` @@ -76,6 +77,31 @@ abstract class YamlFakeDataProvider( return returnOrResolveUnique(key) } + /** + * Returns resolved (unique) value for the parameter with the specified pair of [keys], + * where `first` is the "altKey" and `second` is the "primaryKey". + * + * This function can be used to resolve locale-specific keys that are not present in the default 'en' dictionaries. + * + * An example usage (taken from [Address.countryCode]) looks something like this: + * + * ``` + * fun countryCode() = resolve("default_country_code" or "country_code") + * ``` + * + * Here, the `"default_country_code"` is the key that is only present in the localized dictionaries, + * which may or may not be present in the default 'en' dictionary, + * and `"country_code"` is the default key for this function which is defined in `en/address.yml` dict file. + * + * Will attempt to return a unique value if the call to the function is prefixed with `unique` property. Example: + * ``` + * faker.address.unique.countryCode() => will return a unique value for the `country_code` parameter. + * ``` + */ + protected fun resolve(keys: AltKey): String { + return returnOrResolveUnique(keys) + } + /** * Returns resolved (unique) value for the parameter with the specified [primaryKey] and [secondaryKey]. * @@ -144,6 +170,40 @@ abstract class YamlFakeDataProvider( return returnOrResolveUnique(primaryKey, secondaryKey, thirdKey) } + /** + * Returns the result of this [resolve] function using a pair of [AltKey]s, + * where `first` is the "altKey" and `second` is the "primaryKey". + * + * This function can be used to resolve locale-specific keys that are not present in the default 'en' dictionaries. + * + * An example usage (taken from [Address.countryCode]) looks something like this: + * + * ``` + * fun countryCode() = resolve("default_country_code" or "country_code") + * ``` + * + * Here, the `"default_country_code"` is the key that is only present in the localized dictionaries, + * which may or may not be present in the default 'en' dictionary, + * and `"country_code"` is the default key for this function which is defined in `en/address.yml` dict file. + * + * IF [AbstractFaker.unique] is enabled for this [T] provider type + * OR this [unique] is used + * THEN will attempt to return a unique value. + * + * @throws RetryLimitException if exceeds number of retries to generate a unique value. + */ + private fun returnOrResolveUnique(keys: AltKey): String { + val (altKey, primaryKey) = keys + return try { + resolveUniqueValue(altKey) { fakerService.resolve(yamlCategory, altKey) } + } catch (e: DictionaryKeyNotFoundException) { + resolveUniqueValue(primaryKey) { fakerService.resolve(yamlCategory, primaryKey) } + } catch (e: Exception) { + e.printStackTrace() + throw e + } + } + /** * Returns the result of this [resolve] function. * diff --git a/core/src/test/kotlin/io/github/serpro69/kfaker/FakerServiceTest.kt b/core/src/test/kotlin/io/github/serpro69/kfaker/FakerServiceTest.kt index 0d1f39146..401b05c46 100644 --- a/core/src/test/kotlin/io/github/serpro69/kfaker/FakerServiceTest.kt +++ b/core/src/test/kotlin/io/github/serpro69/kfaker/FakerServiceTest.kt @@ -4,6 +4,7 @@ import io.github.serpro69.kfaker.dictionary.Category import io.github.serpro69.kfaker.dictionary.YamlCategoryData import io.github.serpro69.kfaker.dictionary.Dictionary import io.github.serpro69.kfaker.dictionary.YamlCategory +import io.github.serpro69.kfaker.exception.DictionaryKeyNotFoundException import io.kotest.assertions.assertSoftly import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.DescribeSpec @@ -219,13 +220,13 @@ internal class FakerServiceTest : DescribeSpec({ val fakerService = fakerService(YamlCategory.ADDRESS) it("exception is thrown") { - shouldThrow { + shouldThrow { fakerService.getRawValue(YamlCategory.ADDRESS, "postcode_by_state", "invalid") } } it("exceptions contains message") { - val message = shouldThrow { + val message = shouldThrow { fakerService.getRawValue(YamlCategory.ADDRESS, "postcode_by_state", "invalid") }.message @@ -291,13 +292,13 @@ internal class FakerServiceTest : DescribeSpec({ val fakerService = fakerService(YamlCategory.EDUCATOR) it("exception is thrown") { - shouldThrow { + shouldThrow { fakerService.getRawValue(YamlCategory.EDUCATOR, "tertiary", "invalid", "type") } } it("exception contains message") { - val exception = shouldThrow { + val exception = shouldThrow { fakerService.getRawValue(YamlCategory.EDUCATOR, "tertiary", "invalid", "type") } @@ -309,13 +310,13 @@ internal class FakerServiceTest : DescribeSpec({ val fakerService = fakerService(YamlCategory.EDUCATOR) it("exception is thrown") { - shouldThrow { + shouldThrow { fakerService.getRawValue(YamlCategory.EDUCATOR, "tertiary", "degree", "invalid") } } it("exception contains message") { - val exception = shouldThrow { + val exception = shouldThrow { fakerService.getRawValue(YamlCategory.EDUCATOR, "tertiary", "degree", "invalid") }