Skip to content

Commit

Permalink
Merge pull request #23 from routis/feature/add-orElse
Browse files Browse the repository at this point in the history
Add to `Parser` functions `orElse()` & `orElseEither()`
  • Loading branch information
sksamuel authored Jan 7, 2024
2 parents b5b0f9c + 32c5c70 commit 2d7ef6e
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 7 deletions.
32 changes: 28 additions & 4 deletions tribune-core/src/main/kotlin/com/sksamuel/tribune/core/parser.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
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
* 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.
Expand All @@ -22,7 +21,7 @@ fun interface Parser<in I, out O, out E> {
*
* Eg:
*
* Parser<String>() will return an identity parser that simply returns any intput string.
* Parser<String>() will return an identity parser that simply returns any input string.
*/
operator fun <I> invoke(): Parser<I, I, Nothing> = Parser { it.right() }

Expand Down Expand Up @@ -50,4 +49,29 @@ fun interface Parser<in I, out O, out E> {
fun <J> contramap(f: (J) -> I): Parser<J, O, E> =
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 particular, [O2] is supertype of [O]
*/
fun <I, E : E2, O : O2, I2 : I, E2, O2> Parser<I, O, E>.orElse(other: Parser<I2, O2, E2>): Parser<I2, O2, E2> =
Parser { i ->
parse(i).fold(
ifRight = { it.right() },
ifLeft = { es -> other.parse(i).mapLeft { es2 -> (es as NonEmptyList<E2>) + 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 right [O2] is being returned
* If both parsers fail, errors are being accumulated
*/
fun <I, E : E2, O, I2 : I, E2, O2> Parser<I, O, E>.orElseEither(other: Parser<I2, O2, E2>): Parser<I2, Either<O, O2>, E2> =
this.map { it.left() }.orElse(other.map { it.right() })
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -92,5 +90,25 @@ class ValidatedTest : FunSpec() {
val parser: Parser<UpdateRequest, Pair<ValidName, ValidAge>, 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<String, ValidName, String> = Parser<String>()
.notNull { "Name cannot be blank" }
.minlen(6) { "Name must have 6 characters" }
.map { ValidName(it) }

val ageParser: Parser<String, ValidAge, String> = Parser<String>()
.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()
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ProductCodeError.NotGizmoCode, GizmoCode> = parser.parse(s)

val parser: Parser<String, GizmoCode, ProductCodeError.NotGizmoCode> by lazy {
Parser<String>()
.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<ProductCodeError.NotWidgetCode, WidgetCode> = parser.parse(s)

val parser: Parser<String, WidgetCode, ProductCodeError.NotWidgetCode> by lazy {
Parser<String>()
.match(Regex(WIDGET_PATTERN)) { ProductCodeError.NotWidgetCode }
.map { WidgetCode(it) }
}
}
}

companion object {
operator fun invoke(s: String): EitherNel<ProductCodeError, ProductCode> = parser.parse(s)

val parser: Parser<String, ProductCode, ProductCodeError> = 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()
}
}
}

0 comments on commit 2d7ef6e

Please sign in to comment.