Skip to content

Commit

Permalink
Merge pull request #350 from alexarchambault/indexed
Browse files Browse the repository at this point in the history
Add Indexed type
  • Loading branch information
alexarchambault authored Dec 10, 2021
2 parents 9077d4c + 0ed84fb commit 92ae2e9
Show file tree
Hide file tree
Showing 30 changed files with 419 additions and 124 deletions.
4 changes: 2 additions & 2 deletions cats/shared/src/main/scala/caseapp/cats/CatsArgParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ object CatsArgParser {
implicit def nonEmptyListArgParser[T](
implicit parser: ArgParser[T]
): AccumulatorArgParser[NonEmptyList[T]] =
AccumulatorArgParser.from(parser.description + "*") { (prevOpt, s) =>
parser(None, s).map { t =>
AccumulatorArgParser.from(parser.description + "*") { (prevOpt, idx, span, s) =>
parser(None, idx, span, s).map { t =>
// inefficient for big lists
prevOpt.fold(NonEmptyList.one(t))(_ :+ t)
}
Expand Down
45 changes: 45 additions & 0 deletions core/shared/src/main/scala/caseapp/core/Indexed.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package caseapp.core

import caseapp.core.argparser.{ArgParser, Consumed}

final case class Indexed[+T](
index: Int,
length: Int,
value: T
)

object Indexed {

def apply[T](value: T): Indexed[T] =
Indexed(-1, 0, value)

def list[T](seq: List[T], startIdx: Int): List[Indexed[T]] =
seq.zipWithIndex.map {
case (elem, idx) =>
Indexed(startIdx + idx, 1, elem)
}

def seq[T](seq: Seq[T], startIdx: Int): Seq[Indexed[T]] =
seq.zipWithIndex.map {
case (elem, idx) =>
Indexed(startIdx + idx, 1, elem)
}

implicit def argParser[T: ArgParser]: ArgParser[Indexed[T]] =
new ArgParser[Indexed[T]] {
private val underlying = ArgParser[T]
def apply(current: Option[Indexed[T]], index: Int, span: Int, value: String) =
underlying(current.map(_.value), index, span, value)
.map(t => Indexed(index, span, t))
override def apply(current: Option[Indexed[T]], index: Int) =
underlying(current.map(_.value), index)
.map(t => Indexed(index, 1, t))
override def optional(current: Option[Indexed[T]], index: Int, span: Int, value: String) = {
val (consumed, res) = underlying.optional(current.map(_.value), index, span, value)
val len = if (consumed.value) span else 1
(consumed, res.map(t => Indexed(index, len, t)))
}
override def isFlag = underlying.isFlag
def description = underlying.description
}
}
7 changes: 5 additions & 2 deletions core/shared/src/main/scala/caseapp/core/RemainingArgs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import dataclass.data
* arguments after a first `--`, if any
*/
@data class RemainingArgs(
remaining: Seq[String],
unparsed: Seq[String]
indexedRemaining: Seq[Indexed[String]],
indexedUnparsed: Seq[Indexed[String]]
) {

def remaining: Seq[String] = indexedRemaining.map(_.value)
def unparsed: Seq[String] = indexedUnparsed.map(_.value)

/** Arguments both before and after a `--`.
*
* The first `--`, if any, is not included in this list.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import dataclass.data

@data class AccumulatorArgParser[T](
description: String,
parse: (Option[T], String) => Either[Error, T]
parse: (Option[T], Int, Int, String) => Either[Error, T]
) extends ArgParser[T] {

def apply(current: Option[T], value: String): Either[Error, T] =
parse(current, value)
def apply(current: Option[T], index: Int, span: Int, value: String): Either[Error, T] =
parse(current, index, span, value)

}

Expand All @@ -18,30 +18,30 @@ object AccumulatorArgParser {
def from[T](
description: String
)(
parse: (Option[T], String) => Either[Error, T]
parse: (Option[T], Int, Int, String) => Either[Error, T]
): AccumulatorArgParser[T] =
AccumulatorArgParser(description, parse)

// FIXME (former comment, deprecated?) may not be fine with sequences/options of flags

def list[T](implicit parser: ArgParser[T]): AccumulatorArgParser[List[T]] =
from(parser.description + "*") { (prevOpt, s) =>
parser(None, s).map { t =>
from(parser.description + "*") { (prevOpt, idx, span, s) =>
parser(None, idx, span, s).map { t =>
// inefficient for big lists
prevOpt.getOrElse(Nil) :+ t
}
}

def vector[T](implicit parser: ArgParser[T]): AccumulatorArgParser[Vector[T]] =
from(parser.description + "*") { (prevOpt, s) =>
parser(None, s).map { t =>
from(parser.description + "*") { (prevOpt, idx, span, s) =>
parser(None, idx, span, s).map { t =>
prevOpt.getOrElse(Vector.empty) :+ t
}
}

def option[T](implicit parser: ArgParser[T]): AccumulatorArgParser[Option[T]] =
from(parser.description + "?") { (prevOpt, s) =>
parser(prevOpt.flatten, s).map(Some(_))
from(parser.description + "?") { (prevOpt, idx, span, s) =>
parser(prevOpt.flatten, idx, span, s).map(Some(_))
}

}
17 changes: 11 additions & 6 deletions core/shared/src/main/scala/caseapp/core/argparser/ArgParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ abstract class ArgParser[T] {
* in case of success, a `T`, wrapped in [[scala.Right]]; else, and error message, wrapped in
* [[caseapp.core.Error]] and [[scala.Left]]
*/
def apply(current: Option[T], value: String): Either[Error, T]
def apply(current: Option[T], index: Int, span: Int, value: String): Either[Error, T]

/** Parses a value.
*
Expand All @@ -39,10 +39,15 @@ abstract class ArgParser[T] {
* in case of success, whether `value` was consumed and a `T`, wrapped in [[scala.Right]];
* else, and error message, wrapped in [[caseapp.core.Error]] and [[scala.Left]]
*/
def optional(current: Option[T], value: String): (Consumed, Either[Error, T]) =
(Consumed(true), apply(current, value))

/** Called when the corresponding argument was specific with no value.
def optional(
current: Option[T],
index: Int,
span: Int,
value: String
): (Consumed, Either[Error, T]) =
(Consumed(true), apply(current, index, span, value))

/** Called when the corresponding argument was specified with no value.
*
* Can happen if the option was enabled as very last argument, like `--bar` in `--foo 1 other
* --bar`.
Expand All @@ -53,7 +58,7 @@ abstract class ArgParser[T] {
* a `T` wrapped in [[scala.Right]] in case of success, or an error message wrapped in
* [[caseapp.core.Error]] and [[scala.Left]] else
*/
def apply(current: Option[T]): Either[Error, T] =
def apply(current: Option[T], index: Int): Either[Error, T] =
Left(Error.ArgumentMissing)

/** Whether the parsed value corresponds to a flag.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@ import dataclass.data

@data class FlagAccumulatorArgParser[T](
description: String,
parse: (Option[T], Option[String]) => Either[Error, T]
parse: (Option[T], Int, Int, Option[String]) => Either[Error, T]
) extends ArgParser[T] {

def apply(current: Option[T], value: String): Either[Error, T] =
parse(current, Some(value))
def apply(current: Option[T], index: Int, span: Int, value: String): Either[Error, T] =
parse(current, index, span, Some(value))

override def optional(current: Option[T], value: String): (Consumed, Either[Error, T]) =
(Consumed(false), parse(current, None))
override def optional(
current: Option[T],
index: Int,
span: Int,
value: String
): (Consumed, Either[Error, T]) =
(Consumed(false), parse(current, index, span, None))

override def apply(current: Option[T]): Either[Error, T] =
parse(current, None)
override def apply(current: Option[T], index: Int): Either[Error, T] =
parse(current, index, 1, None)

override def isFlag: Boolean =
true
Expand All @@ -28,33 +33,33 @@ object FlagAccumulatorArgParser {
def from[T](
description: String
)(
parse: (Option[T], Option[String]) => Either[Error, T]
parse: (Option[T], Int, Int, Option[String]) => Either[Error, T]
): FlagAccumulatorArgParser[T] =
FlagAccumulatorArgParser(description, parse)

val counter: FlagAccumulatorArgParser[Int @@ Counter] =
from("counter") { (prevOpt, _) =>
from("counter") { (prevOpt, _, _, _) =>
Right(Tag.of(prevOpt.fold(0)(Tag.unwrap) + 1))
}

def list[T](implicit parser: ArgParser[T]): FlagAccumulatorArgParser[List[T]] =
from(parser.description + "*") { (prevOpt, s) =>
s.fold(parser(None))(parser(None, _)).map { t =>
from(parser.description + "*") { (prevOpt, idx, span, s) =>
s.fold(parser(None, idx))(parser(None, idx, span, _)).map { t =>
// inefficient for big lists
prevOpt.getOrElse(Nil) :+ t
}
}

def vector[T](implicit parser: ArgParser[T]): FlagAccumulatorArgParser[Vector[T]] =
from(parser.description + "*") { (prevOpt, s) =>
s.fold(parser(None))(parser(None, _)).map { t =>
from(parser.description + "*") { (prevOpt, idx, span, s) =>
s.fold(parser(None, idx))(parser(None, idx, span, _)).map { t =>
prevOpt.getOrElse(Vector.empty) :+ t
}
}

def option[T](implicit parser: ArgParser[T]): FlagAccumulatorArgParser[Option[T]] =
from(parser.description + "?") { (prevOpt, s) =>
s.fold(parser(prevOpt.flatten))(parser(prevOpt.flatten, _))
from(parser.description + "?") { (prevOpt, idx, span, s) =>
s.fold(parser(prevOpt.flatten, idx))(parser(prevOpt.flatten, idx, span, _))
.map(Some(_))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import dataclass.data

@data class FlagArgParser[T](
description: String,
parse: Option[String] => Either[Error, T]
parse: (Option[String], Int, Int) => Either[Error, T]
) extends ArgParser[T] {

def apply(current: Option[T], value: String): Either[Error, T] =
parse(Some(value))
def apply(current: Option[T], index: Int, span: Int, value: String): Either[Error, T] =
parse(Some(value), index, span)

override def optional(current: Option[T], value: String): (Consumed, Either[Error, T]) =
(Consumed(false), parse(None))
override def optional(
current: Option[T],
index: Int,
span: Int,
value: String
): (Consumed, Either[Error, T]) =
(Consumed(false), parse(None, index, span))

override def apply(current: Option[T]): Either[Error, T] =
parse(None)
override def apply(current: Option[T], index: Int): Either[Error, T] =
parse(None, index, 1)

override def isFlag: Boolean =
true
Expand All @@ -25,7 +30,7 @@ import dataclass.data
object FlagArgParser {

def from[T](description: String)(parse: Option[String] => Either[Error, T]): FlagArgParser[T] =
FlagArgParser(description, parse)
FlagArgParser(description, (valueOpt, _, _) => parse(valueOpt))

private val trues = Set("true", "1")
private val falses = Set("false", "0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,27 @@ import dataclass.data

@data class LastArgParser[T](parser: ArgParser[T]) extends ArgParser[Last[T]] {

def apply(current: Option[Last[T]], value: String): Either[Error, Last[T]] =
parser(None, value).map(Last(_))
def apply(
current: Option[Last[T]],
index: Int,
span: Int,
value: String
): Either[Error, Last[T]] =
parser(None, index, span, value).map(Last(_))

override def optional(
current: Option[Last[T]],
index: Int,
span: Int,
value: String
): (Consumed, Either[Error, Last[T]]) = {
val (consumed, res) = parser.optional(None, value)
val (consumed, res) = parser.optional(None, index, span, value)
val res0 = res.map(t => Last(t))
(consumed, res0)
}

override def apply(current: Option[Last[T]]): Either[Error, Last[T]] =
parser(None).map(Last(_))
override def apply(current: Option[Last[T]], index: Int): Either[Error, Last[T]] =
parser(None, index).map(Last(_))

override def isFlag: Boolean =
parser.isFlag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ final class MapErrorArgParser[T, U](
to: T => Either[Error, U]
) extends ArgParser[U] {

def apply(current: Option[U], value: String): Either[Error, U] =
argParser(current.map(from), value).flatMap(to)

override def optional(current: Option[U], value: String): (Consumed, Either[Error, U]) = {
val (consumed, res) = argParser.optional(current.map(from), value)
def apply(current: Option[U], index: Int, span: Int, value: String): Either[Error, U] =
argParser(current.map(from), index, span, value).flatMap(to)

override def optional(
current: Option[U],
index: Int,
span: Int,
value: String
): (Consumed, Either[Error, U]) = {
val (consumed, res) = argParser.optional(current.map(from), index, span, value)
val res0 = res.flatMap(to)
(consumed, res0)
}

override def apply(current: Option[U]): Either[Error, U] =
argParser(current.map(from)).flatMap(to)
override def apply(current: Option[U], index: Int): Either[Error, U] =
argParser(current.map(from), index).flatMap(to)

override def isFlag: Boolean =
argParser.isFlag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import dataclass.data

@data class SimpleArgParser[T](
description: String,
parse: String => Either[Error, T]
parse: (String, Int, Int) => Either[Error, T]
) extends ArgParser[T] {

def apply(current: Option[T], value: String): Either[Error, T] =
def apply(current: Option[T], index: Int, span: Int, value: String): Either[Error, T] =
current match {
case None =>
parse(value)
parse(value, index, span)
case Some(_) =>
Left(Error.ArgumentAlreadySpecified("???"))
}
Expand All @@ -21,7 +21,7 @@ import dataclass.data
object SimpleArgParser {

def from[T](description: String)(parse: String => Either[Error, T]): SimpleArgParser[T] =
SimpleArgParser(description, parse)
SimpleArgParser(description, (value, _, _) => parse(value))

val int: SimpleArgParser[Int] =
from("int") { s =>
Expand Down
Loading

0 comments on commit 92ae2e9

Please sign in to comment.