Skip to content

Commit

Permalink
Updated list and set handling
Browse files Browse the repository at this point in the history
  • Loading branch information
sksamuel committed Dec 30, 2023
1 parent 8f61287 commit 3a9d930
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -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<I> to List<A>.
* In other words, given a parser from I to A, returns a parser from Collection<I> to List<A>.
*
* @return a parser that accepts lists
* @return a [Parser] that produces sets
*/
fun <I, A, E> Parser<I, A, E>.asList(): Parser<Collection<I>, List<A>, E> {
fun <I, O, E> Parser<I, O, E>.asList(): Parser<Collection<I>, List<O>, E> {
val self = this
return Parser { input ->
input.map { this@asList.parse(it) }.sequence()
val results = input.map { self.parse(it) }
val lefts: List<E> = results.mapNotNull { it.leftOrNull() }.flatten()
val rights: List<O> = results.mapNotNull { it.getOrNull() }
if (lefts.isNotEmpty())
lefts.toNonEmptyListOrNull()?.left() ?: error("unknown")
else
rights.right()
}
}

fun <I, A, E> Parser.Companion.list(elementParser: Parser<I, A, E>): Parser<Collection<I>, List<A>, E> =
fun <I, O, E> Parser.Companion.list(elementParser: Parser<I, O, E>): Parser<Collection<I>, List<O>, 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 <I, A, E> Parser<I, A, E>.asList(
min: Int = 0,
max: Int = Int.MAX_VALUE,
ifInvalidSize: (Int) -> E
): Parser<List<I>, List<A>, E> {
return Parser { input ->
if ((min..max).contains(input.size)) input.map { this@asList.parse(it) }.sequence()
else ifInvalidSize(input.size).leftNel()
}
}
fun <I, O, E> Parser<I, List<O?>, E>.filterNulls(): Parser<I, List<O>, E> = map { it.filterNotNull() }
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,26 +15,20 @@ import com.sksamuel.tribune.core.Parser
*
* @return a [Parser] that produces sets
*/
fun <I, A, E> Parser<I, A, E>.asSet(): Parser<Collection<I>, Set<A>, E> {
fun <I, O, E> Parser<I, O, E>.asSet(): Parser<Collection<I>, Set<O>, 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<E> = results.mapNotNull { it.leftOrNull() }.flatten()
val rights: List<O> = 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<I> to Set<A>.
*
* @return a [Parser] that produces sets
*/
fun <I, A, E> Parser<I, A?, E>.asSetFilterNulls(): Parser<Collection<I>, Set<A>, E> {
return Parser { input ->
input.map { this@asSetFilterNulls.parse(it) }.sequence().map { it.filterNotNull().toSet() }
}
}
fun <I, O, E> Parser<I, Set<O?>, E>.filterNulls(): Parser<I, Set<O>, E> = map { it.filterNotNull().toSet() }

fun <I, A, E> Parser.Companion.set(elementParser: Parser<I, A, E>): Parser<Collection<I>, Set<A>, E> =
fun <I, O, E> Parser.Companion.set(elementParser: Parser<I, O, E>): Parser<Collection<I>, Set<O>, E> =
elementParser.asSet()
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>, List<Foo>, Nothing> = Parser<String>().map { Foo(it) }.asList()
ps.parse(listOf("a", "b")) shouldBe listOf(Foo("a"), Foo("b")).right()
val p = Parser<String>().map { ParsedString(it) }
val plist: Parser<Collection<String>, List<ParsedString>, Nothing> = p.asList()
plist.parse(listOf("a", "b")).getOrNull() shouldBe listOf(ParsedString("a"), ParsedString("b"))
}

test("asList should accumulate errors") {
val p = Parser<String>().minlen(2) { "whack $it" }.map { ParsedString(it) }
val plist: Parser<Collection<String>, List<ParsedString>, String> = p.asList()
plist.parse(listOf("a", "b")).leftOrNull() shouldBe listOf("whack a", "whack b")
}

test("asList with min length") {
val ps = Parser<String>().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<String>().map { if (it == "a") null else ParsedString(it) }
val plist: Parser<Collection<String>, List<ParsedString>, String> = p.asList().filterNulls()
plist.parse(listOf("a", "b")).getOrNull() shouldBe listOf(ParsedString("b"))
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String>().map { ParsedString(it) }
val pset: Parser<Collection<String>, Set<ParsedString>, Nothing> = p.asSet()
pset.parse(listOf("a", "b")).getOrNull() shouldBe setOf(ParsedString("a"), ParsedString("b"))
}

test("asSet should accumulate errors") {
val p = Parser<String>().minlen(2) { "whack $it" }.map { ParsedString(it) }
val pset: Parser<Collection<String>, Set<ParsedString>, String> = p.asSet()
pset.parse(listOf("a", "b")).leftOrNull() shouldBe listOf("whack a", "whack b")
}

test("filterNulls") {
val p = Parser<String>().map { if (it == "a") null else ParsedString(it) }
val pset: Parser<Collection<String>, Set<ParsedString>, String> = p.asSet().filterNulls()
pset.parse(listOf("a", "b")).getOrNull() shouldBe setOf(ParsedString("b"))
}
}
}

0 comments on commit 3a9d930

Please sign in to comment.