From 3faf9458c678950f1fab3704250f0fb554a29b0f Mon Sep 17 00:00:00 2001 From: Jamie Willis Date: Tue, 7 Feb 2023 00:11:50 +0000 Subject: [PATCH] Combinators that return `cats.data.NonEmptyList` (#5) * Added combinator module with non-empty versions of the regular combinators * Added full qualification for combinator * Added documentation for combinators * Module documentation * Fixed another qualification * Fixed imports * Fixed copyright headers and imports (though this looks like a scalastyle bug... * Used irrefutable match, it's just cleaner * Added tests --- .../parsley/cats/MonoidKForParsley.scala | 3 +- .../parsley/cats/MonoidKForParsley.scala | 3 +- .../parsley/cats/ApplicativeForParsley.scala | 9 +- .../scala/parsley/cats/DeferForParsley.scala | 4 +- .../parsley/cats/FunctorForParsley.scala | 4 +- .../scala/parsley/cats/MonadForParsley.scala | 15 +- .../main/scala/parsley/cats/combinator.scala | 159 ++++++++++++++++++ .../main/scala/parsley/cats/instances.scala | 3 + .../src/test/scala/parsley/ParsleyTest.scala | 101 +++++++++++ .../scala/parsley/cats/CombinatorTests.scala | 63 +++++++ 10 files changed, 343 insertions(+), 21 deletions(-) create mode 100644 parsley-cats/shared/src/main/scala/parsley/cats/combinator.scala create mode 100644 parsley-cats/shared/src/test/scala/parsley/ParsleyTest.scala create mode 100644 parsley-cats/shared/src/test/scala/parsley/cats/CombinatorTests.scala diff --git a/parsley-cats/shared/src/main/scala-2.12/parsley/cats/MonoidKForParsley.scala b/parsley-cats/shared/src/main/scala-2.12/parsley/cats/MonoidKForParsley.scala index 6a61813..43c306d 100644 --- a/parsley-cats/shared/src/main/scala-2.12/parsley/cats/MonoidKForParsley.scala +++ b/parsley-cats/shared/src/main/scala-2.12/parsley/cats/MonoidKForParsley.scala @@ -4,7 +4,6 @@ package parsley.cats import parsley.Parsley -import parsley.combinator import cats.{Functor, MonoidK} @@ -15,6 +14,6 @@ private [parsley] trait MonoidKForParsley extends MonoidK[Parsley] { // MonoidK Overrides override def sum[A, B](mx: Parsley[A], my: Parsley[B])(implicit F: Functor[Parsley]): Parsley[Either[A,B]] = mx <+> my - override def combineAllK[A](ps: TraversableOnce[Parsley[A]]): Parsley[A] = combinator.choice(ps.toIterator.toSeq: _*) + override def combineAllK[A](ps: TraversableOnce[Parsley[A]]): Parsley[A] = parsley.combinator.choice(ps.toIterator.toSeq: _*) override def combineAllOptionK[A](ps: TraversableOnce[Parsley[A]]): Option[Parsley[A]] = ps.toIterator.reduceRightOption(_<|>_) } diff --git a/parsley-cats/shared/src/main/scala-2.13+/parsley/cats/MonoidKForParsley.scala b/parsley-cats/shared/src/main/scala-2.13+/parsley/cats/MonoidKForParsley.scala index 721ec96..ebac5f0 100644 --- a/parsley-cats/shared/src/main/scala-2.13+/parsley/cats/MonoidKForParsley.scala +++ b/parsley-cats/shared/src/main/scala-2.13+/parsley/cats/MonoidKForParsley.scala @@ -4,7 +4,6 @@ package parsley.cats import parsley.Parsley -import parsley.combinator import cats.{Functor, MonoidK} @@ -15,6 +14,6 @@ private [parsley] trait MonoidKForParsley extends MonoidK[Parsley] { // MonoidK Overrides override def sum[A, B](mx: Parsley[A], my: Parsley[B])(implicit F: Functor[Parsley]): Parsley[Either[A,B]] = mx <+> my - override def combineAllK[A](ps: IterableOnce[Parsley[A]]): Parsley[A] = combinator.choice(ps.iterator.toSeq: _*) + override def combineAllK[A](ps: IterableOnce[Parsley[A]]): Parsley[A] = parsley.combinator.choice(ps.iterator.toSeq: _*) override def combineAllOptionK[A](ps: IterableOnce[Parsley[A]]): Option[Parsley[A]] = ps.iterator.reduceRightOption(_<|>_) } diff --git a/parsley-cats/shared/src/main/scala/parsley/cats/ApplicativeForParsley.scala b/parsley-cats/shared/src/main/scala/parsley/cats/ApplicativeForParsley.scala index 0cfbbe6..3b37021 100644 --- a/parsley-cats/shared/src/main/scala/parsley/cats/ApplicativeForParsley.scala +++ b/parsley-cats/shared/src/main/scala/parsley/cats/ApplicativeForParsley.scala @@ -3,12 +3,11 @@ */ package parsley.cats +import cats.Applicative + import parsley.Parsley -import parsley.combinator import parsley.lift._ -import cats.Applicative - private [parsley] trait ApplicativeForParsley extends Applicative[Parsley] { override def pure[A](x: A): Parsley[A] = Parsley.pure(x) override def unit: Parsley[Unit] = Parsley.unit @@ -20,8 +19,8 @@ private [parsley] trait ApplicativeForParsley extends Applicative[Parsley] { override def product[A, B](mx: Parsley[A], my: Parsley[B]): Parsley[(A, B)] = mx <~> my override def ap[A, B](mf: Parsley[A => B])(mx: Parsley[A]): Parsley[B] = mf <*> mx - override def replicateA[A](n: Int, mx: Parsley[A]): Parsley[List[A]] = combinator.exactly(n, mx) - override def replicateA_[A](n: Int, mx: Parsley[A]): Parsley[Unit] = combinator.skip(mx, (1 until n).map(_ => mx): _*) + override def replicateA[A](n: Int, mx: Parsley[A]): Parsley[List[A]] = parsley.combinator.exactly(n, mx) + override def replicateA_[A](n: Int, mx: Parsley[A]): Parsley[Unit] = parsley.combinator.skip(mx, (1 until n).map(_ => mx): _*) // Maps and Tuples override def map2[A, B, Z](mx: Parsley[A], my: Parsley[B])(f: (A, B) => Z): Parsley[Z] = lift2(f, mx, my) diff --git a/parsley-cats/shared/src/main/scala/parsley/cats/DeferForParsley.scala b/parsley-cats/shared/src/main/scala/parsley/cats/DeferForParsley.scala index 3e0f8dd..0fedcd5 100644 --- a/parsley-cats/shared/src/main/scala/parsley/cats/DeferForParsley.scala +++ b/parsley-cats/shared/src/main/scala/parsley/cats/DeferForParsley.scala @@ -3,10 +3,10 @@ */ package parsley.cats -import parsley.Parsley, Parsley.LazyParsley - import cats.Defer +import parsley.Parsley, Parsley.LazyParsley + private [parsley] class DeferForParsley extends Defer[Parsley] { def defer[A](p: =>parsley.Parsley[A]): parsley.Parsley[A] = ~p } diff --git a/parsley-cats/shared/src/main/scala/parsley/cats/FunctorForParsley.scala b/parsley-cats/shared/src/main/scala/parsley/cats/FunctorForParsley.scala index 0d55399..8170a4a 100644 --- a/parsley-cats/shared/src/main/scala/parsley/cats/FunctorForParsley.scala +++ b/parsley-cats/shared/src/main/scala/parsley/cats/FunctorForParsley.scala @@ -3,10 +3,10 @@ */ package parsley.cats -import parsley.Parsley - import cats.Functor +import parsley.Parsley + private [parsley] trait FunctorForParsley extends Functor[Parsley] { override def map[A, B](mx: Parsley[A])(f: A => B): Parsley[B] = mx.map(f) override def as[A, B](mx: Parsley[A], y: B): Parsley[B] = mx #> y diff --git a/parsley-cats/shared/src/main/scala/parsley/cats/MonadForParsley.scala b/parsley-cats/shared/src/main/scala/parsley/cats/MonadForParsley.scala index 5a4fe5f..0520617 100644 --- a/parsley-cats/shared/src/main/scala/parsley/cats/MonadForParsley.scala +++ b/parsley-cats/shared/src/main/scala/parsley/cats/MonadForParsley.scala @@ -4,7 +4,6 @@ package parsley.cats import parsley.Parsley -import parsley.combinator import parsley.registers.{RegisterMaker, RegisterMethods} import cats.{Alternative, Monad} @@ -18,11 +17,11 @@ private [parsley] trait MonadForParsley extends Monad[Parsley] { } // Monad Overrides - override def ifM[B](mx: Parsley[Boolean])(ifTrue: => Parsley[B], ifFalse: => Parsley[B]): Parsley[B] = combinator.ifP(mx, ifTrue, ifFalse) + override def ifM[B](mx: Parsley[Boolean])(ifTrue: => Parsley[B], ifFalse: => Parsley[B]): Parsley[B] = parsley.combinator.ifP(mx, ifTrue, ifFalse) override def whileM_[A](p: Parsley[Boolean])(body: =>Parsley[A]): Parsley[Unit] = { - combinator.when(p, combinator.whileP(body ~> p)) + parsley.combinator.when(p, parsley.combinator.whileP(body ~> p)) } - override def untilM_[A](body: Parsley[A])(p: => Parsley[Boolean]): Parsley[Unit] = combinator.whileP(body *> p.map(!_)) + override def untilM_[A](body: Parsley[A])(p: => Parsley[Boolean]): Parsley[Unit] = parsley.combinator.whileP(body *> p.map(!_)) override def whileM[G[_]: Alternative, A](p: Parsley[Boolean])(body: => Parsley[A]): Parsley[G[A]] = { val G = implicitly[Alternative[G]] @@ -39,25 +38,25 @@ private [parsley] trait MonadForParsley extends Monad[Parsley] { } override def untilDefinedM[A](mox: Parsley[Option[A]]): Parsley[A] = { - lazy val loop: Parsley[A] = combinator.decide(mox, loop) + lazy val loop: Parsley[A] = parsley.combinator.decide(mox, loop) loop } override def iterateUntil[A](mx: Parsley[A])(p: A => Boolean): Parsley[A] = { lazy val loop: Parsley[A] = mx.persist { mx => - combinator.ifP(mx.map(p), mx, loop) + parsley.combinator.ifP(mx.map(p), mx, loop) } loop } override def iterateWhile[A](mx: Parsley[A])(p: A => Boolean): Parsley[A] = { lazy val loop: Parsley[A] = mx.persist { mx => - combinator.ifP(mx.map(p), loop, mx) + parsley.combinator.ifP(mx.map(p), loop, mx) } loop } override def ifElseM[A](branches: (Parsley[Boolean], Parsley[A])*)(els: Parsley[A]): Parsley[A] = { branches.foldRight(els) { - case ((cond, t), e) => combinator.ifP(cond, t, e) + case ((cond, t), e) => parsley.combinator.ifP(cond, t, e) } } } diff --git a/parsley-cats/shared/src/main/scala/parsley/cats/combinator.scala b/parsley-cats/shared/src/main/scala/parsley/cats/combinator.scala new file mode 100644 index 0000000..27103a5 --- /dev/null +++ b/parsley-cats/shared/src/main/scala/parsley/cats/combinator.scala @@ -0,0 +1,159 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Cats Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.cats + +import cats.data.NonEmptyList + +import parsley.Parsley, Parsley.notFollowedBy +import parsley.combinator.{many, manyUntil} +import parsley.lift.lift2 + +/** This module contains pre-made combinators that are very useful for a variety of purposes, specialised to `cats`. + * + * In particular, it contains functionality found normally in `parsley.combinator`, but returning the `cats` `NonEmptyList` + * instead of a regular Scala `List`. + * + * @since 1.2.0 + */ +object combinator { + private [cats] def nonEmptyList[A](p: Parsley[A], ps: =>Parsley[List[A]]) = lift2[A, List[A], NonEmptyList[A]](NonEmptyList(_, _), p, ps) + + /** This combinator repeatedly parses a given parser '''one''' or more times, collecting the results into a list. + * + * Parses a given parser, `p`, repeatedly until it fails. If `p` failed having consumed input, + * this combinator fails. Otherwise when `p` fails '''without consuming input''', this combinator + * will return all of the results, `x,,1,,` through `x,,n,,` (with `n >= 1`), in a non-empty list: `NonEmptyList.of(x,,1,,, .., x,,n,,)`. + * If `p` was not successful at least one time, this combinator fails. + * + * @example {{{ + * scala> import parsley.character.string + * scala> import parsley.combinator.some + * scala> val p = some(string("ab")) + * scala> p.parse("") + * val res0 = Failure(..) + * scala> p.parse("ab") + * val res1 = Success(NonEmptyList.of("ab")) + * scala> p.parse("abababab") + * val res2 = Success(NonEmptyList.of("ab", "ab", "ab", "ab")) + * scala> p.parse("aba") + * val res3 = Failure(..) + * }}} + * + * @param p the parser to execute multiple times. + * @return a parser that parses `p` until it fails, returning the non-empty list of all the successful results. + * @since 1.2.0 + */ + def some[A](p: Parsley[A]): Parsley[NonEmptyList[A]] = nonEmptyList(p, many(p)) + + /** This combinator repeatedly parses a given parser '''one''' or more times, until the `end` parser succeeds, collecting the results into a list. + * + * First ensures that trying to parse `end` fails, then tries to parse `p`. If it succeed then it will repeatedly: try to parse `end`, if it fails + * '''without consuming input''', then parses `p`, which must succeed. When `end` does succeed, this combinator will return all of the results + * generated by `p`, `x,,1,,` through `x,,n,,` (with `n >= 1`), in a non-empty list: `NonEmptyList.of(x,,1,,, .., x,,n,,)`. The parser `p` must succeed + * at least once before `end` succeeds. + * + * @example This can be useful for scanning comments: {{{ + * scala> import parsley.character.{string, item, endOfLine} + * scala> import parsley.combinator.many + * scala> val comment = string("//") *> someUntil(item, endOfLine) + * scala> p.parse("//hello world") + * val res0 = Failure(..) + * scala> p.parse("//hello world\n") + * val res1 = Success(NonEmptyList.of('h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd')) + * scala> p.parse("//\n") + * val res2 = Failure(..) + * scala> p.parse("//a\n") + * val res3 = Success(NonEmptyList.of('a')) + * }}} + * + * @param p the parser to execute multiple times. + * @param end the parser that stops the parsing of `p`. + * @return a parser that parses `p` until `end` succeeds, returning the non-empty list of all the successful results. + * @since 1.2.0 + */ + def someUntil[A](p: Parsley[A], end: Parsley[_]): Parsley[NonEmptyList[A]] = notFollowedBy(end) *> (nonEmptyList(p, manyUntil(p, end))) + + /** This combinator parses '''one''' or more occurrences of `p`, separated by `sep`. + * + * First parses a `p`. Then parses `sep` followed by `p` until there are no more `sep`s. + * The results of the `p`'s, `x,,1,,` through `x,,n,,`, are returned as `NonEmptyList.of(x,,1,,, .., x,,n,,)`. + * If `p` or `sep` fails having consumed input, the whole parser fails. Requires at least + * one `p` to have been parsed. + * + * @example {{{ + * scala> ... + * scala> val args = sepBy1(int, string(", ")) + * scala> args.parse("7, 3, 2") + * val res0 = Success(NonEmptyList.of(7, 3, 2)) + * scala> args.parse("") + * val res1 = Failure(..) + * scala> args.parse("1") + * val res2 = Success(NonEmptyList.of(1)) + * scala> args.parse("1, 2, ") + * val res3 = Failure(..) // no trailing comma allowed + * }}} + * + * @param p the parser whose results are collected into a list. + * @param sep the delimiter that must be parsed between every `p`. + * @return a parser that parses `p` delimited by `sep`, returning the non-empty list of `p`'s results. + * @since 1.2.0 + */ + def sepBy1[A](p: Parsley[A], sep: =>Parsley[_]): Parsley[NonEmptyList[A]] = nonEmptyList(p, many(sep *> p)) + + /** This combinator parses '''one''' or more occurrences of `p`, separated and optionally ended by `sep`. + * + * First parses a `p`. Then parses `sep` followed by `p` until there are no more: if a final `sep` exists, this is parsed. + * The results of the `p`'s, `x,,1,,` through `x,,n,,`, are returned as `NonEmptyList.of(x,,1,,, .., x,,n,,)`. + * If `p` or `sep` fails having consumed input, the whole parser fails. Requires at least + * one `p` to have been parsed. + * + * @example {{{ + * scala> ... + * scala> val args = sepEndBy1(int, string(";\n")) + * scala> args.parse("7;\n3;\n2") + * val res0 = Success(NonEmptyList.of(7, 3, 2)) + * scala> args.parse("") + * val res1 = Failure(..) + * scala> args.parse("1") + * val res2 = Success(NonEmptyList.of(1)) + * scala> args.parse("1;\n2;\n") + * val res3 = Success(NonEmptyList.of(1, 2)) + * }}} + * + * @param p the parser whose results are collected into a list. + * @param sep the delimiter that must be parsed between every `p`. + * @return a parser that parses `p` delimited by `sep`, returning the non-empty list of `p`'s results. + * @since 1.2.0 + */ + def sepEndBy1[A](p: Parsley[A], sep: =>Parsley[_]): Parsley[NonEmptyList[A]] = parsley.combinator.sepEndBy1(p, sep).map { xxs => + val (x::xs) = xxs + NonEmptyList(x, xs) + } + + /** This combinator parses '''one''' or more occurrences of `p`, separated and ended by `sep`. + * + * Parses `p` followed by `sep` one or more times. + * The results of the `p`'s, `x,,1,,` through `x,,n,,`, are returned as `NonEmptyList.of(x,,1,,, .., x,,n,,)`. + * If `p` or `sep` fails having consumed input, the whole parser fails. + * + * @example {{{ + * scala> ... + * scala> val args = endBy1(int, string(";\n")) + * scala> args.parse("7;\n3;\n2") + * val res0 = Failure(..) + * scala> args.parse("") + * val res1 = Failure(..) + * scala> args.parse("1;\n") + * val res2 = Success(NonEmptyList.of(1)) + * scala> args.parse("1;\n2;\n") + * val res3 = Success(NonEmptyList.of(1, 2)) + * }}} + * + * @param p the parser whose results are collected into a list. + * @param sep the delimiter that must be parsed between every `p`. + * @return a parser that parses `p` delimited by `sep`, returning the non-empty list of `p`'s results. + * @since 1.2.0 + */ + def endBy1[A](p: Parsley[A], sep: =>Parsley[_]): Parsley[NonEmptyList[A]] = some(p <* sep) +} diff --git a/parsley-cats/shared/src/main/scala/parsley/cats/instances.scala b/parsley-cats/shared/src/main/scala/parsley/cats/instances.scala index 20207fb..48038d5 100644 --- a/parsley-cats/shared/src/main/scala/parsley/cats/instances.scala +++ b/parsley-cats/shared/src/main/scala/parsley/cats/instances.scala @@ -1,3 +1,6 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Cats Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ package parsley.cats import parsley.Parsley diff --git a/parsley-cats/shared/src/test/scala/parsley/ParsleyTest.scala b/parsley-cats/shared/src/test/scala/parsley/ParsleyTest.scala new file mode 100644 index 0000000..6a18f97 --- /dev/null +++ b/parsley-cats/shared/src/test/scala/parsley/ParsleyTest.scala @@ -0,0 +1,101 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley + +import org.scalatest.Assertions +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import parsley.combinator.eof +import parsley.Result +import parsley.errors.{ErrorBuilder, tokenextractors} +import org.scalatest.Inside +import org.scalactic.source.Position + +case class TestError(pos: (Int, Int), lines: TestErrorLines) + +sealed trait TestErrorLines +case class VanillaError(unexpected: Option[TestErrorItem], expecteds: Set[TestErrorItem], reasons: Set[String]) extends TestErrorLines +case class SpecialisedError(msgs: Set[String]) extends TestErrorLines + +sealed trait TestErrorItem +case class Raw(item: String) extends TestErrorItem +case class Named(item: String) extends TestErrorItem +case object EndOfInput extends TestErrorItem + +class TestErrorBuilder extends ErrorBuilder[TestError] with tokenextractors.MatchParserDemand { + override def format(pos: Position, source: Source, lines: ErrorInfoLines): TestError = TestError(pos, lines) + + type Position = (Int, Int) + override def pos(line: Int, col: Int): Position = (line, col) + + type Source = Unit + override def source(sourceName: Option[String]): Source = () + + type ErrorInfoLines = TestErrorLines + override def vanillaError(unexpected: UnexpectedLine, expected: ExpectedLine, reasons: Messages, line: LineInfo): ErrorInfoLines = { + VanillaError(unexpected, expected, reasons) + } + override def specialisedError(msgs: Messages, line: LineInfo): ErrorInfoLines = SpecialisedError(msgs) + + type ExpectedItems = Set[Item] + override def combineExpectedItems(alts: Set[Item]): ExpectedItems = alts + + type Messages = Set[Message] + override def combineMessages(alts: Seq[Message]): Messages = alts.toSet + + type UnexpectedLine = Option[Item] + override def unexpected(item: Option[Item]): UnexpectedLine = item + type ExpectedLine = ExpectedItems + override def expected(alts: ExpectedItems): ExpectedLine = alts + + type Message = String + override def reason(reason: String): Message = reason + override def message(msg: String): Message = msg + + type LineInfo = Unit + override def lineInfo(line: String, linesBefore: Seq[String], linesAfter: Seq[String], errorPointsAt: Int, errorWidth: Int): Unit = () + + override val numLinesBefore: Int = 2 + override val numLinesAfter: Int = 2 + + type Item = TestErrorItem + type Raw = parsley.Raw + type Named = parsley.Named + type EndOfInput = parsley.EndOfInput.type + override def raw(item: String): Raw = Raw(item) + override def named(item: String): Named = Named(item) + override val endOfInput: EndOfInput = EndOfInput +} + +abstract class ParsleyTest extends AnyFlatSpec with Matchers with Assertions with Inside { + val trivialError = Symbol("trivialError") + val expectedEmpty = Symbol("expectedEmpty") + + final def cases[A](p: Parsley[A], noEof: Boolean = false)(tests: (String, Option[A], Position)*): Unit = { + for ((input, res, _pos) <- tests) { + implicit val pos: Position = _pos + res match { + case None if noEof => p.parse(input) shouldBe a [Failure[_]] + case None => p.parseAll(input) shouldBe a [Failure[_]] + case Some(x) if noEof => p.parse(input) shouldBe Success(x) + case Some(x)=> p.parseAll(input) shouldBe Success(x) + } + } + } + + implicit val eb: ErrorBuilder[TestError] = new TestErrorBuilder + + implicit class FullParse[A](val p: Parsley[A]) { + def parseAll[Err: ErrorBuilder](input: String): Result[Err, A] = (p <* eof).parse(input) + } + + implicit class TestCase[A](val x: A) { + def ->[B](xs: B)(implicit pos: Position): (A, B, Position) = (x, xs, pos) + } + + implicit class MultiPair[A](val x: A) { + def -->[B](xs: B*): (A, Seq[B]) = (x, xs) + } +} diff --git a/parsley-cats/shared/src/test/scala/parsley/cats/CombinatorTests.scala b/parsley-cats/shared/src/test/scala/parsley/cats/CombinatorTests.scala new file mode 100644 index 0000000..d2a3995 --- /dev/null +++ b/parsley-cats/shared/src/test/scala/parsley/cats/CombinatorTests.scala @@ -0,0 +1,63 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.cats + +import Predef.{ArrowAssoc => _} + +import parsley.{ParsleyTest, Success, Failure} +import parsley.implicits.character.{charLift, stringLift} +import parsley.cats.combinator._ +import cats.data.NonEmptyList + +class CombinatorTests extends ParsleyTest { + "sepBy1" must "not allow sep at the end of chain" in cases(sepBy1('a', 'b')) ( + "ab" -> None, + ) + it should "be able to parse 2 or more p" in cases(sepBy1('a', 'b')) ( + "aba" -> Some(NonEmptyList.of('a', 'a')), + "ababa" -> Some(NonEmptyList.of('a', 'a', 'a')), + "abababa" -> Some(NonEmptyList.of('a', 'a', 'a', 'a')), + ) + + it must "require a p" in cases(sepBy1('a', 'b')) ( + "a" -> Some(NonEmptyList.of('a')), + "" -> None, + ) + + "sepEndBy1" should "not require sep at the end of chain" in cases(sepEndBy1('a', 'b')) ( + "a" -> Some(NonEmptyList.of('a')) + ) + it should "be able to parse 2 or more p" in cases(sepEndBy1('a', 'b'))( + "aba" -> Some(NonEmptyList.of('a', 'a')), + "ababa" -> Some(NonEmptyList.of('a', 'a', 'a')), + ) + it should "be able to parse a final sep" in cases(sepEndBy1('a', 'b'))( + "ab" -> Some(NonEmptyList.of('a')), + "abab" -> Some(NonEmptyList.of('a', 'a')), + "ababab" -> Some(NonEmptyList.of('a', 'a', 'a')), + ) + it should "fail if p fails after consuming input" in cases(sepEndBy1("aa", 'b')) ( + "ab" -> None, + ) + it should "fail if sep fails after consuming input" in cases(sepEndBy1('a', "bb")) ( + "ab" -> None, + ) + it must "require a p" in { + sepEndBy1('a', 'b').parse("a") should not be a [Failure[_]] + sepEndBy1('a', 'b').parse(input = "") shouldBe a [Failure[_]] + } + + "endBy1" must "require sep at end of chain" in { + endBy1('a', 'b').parse("a") shouldBe a [Failure[_]] + endBy1('a', 'b').parse("ab") should be (Success(NonEmptyList.of('a'))) + } + it should "be able to parse 2 or more p" in { + endBy1('a', 'b').parse("abab") should be (Success(NonEmptyList.of('a', 'a'))) + endBy1('a', 'b').parse("ababab") should be (Success(NonEmptyList.of('a', 'a', 'a'))) + } + it must "require a p" in { + endBy1('a', 'b').parse("ab") should not be a [Failure[_]] + endBy1('a', 'b').parse(input = "") shouldBe a [Failure[_]] + } +}