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 alternative primary key when resolving values #232

Merged
merged 6 commits into from
Apr 9, 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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public fun <init> (Ljava/lang/Throwable;)V
}

public final class io/github/serpro69/kfaker/exception/RetryLimitException : java/lang/Exception {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-", "")
}
}
}
}
})
22 changes: 16 additions & 6 deletions core/src/main/kotlin/io/github/serpro69/kfaker/FakerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<*> -> {
Expand All @@ -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<*, *> -> {
Expand All @@ -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}")
Expand All @@ -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,
Expand All @@ -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<*, *> -> {
Expand All @@ -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.")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.serpro69.kfaker.extension

typealias AltKey<A, B> = Pair<A, B>

internal infix fun <A, B> A.or(second: B): AltKey<A, B> = AltKey(this, second)
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -66,8 +68,7 @@ abstract class YamlFakeDataProvider<T : FakeDataProvider>(
/**
* 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
* ```
Expand All @@ -76,6 +77,31 @@ abstract class YamlFakeDataProvider<T : FakeDataProvider>(
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, String>): String {
return returnOrResolveUnique(keys)
}

/**
* Returns resolved (unique) value for the parameter with the specified [primaryKey] and [secondaryKey].
*
Expand Down Expand Up @@ -144,6 +170,40 @@ abstract class YamlFakeDataProvider<T : FakeDataProvider>(
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, String>): 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -219,13 +220,13 @@ internal class FakerServiceTest : DescribeSpec({
val fakerService = fakerService(YamlCategory.ADDRESS)

it("exception is thrown") {
shouldThrow<NoSuchElementException> {
shouldThrow<DictionaryKeyNotFoundException> {
fakerService.getRawValue(YamlCategory.ADDRESS, "postcode_by_state", "invalid")
}
}

it("exceptions contains message") {
val message = shouldThrow<NoSuchElementException> {
val message = shouldThrow<DictionaryKeyNotFoundException> {
fakerService.getRawValue(YamlCategory.ADDRESS, "postcode_by_state", "invalid")
}.message

Expand Down Expand Up @@ -291,13 +292,13 @@ internal class FakerServiceTest : DescribeSpec({
val fakerService = fakerService(YamlCategory.EDUCATOR)

it("exception is thrown") {
shouldThrow<NoSuchElementException> {
shouldThrow<DictionaryKeyNotFoundException> {
fakerService.getRawValue(YamlCategory.EDUCATOR, "tertiary", "invalid", "type")
}
}

it("exception contains message") {
val exception = shouldThrow<NoSuchElementException> {
val exception = shouldThrow<DictionaryKeyNotFoundException> {
fakerService.getRawValue(YamlCategory.EDUCATOR, "tertiary", "invalid", "type")
}

Expand All @@ -309,13 +310,13 @@ internal class FakerServiceTest : DescribeSpec({
val fakerService = fakerService(YamlCategory.EDUCATOR)

it("exception is thrown") {
shouldThrow<NoSuchElementException> {
shouldThrow<DictionaryKeyNotFoundException> {
fakerService.getRawValue(YamlCategory.EDUCATOR, "tertiary", "degree", "invalid")
}
}

it("exception contains message") {
val exception = shouldThrow<NoSuchElementException> {
val exception = shouldThrow<DictionaryKeyNotFoundException> {
fakerService.getRawValue(YamlCategory.EDUCATOR, "tertiary", "degree", "invalid")
}

Expand Down