Skip to content

Commit

Permalink
Add support for alternative primary key when resolving values (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
serpro69 authored Apr 9, 2024
1 parent a843720 commit 110ac11
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 18 deletions.
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

0 comments on commit 110ac11

Please sign in to comment.