From 312a60a3c93f1ca84c0424cd1f5c0703be86484f Mon Sep 17 00:00:00 2001 From: routis <> Date: Fri, 5 Jan 2024 14:49:37 +0200 Subject: [PATCH 1/3] Added to parser orElse() & orElseEither() --- .../com/sksamuel/tribune/core/parser.kt | 27 +++++- .../com/sksamuel/tribune/core/ParserTest.kt | 24 +++++- .../com/sksamuel/tribune/core/SealedTests.kt | 82 +++++++++++++++++++ 3 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 tribune-core/src/test/kotlin/com/sksamuel/tribune/core/SealedTests.kt diff --git a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt index 0654e99..39b2455 100644 --- a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt +++ b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt @@ -1,7 +1,6 @@ package com.sksamuel.tribune.core -import arrow.core.EitherNel -import arrow.core.right +import arrow.core.* /** * A [Parser] is a function I => [EitherNel] that parses the input I, returing either @@ -50,4 +49,28 @@ fun interface Parser { fun contramap(f: (J) -> I): Parser = Parser { parse(f(it)) } + } + +/** + * Returns a new parser that first tries to parse the input with [this] and if it fails tries the [other]. + * If both parsers fail, errors are being accumulated + * The two parsers have outputs [O] and [O2] that are related. In particalur, [O2] is supertype of [O] + */ +fun Parser.orElse(other: Parser): Parser = + Parser { i -> + parse(i).fold( + ifRight = { it.right() }, + ifLeft = { es -> other.parse(i).mapLeft { es2 -> (es as NonEmptyList) + es2 }} + ) + } + +/** + * Returns a new parser that first tries to parse the input with [this] and if it fails tries the [other]. + * The outputs of the two parsers, ([O] and [O2]) don't have to be related. + * In case the first parser succeeds a left [O] is being returned + * In case the second parser succeeds a ritht [O2] is being returned + * If both parsers fail, errors are being accumulated + */ +fun Parser.orElseEither(other: Parser): Parser, E2> = + this.map { it.left() }.orElse( other.map { it.right() }) \ No newline at end of file diff --git a/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/ParserTest.kt b/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/ParserTest.kt index 4a431d7..4b8ea22 100644 --- a/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/ParserTest.kt +++ b/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/ParserTest.kt @@ -1,8 +1,6 @@ package com.sksamuel.tribune.core -import arrow.core.EitherNel -import arrow.core.leftNel -import arrow.core.right +import arrow.core.* import com.sksamuel.tribune.core.floats.float import com.sksamuel.tribune.core.ints.int import com.sksamuel.tribune.core.longs.long @@ -92,5 +90,25 @@ class ValidatedTest : FunSpec() { val parser: Parser, String> = Parser.zip(nameParser, ageParser) val result = parser.parse(UpdateRequest("a", "b")) } + + test("orElseEither Parser") { + data class ValidName(val name: String) + data class ValidAge(val age: Int) + val nameParser: Parser = Parser() + .notNull { "Name cannot be blank" } + .minlen(6) { "Name must have 6 characters" } + .map { ValidName(it) } + + val ageParser: Parser = Parser() + .notNull { "Age cannot be null" } + .int { "Age must be a number" } + .map { ValidAge(it) } + + val parser = nameParser.orElseEither(ageParser) + + parser.parse("Just a name") shouldBe ValidName("Just a name").left().right() + parser.parse("18") shouldBe ValidAge(18).right().right() + parser.parse("a") shouldBe nonEmptyListOf("Name must have 6 characters", "Age must be a number").left() + } } } diff --git a/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/SealedTests.kt b/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/SealedTests.kt new file mode 100644 index 0000000..86f1b73 --- /dev/null +++ b/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/SealedTests.kt @@ -0,0 +1,82 @@ +package com.sksamuel.tribune.core + +import arrow.core.* +import com.sksamuel.tribune.core.strings.match +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +sealed interface ProductCodeError { + data object NotWidgetCode : ProductCodeError + data object NotGizmoCode : ProductCodeError +} + +sealed interface ProductCode { + val value: String + + @JvmInline + value class GizmoCode private constructor(override val value: String) : ProductCode { + companion object { + private const val GIZMO_PATTERN = "\\AG\\d{3}\\z" + operator fun invoke(s: String): EitherNel = parser.parse(s) + + val parser: Parser by lazy { + Parser() + .match(Regex(GIZMO_PATTERN)) { ProductCodeError.NotGizmoCode } + .map { GizmoCode(it) } + } + } + } + + @JvmInline + value class WidgetCode private constructor(override val value: String) : ProductCode { + companion object { + private const val WIDGET_PATTERN = "\\AW\\d{4}\\z" + operator fun invoke(s: String): EitherNel = parser.parse(s) + + val parser: Parser by lazy { + Parser() + .match(Regex(WIDGET_PATTERN)) { ProductCodeError.NotWidgetCode } + .map { WidgetCode(it) } + } + } + } + + companion object { + operator fun invoke(s: String): EitherNel = parser.parse(s) + + val parser: Parser = GizmoCode.parser.orElse(WidgetCode.parser) + } + + +} + +class SealedTests : FunSpec() { + init { + test("GizmoCode success") { + val s = "G123" + ProductCode.GizmoCode(s).map { it.value } shouldBe s.right() + } + + test("GizmoCode failure") { + ProductCode.GizmoCode("12342") shouldBe ProductCodeError.NotGizmoCode.nel().left() + } + + test("WidgetCode success") { + val s = "W1234" + ProductCode.WidgetCode(s).map { it.value } shouldBe s.right() + } + + test("WidgetCode failure") { + ProductCode.WidgetCode("12342") shouldBe ProductCodeError.NotWidgetCode.nel().left() + } + + test("ProductCode success") { + ProductCode("W1234") shouldBe ProductCode.WidgetCode("W1234") + ProductCode("G123") shouldBe ProductCode.GizmoCode("G123") + } + + test("ProductCode failure") { + ProductCode("1234") shouldBe nonEmptyListOf(ProductCodeError.NotGizmoCode, ProductCodeError.NotWidgetCode).left() + } + } +} \ No newline at end of file From 568ecb298576fff6174e833c36173f0107186116 Mon Sep 17 00:00:00 2001 From: routis <> Date: Fri, 5 Jan 2024 15:56:25 +0200 Subject: [PATCH 2/3] Typos fixed --- .../main/kotlin/com/sksamuel/tribune/core/parser.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt index 39b2455..66f9cf8 100644 --- a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt +++ b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt @@ -55,13 +55,14 @@ fun interface Parser { /** * Returns a new parser that first tries to parse the input with [this] and if it fails tries the [other]. * If both parsers fail, errors are being accumulated - * The two parsers have outputs [O] and [O2] that are related. In particalur, [O2] is supertype of [O] + * The two parsers have outputs [O] and [O2] that are related. + * In particular, [O2] is supertype of [O] */ fun Parser.orElse(other: Parser): Parser = Parser { i -> parse(i).fold( ifRight = { it.right() }, - ifLeft = { es -> other.parse(i).mapLeft { es2 -> (es as NonEmptyList) + es2 }} + ifLeft = { es -> other.parse(i).mapLeft { es2 -> (es as NonEmptyList) + es2 } } ) } @@ -69,8 +70,8 @@ fun Parser.orElse(other: Parser Parser.orElseEither(other: Parser): Parser, E2> = - this.map { it.left() }.orElse( other.map { it.right() }) \ No newline at end of file +fun Parser.orElseEither(other: Parser): Parser, E2> = + this.map { it.left() }.orElse(other.map { it.right() }) From 32c5c7006db3345aa867406c5ba22e302012ef02 Mon Sep 17 00:00:00 2001 From: routis <> Date: Fri, 5 Jan 2024 20:12:39 +0200 Subject: [PATCH 3/3] Fixed some more typos --- .../src/main/kotlin/com/sksamuel/tribune/core/parser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt index 66f9cf8..12d5405 100644 --- a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt +++ b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt @@ -3,7 +3,7 @@ package com.sksamuel.tribune.core import arrow.core.* /** - * A [Parser] is a function I => [EitherNel] that parses the input I, returing either + * A [Parser] is a function I => [EitherNel] that parses the input I, returning either * an output O or error E. * * It is implemented as an interface to allow for variance on the type parameters. @@ -21,7 +21,7 @@ fun interface Parser { * * Eg: * - * Parser() will return an identity parser that simply returns any intput string. + * Parser() will return an identity parser that simply returns any input string. */ operator fun invoke(): Parser = Parser { it.right() }