-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
363 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
src/commonMain/kotlin/com/xebia/functional/prompt/PromptTemplate.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package com.xebia.functional.prompt | ||
|
||
import arrow.core.raise.Raise | ||
import okio.FileSystem | ||
import okio.Path | ||
import okio.buffer | ||
import okio.use | ||
|
||
fun Raise<InvalidTemplate>.PromptTemplate( | ||
examples: List<String>, | ||
suffix: String, | ||
variables: List<String>, | ||
prefix: String | ||
): PromptTemplate { | ||
val template = """|$prefix | ||
| | ||
|${examples.joinToString(separator = "\n")} | ||
| | ||
|$suffix""".trimMargin() | ||
return PromptTemplate(Config(template, variables)) | ||
} | ||
|
||
fun Raise<InvalidTemplate>.PromptTemplate(template: String, variables: List<String>): PromptTemplate = | ||
PromptTemplate(Config(template, variables)) | ||
|
||
/** | ||
* Creates a PromptTemplate based on a Path | ||
* JVM & Native have overloads for FileSystem.SYSTEM, | ||
* on NodeJs you need to manually pass FileSystem.SYSTEM. | ||
* | ||
* This function can currently not be used on the browser. | ||
* | ||
* https://github.com/square/okio/issues/1070 | ||
* https://youtrack.jetbrains.com/issue/KT-47038 | ||
*/ | ||
suspend fun Raise<InvalidTemplate>.PromptTemplate( | ||
path: Path, | ||
variables: List<String>, | ||
fileSystem: FileSystem | ||
): PromptTemplate = | ||
fileSystem.source(path).use { source -> | ||
source.buffer().use { buffer -> | ||
val template = buffer.readUtf8() | ||
val config = Config(template, variables) | ||
PromptTemplate(config) | ||
} | ||
} | ||
|
||
interface PromptTemplate { | ||
val inputKeys: List<String> | ||
suspend fun format(variables: Map<String, String>): String | ||
|
||
companion object { | ||
operator fun invoke(config: Config): PromptTemplate = object : PromptTemplate { | ||
override val inputKeys: List<String> = config.inputVariables | ||
|
||
override suspend fun format(variables: Map<String, String>): String { | ||
val mergedArgs = mergePartialAndUserVariables(variables, config.inputVariables) | ||
return when (config.templateFormat) { | ||
TemplateFormat.FString -> { | ||
val sortedArgs = mergedArgs.toList().sortedBy { it.first } | ||
sortedArgs.fold(config.template) { acc, (k, v) -> acc.replace("{$k}", v) } | ||
} | ||
} | ||
} | ||
|
||
private fun mergePartialAndUserVariables( | ||
variables: Map<String, String>, | ||
inputVariables: List<String> | ||
): Map<String, String> = | ||
inputVariables.fold(variables) { acc, k -> | ||
if (!acc.containsKey(k)) acc + (k to "{$k}") else acc | ||
} | ||
} | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
src/commonMain/kotlin/com/xebia/functional/prompt/models.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package com.xebia.functional.prompt | ||
|
||
import arrow.core.Either | ||
import arrow.core.NonEmptyList | ||
import arrow.core.raise.Raise | ||
import arrow.core.raise.either | ||
import arrow.core.raise.ensure | ||
import arrow.core.raise.zipOrAccumulate | ||
|
||
enum class TemplateFormat { | ||
FString | ||
} | ||
|
||
data class InvalidTemplate(val reason: String) | ||
|
||
fun Raise<InvalidTemplate>.Config(template: String, inputVariables: List<String>): Config = | ||
Config.either(template, inputVariables).bind() | ||
|
||
class Config private constructor( | ||
val inputVariables: List<String>, | ||
val template: String, | ||
val templateFormat: TemplateFormat = TemplateFormat.FString | ||
) { | ||
companion object { | ||
// We cannot define `operator fun invoke` with `Raise` without context receivers, | ||
// so we define an intermediate `Either` based function. | ||
// This is because adding `Raise<InvalidTemplate>` results in 2 receivers. | ||
fun either(template: String, variables: List<String>): Either<InvalidTemplate, Config> = | ||
either<NonEmptyList<InvalidTemplate>, Config> { | ||
val placeholders = placeholderValues(template) | ||
|
||
zipOrAccumulate( | ||
{ validate(template, variables.toSet() - placeholders.toSet(), "unused") }, | ||
{ validate(template, placeholders.toSet() - variables.toSet(), "missing") }, | ||
{ validateDuplicated(template, placeholders) } | ||
) { _, _, _ -> Config(variables, template) } | ||
}.mapLeft { InvalidTemplate(it.joinToString(transform = InvalidTemplate::reason)) } | ||
} | ||
} | ||
|
||
private fun Raise<InvalidTemplate>.validate(template: String, diffSet: Set<String>, msg: String): Unit = | ||
ensure(diffSet.isEmpty()) { | ||
InvalidTemplate("Template '$template' has $msg arguments: ${diffSet.joinToString(", ") { "{$it}" }}") | ||
} | ||
|
||
private fun Raise<InvalidTemplate>.validateDuplicated(template: String, placeholders: List<String>) { | ||
val args = placeholders.groupBy { it }.filter { it.value.size > 1 }.keys | ||
ensure(args.isEmpty()) { | ||
InvalidTemplate("Template '$template' has duplicate arguments: ${args.joinToString(", ") { "{$it}" }}") | ||
} | ||
} | ||
|
||
private fun placeholderValues(template: String): List<String> { | ||
@Suppress("RegExpRedundantEscape") | ||
val regex = Regex("""\{([^\{\}]+)\}""") | ||
return regex.findAll(template).toList().mapNotNull { it.groupValues.getOrNull(1) } | ||
} |
59 changes: 59 additions & 0 deletions
59
src/commonTest/kotlin/com/xebia/functional/prompt/ConfigSpec.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package com.xebia.functional.prompt | ||
|
||
import arrow.core.raise.either | ||
import io.kotest.assertions.arrow.core.shouldBeLeft | ||
import io.kotest.assertions.arrow.core.shouldBeRight | ||
import io.kotest.core.spec.style.StringSpec | ||
import io.kotest.matchers.shouldBe | ||
|
||
class ConfigSpec : StringSpec({ | ||
|
||
"should return a valid Config if the template and input variables are valid" { | ||
val template = "Hello {name}, you are {age} years old." | ||
val variables = listOf("name", "age") | ||
|
||
val config = either { Config(template, variables) }.shouldBeRight() | ||
|
||
config.inputVariables shouldBe variables | ||
config.template shouldBe template | ||
config.templateFormat shouldBe TemplateFormat.FString | ||
} | ||
|
||
"should fail with a InvalidTemplateError if the template has missing arguments" { | ||
val template = "Hello {name}, you are {age} years old." | ||
val variables = listOf("name") | ||
|
||
either { | ||
Config(template, variables) | ||
} shouldBeLeft InvalidTemplate("Template 'Hello {name}, you are {age} years old.' has missing arguments: {age}") | ||
} | ||
|
||
"should fail with a InvalidTemplateError if the template has unused arguments" { | ||
val template = "Hello {name}, you are {age} years old." | ||
val variables = listOf("name", "age", "unused") | ||
|
||
either { | ||
Config(template, variables) | ||
} shouldBeLeft InvalidTemplate("Template 'Hello {name}, you are {age} years old.' has unused arguments: {unused}") | ||
} | ||
|
||
"should fail with a InvalidTemplateError if there are duplicate input variables" { | ||
val template = "Hello {name}, you are {name} years old." | ||
val variables = listOf("name") | ||
|
||
either { | ||
Config(template, variables) | ||
} shouldBeLeft InvalidTemplate("Template 'Hello {name}, you are {name} years old.' has duplicate arguments: {name}") | ||
} | ||
|
||
"should fail with a combination of InvalidTemplateErrors if there are multiple things wrong" { | ||
val template = "Hello {name}, you are {name} years old." | ||
val variables = listOf("name", "age") | ||
val unused = "Template 'Hello {name}, you are {name} years old.' has unused arguments: {age}" | ||
val duplicated = "Template 'Hello {name}, you are {name} years old.' has duplicate arguments: {name}" | ||
|
||
either { | ||
Config(template, variables) | ||
} shouldBeLeft InvalidTemplate("$unused, $duplicated") | ||
} | ||
}) |
Oops, something went wrong.