diff --git a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/collections/lists.kt b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/collections/lists.kt index fcf38a6..35fbb43 100644 --- a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/collections/lists.kt +++ b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/collections/lists.kt @@ -1,45 +1,34 @@ package com.sksamuel.tribune.core.collections -import arrow.core.leftNel -import arrow.core.sequence +import arrow.core.flatten +import arrow.core.left +import arrow.core.right +import arrow.core.toNonEmptyListOrNull import com.sksamuel.tribune.core.Parser +import com.sksamuel.tribune.core.map /** * Lifts an existing [Parser] to support lists of the input types supported by * the underlying parser. * - * In other words, given a parser from I to A, returns a parser from List to List. + * In other words, given a parser from I to A, returns a parser from Collection to List. * - * @return a parser that accepts lists + * @return a [Parser] that produces sets */ -fun Parser.asList(): Parser, List, E> { +fun Parser.asList(): Parser, List, E> { + val self = this return Parser { input -> - input.map { this@asList.parse(it) }.sequence() + val results = input.map { self.parse(it) } + val lefts: List = results.mapNotNull { it.leftOrNull() }.flatten() + val rights: List = results.mapNotNull { it.getOrNull() } + if (lefts.isNotEmpty()) + lefts.toNonEmptyListOrNull()?.left() ?: error("unknown") + else + rights.right() } } -fun Parser.Companion.list(elementParser: Parser): Parser, List, E> = +fun Parser.Companion.list(elementParser: Parser): Parser, List, E> = elementParser.asList() -/** - * Lifts an existing [Parser] to support lists of the input types supported by - * the underlying parser. This version of repeated supports upper and lower bounds - * on the list size. - * - * In other words, given a parser, this will return a parser that handles lists of the inputs. - * - * @param min the minimum number of elements in the list - * @param max the maximum number of elements in the list - * - * @return a parser that accepts lists - */ -fun Parser.asList( - min: Int = 0, - max: Int = Int.MAX_VALUE, - ifInvalidSize: (Int) -> E -): Parser, List, E> { - return Parser { input -> - if ((min..max).contains(input.size)) input.map { this@asList.parse(it) }.sequence() - else ifInvalidSize(input.size).leftNel() - } -} +fun Parser, E>.filterNulls(): Parser, E> = map { it.filterNotNull() } diff --git a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/collections/sets.kt b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/collections/sets.kt index 5c1650d..0e0be8a 100644 --- a/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/collections/sets.kt +++ b/tribune-core/src/main/kotlin/com/sksamuel/tribune/core/collections/sets.kt @@ -1,7 +1,11 @@ package com.sksamuel.tribune.core.collections -import arrow.core.sequence +import arrow.core.flatten +import arrow.core.left +import arrow.core.right +import arrow.core.toNonEmptyListOrNull import com.sksamuel.tribune.core.Parser +import com.sksamuel.tribune.core.map /** * Lifts an existing [Parser] to support sets of the input types supported by @@ -11,26 +15,20 @@ import com.sksamuel.tribune.core.Parser * * @return a [Parser] that produces sets */ -fun Parser.asSet(): Parser, Set, E> { +fun Parser.asSet(): Parser, Set, E> { + val self = this return Parser { input -> - input.map { this@asSet.parse(it) }.sequence().map { it.toSet() } + val results = input.map { self.parse(it) } + val lefts: List = results.mapNotNull { it.leftOrNull() }.flatten() + val rights: List = results.mapNotNull { it.getOrNull() } + if (lefts.isNotEmpty()) + lefts.toNonEmptyListOrNull()?.left() ?: error("unknown") + else + rights.toSet().right() } } -/** - * Lifts an existing [Parser] I => A? to support sets of the input types supported by - * the underlying parser. Any nulls produced by the underlying parser will be filtered out - * without erroring. - * - * In other words, given a parser from I to A?, returns a parser from Collection to Set. - * - * @return a [Parser] that produces sets - */ -fun Parser.asSetFilterNulls(): Parser, Set, E> { - return Parser { input -> - input.map { this@asSetFilterNulls.parse(it) }.sequence().map { it.filterNotNull().toSet() } - } -} +fun Parser, E>.filterNulls(): Parser, E> = map { it.filterNotNull().toSet() } -fun Parser.Companion.set(elementParser: Parser): Parser, Set, E> = +fun Parser.Companion.set(elementParser: Parser): Parser, Set, E> = elementParser.asSet() 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 a837946..0654e99 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 @@ -5,7 +5,7 @@ import arrow.core.right /** * A [Parser] is a function I => [EitherNel] that parses the input I, returing either - * an output O or an error E. + * an output O or error E. * * It is implemented as an interface to allow for variance on the type parameters. */ diff --git a/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/collections/ListTest.kt b/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/collections/ListTest.kt index 9b4dd0c..bba797f 100644 --- a/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/collections/ListTest.kt +++ b/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/collections/ListTest.kt @@ -1,25 +1,33 @@ package com.sksamuel.tribune.core.collections -import arrow.core.leftNel import arrow.core.right -import com.sksamuel.tribune.core.Foo import com.sksamuel.tribune.core.Parser import com.sksamuel.tribune.core.map +import com.sksamuel.tribune.core.strings.minlen import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe class ListTest : FunSpec() { init { + data class ParsedString(val str: String) + test("asList") { - val ps: Parser, List, Nothing> = Parser().map { Foo(it) }.asList() - ps.parse(listOf("a", "b")) shouldBe listOf(Foo("a"), Foo("b")).right() + val p = Parser().map { ParsedString(it) } + val plist: Parser, List, Nothing> = p.asList() + plist.parse(listOf("a", "b")).getOrNull() shouldBe listOf(ParsedString("a"), ParsedString("b")) + } + + test("asList should accumulate errors") { + val p = Parser().minlen(2) { "whack $it" }.map { ParsedString(it) } + val plist: Parser, List, String> = p.asList() + plist.parse(listOf("a", "b")).leftOrNull() shouldBe listOf("whack a", "whack b") } - test("asList with min length") { - val ps = Parser().map { Foo(it) }.asList(min = 2) { "Must have at least two elements" } - ps.parse(listOf("a", "b")) shouldBe listOf(Foo("a"), Foo("b")).right() - ps.parse(listOf("a")) shouldBe "Must have at least two elements".leftNel() + test("filterNulls") { + val p = Parser().map { if (it == "a") null else ParsedString(it) } + val plist: Parser, List, String> = p.asList().filterNulls() + plist.parse(listOf("a", "b")).getOrNull() shouldBe listOf(ParsedString("b")) } } } diff --git a/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/collections/SetTest.kt b/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/collections/SetTest.kt new file mode 100644 index 0000000..43e68c0 --- /dev/null +++ b/tribune-core/src/test/kotlin/com/sksamuel/tribune/core/collections/SetTest.kt @@ -0,0 +1,32 @@ +package com.sksamuel.tribune.core.collections + +import com.sksamuel.tribune.core.Parser +import com.sksamuel.tribune.core.map +import com.sksamuel.tribune.core.strings.minlen +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +class SetTest : FunSpec() { + init { + + data class ParsedString(val str: String) + + test("asSet") { + val p = Parser().map { ParsedString(it) } + val pset: Parser, Set, Nothing> = p.asSet() + pset.parse(listOf("a", "b")).getOrNull() shouldBe setOf(ParsedString("a"), ParsedString("b")) + } + + test("asSet should accumulate errors") { + val p = Parser().minlen(2) { "whack $it" }.map { ParsedString(it) } + val pset: Parser, Set, String> = p.asSet() + pset.parse(listOf("a", "b")).leftOrNull() shouldBe listOf("whack a", "whack b") + } + + test("filterNulls") { + val p = Parser().map { if (it == "a") null else ParsedString(it) } + val pset: Parser, Set, String> = p.asSet().filterNulls() + pset.parse(listOf("a", "b")).getOrNull() shouldBe setOf(ParsedString("b")) + } + } +}