Skip to content

Commit

Permalink
Add an optional prefix for Recurse (#483)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexandre Archambault <alexandre.archambault@gmail.com>
  • Loading branch information
corey-at-protenus and alexarchambault authored Jun 14, 2024
1 parent 27cdd86 commit 950c9da
Show file tree
Hide file tree
Showing 17 changed files with 133 additions and 64 deletions.
11 changes: 10 additions & 1 deletion annotations/shared/src/main/scala/caseapp/Annotations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,17 @@ final case class AppVersion(appVersion: String) extends StaticAnnotation
final case class ArgsName(argsName: String) extends StaticAnnotation

/** Don't parse the annotated field as a single argument. Recurse on its fields instead.
*
* Optionally, add a prefix to all the names of the fields.
*/
final class Recurse extends StaticAnnotation
final case class Recurse(prefix: String) extends StaticAnnotation {
def this() = this("")
}

object Recurse {
def apply(): Recurse =
new Recurse
}

/** Do not include this field / argument in the help message
*/
Expand Down
2 changes: 1 addition & 1 deletion build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ trait MimaChecks extends Mima {
"--merged",
"HEAD^",
"--contains",
"993ac3020db84ba06231e0268c920c9f8b9f3520"
"27cdd86548d413c656b9493e625523b1e642c9be"
)
.call()
.out.lines()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package caseapp.core.help

import caseapp.{ExtraName, Group, HelpMessage, Parser}
import caseapp.{ExtraName, Group, HelpMessage, Parser, Recurse}
import caseapp.core.parser.{Argument, NilParser, StandardArgument}
import caseapp.core.{Arg, Error}
import caseapp.core.parser.RecursiveConsParser
Expand Down Expand Up @@ -55,7 +55,7 @@ abstract class WithFullHelpCompanion {

val withHelpParser = WithHelp.parser[T, D](underlying)

val p = RecursiveConsParser(withHelpParser, helpArgument :: NilParser)
val p = RecursiveConsParser(withHelpParser, helpArgument :: NilParser, Recurse())

p.to[WithFullHelp[T]]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package caseapp.core.help

import caseapp.{ExtraName, Group, Help, HelpMessage, Parser}
import caseapp.{ExtraName, Group, HelpMessage, Parser, Recurse}
import caseapp.core.parser.{Argument, NilParser, StandardArgument}
import caseapp.core.{Arg, Error}
import caseapp.core.parser.{EitherParser, RecursiveConsParser}
Expand Down Expand Up @@ -56,7 +56,7 @@ abstract class WithHelpCompanion {

val p = usageArgument ::
helpArgument ::
RecursiveConsParser(either, NilParser)
RecursiveConsParser(either, NilParser, Recurse())

p.to[WithHelp[T]]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ sealed abstract class HListParserBuilder[
-G <: HList,
-H <: HList,
-T <: HList,
R <: HList
-R <: HList
] {
type P <: HList
def apply(
Expand All @@ -26,7 +26,8 @@ sealed abstract class HListParserBuilder[
helpMessages: M,
groups: G,
noHelp: H,
tags: T
tags: T,
recurse: R
): Parser.Aux[L, P]
}

Expand Down Expand Up @@ -73,7 +74,7 @@ object HListParserBuilder extends LowPriorityHListParserBuilder {
R <: HList,
P0 <: HList
](
p: (() => D, N, V, M, G, H, T) => Parser.Aux[L, P0]
p: (() => D, N, V, M, G, H, T, R) => Parser.Aux[L, P0]
): Aux[L, D, N, V, M, G, H, T, R, P0] =
new HListParserBuilder[L, D, N, V, M, G, H, T, R] {
type P = P0
Expand All @@ -84,13 +85,14 @@ object HListParserBuilder extends LowPriorityHListParserBuilder {
helpMessages: M,
group: G,
noHelp: H,
tags: T
tags: T,
recurse: R
) =
p(() => default, names, valueDescriptions, helpMessages, group, noHelp, tags)
p(() => default, names, valueDescriptions, helpMessages, group, noHelp, tags, recurse)
}

implicit val hnil: Aux[HNil, HNil, HNil, HNil, HNil, HNil, HNil, HNil, HNil, HNil] =
instance { (_, _, _, _, _, _, _) =>
instance { (_, _, _, _, _, _, _, _) =>
NilParser
}

Expand Down Expand Up @@ -124,7 +126,7 @@ object HListParserBuilder extends LowPriorityHListParserBuilder {
None.type :: RT,
Option[H] :: PT
] =
instance { (default0, names, valueDescriptions, helpMessages, groups, noHelp, tags) =>
instance { (default0, names, valueDescriptions, helpMessages, groups, noHelp, tags, recurse) =>

val tailParser = tail.value(
default0().tail,
Expand All @@ -133,7 +135,8 @@ object HListParserBuilder extends LowPriorityHListParserBuilder {
helpMessages.tail,
groups.tail,
noHelp.tail,
tags.tail
tags.tail,
recurse.tail
)

val arg = Arg(
Expand Down Expand Up @@ -181,7 +184,7 @@ object HListParserBuilder extends LowPriorityHListParserBuilder {
Some[Recurse] :: RT,
headParser.value.D :: PT
] =
instance { (default0, names, valueDescriptions, helpMessages, groups, noHelp, tags) =>
instance { (default0, names, valueDescriptions, helpMessages, groups, noHelp, tags, recurse) =>

val tailParser = tail(
default0().tail,
Expand All @@ -190,10 +193,11 @@ object HListParserBuilder extends LowPriorityHListParserBuilder {
helpMessages.tail,
groups.tail,
noHelp.tail,
tags.tail
tags.tail,
recurse.tail
)

RecursiveConsParser(headParser.value, tailParser)
RecursiveConsParser(headParser.value, tailParser, recurse.head.value)
.mapHead(field[K](_))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ abstract class LowPriorityHListParserBuilder {
None.type :: RT,
Option[H] :: PT
] =
instance { (default0, names, valueDescriptions, helpMessages, groups, noHelp, tags) =>
instance { (default0, names, valueDescriptions, helpMessages, groups, noHelp, tags, recurse) =>

val tailParser = tail.value(
default0().tail,
Expand All @@ -47,7 +47,8 @@ abstract class LowPriorityHListParserBuilder {
helpMessages.tail,
groups.tail,
noHelp.tail,
tags.tail
tags.tail,
recurse.tail
)

val arg = Arg(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ trait LowPriorityParserImplicits {
helpMessages(),
group(),
noHelp(),
tags()
tags(),
recurse()
)
.map(gen.from)
.withDefaultOrigin(typeable.describe)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package caseapp.core.parser

import caseapp.{HelpMessage, Name, ValueDescription}
import caseapp.{HelpMessage, Name, Recurse, ValueDescription}
import caseapp.core.argparser.ArgParser
import caseapp.core.Arg
import shapeless.{::, Generic, HList, ops}
Expand Down Expand Up @@ -55,7 +55,7 @@ object ParserOps {

class AddAllHelper[T <: HList, D <: HList, U](val parser: Parser.Aux[T, D]) extends AnyVal {
def apply[DU](implicit other: Parser.Aux[U, DU]): Parser.Aux[U :: T, DU :: D] =
RecursiveConsParser(other, parser)
RecursiveConsParser(other, parser, Recurse())
}

sealed abstract class AsHelper[T, F] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package caseapp.core.parser

import caseapp.Name
import caseapp.{Name, Recurse}
import caseapp.core.{Arg, Error}
import caseapp.core.util.Formatter
import shapeless.{::, HList}
import dataclass.data

@data class RecursiveConsParser[H, HD, T <: HList, TD <: HList](
headParser: Parser.Aux[H, HD],
tailParser: Parser.Aux[T, TD]
tailParser: Parser.Aux[T, TD],
recurse: Recurse
) extends Parser[H :: T] {

type D = HD :: TD
Expand All @@ -23,7 +24,7 @@ import dataclass.data
nameFormatter: Formatter[Name]
): Either[(Error, Arg, List[String]), Option[(D, Arg, List[String])]] =
headParser
.step(args, index, d.head, nameFormatter)
.step(args, index, d.head, Formatter.addRecursePrefix(recurse, nameFormatter))
.flatMap {
case None =>
tailParser
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package caseapp.core.help

import caseapp.{ExtraName, Group, HelpMessage, Parser}
import caseapp.{ExtraName, Group, HelpMessage, Parser, Recurse}
import caseapp.core.Scala3Helpers.*
import caseapp.core.parser.{Argument, NilParser, StandardArgument}
import caseapp.core.{Arg, Error}
Expand Down Expand Up @@ -51,7 +51,7 @@ abstract class WithFullHelpCompanion {

val withHelpParser = WithHelp.parser[T](underlying)

val p = RecursiveConsParser(withHelpParser, helpArgument :: NilParser)
val p = RecursiveConsParser(withHelpParser, helpArgument :: NilParser, Recurse())

p.to[WithFullHelp[T]]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package caseapp.core.help

import caseapp.{ExtraName, Group, Help, HelpMessage, Parser}
import caseapp.{ExtraName, Group, Help, HelpMessage, Parser, Recurse}
import caseapp.core.Scala3Helpers.*
import caseapp.core.parser.{Argument, NilParser, StandardArgument}
import caseapp.core.{Arg, Error}
Expand Down Expand Up @@ -55,7 +55,7 @@ abstract class WithHelpCompanion {

val p = usageArgument ::
helpArgument ::
RecursiveConsParser(either, NilParser)
RecursiveConsParser(either, NilParser, Recurse())

p.to[WithHelp[T]]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,14 @@ object LowPriorityParserImplicits {
val parserExpr = fields0
.foldRight[(TypeRepr, Expr[Parser[_]])]((TypeRepr.of[EmptyTuple], '{ NilParser })) {
case ((sym, symTpe), (tailType, tailParserExpr)) =>
val isRecursive = sym.annotations.exists(_.tpe =:= TypeRepr.of[caseapp.Recurse])
val recurse = sym.annotations
.find(_.tpe =:= TypeRepr.of[caseapp.Recurse])
.collect {
case Apply(_, List(arg)) =>
'{ caseapp.Recurse(${ arg.asExprOf[String] }) }
case Apply(_, Nil) =>
'{ caseapp.Recurse() }
}
val extraNames = sym.annotations
.filter(_.tpe =:= TypeRepr.of[caseapp.ExtraName])
.collect {
Expand Down Expand Up @@ -186,36 +193,42 @@ object LowPriorityParserImplicits {
}
tailType.asType match {
case '[EmptyTuple] =>
if (isRecursive)
'{
RecursiveConsParser[t, EmptyTuple](
${ headParserExpr.asExprOf[Parser[t]] },
${ tailParserExpr.asExprOf[Parser[EmptyTuple]] }
)
}
else
'{
ConsParser[t, EmptyTuple](
$argumentExpr,
${ tailParserExpr.asExprOf[Parser[EmptyTuple]] }
)
}
recurse match {
case Some(recurseExpr) =>
'{
RecursiveConsParser[t, EmptyTuple](
${ headParserExpr.asExprOf[Parser[t]] },
${ tailParserExpr.asExprOf[Parser[EmptyTuple]] },
${ recurseExpr }
)
}
case None =>
'{
ConsParser[t, EmptyTuple](
$argumentExpr,
${ tailParserExpr.asExprOf[Parser[EmptyTuple]] }
)
}
}

case '[head *: tail] =>
if (isRecursive)
'{
RecursiveConsParser[t, head *: tail](
${ headParserExpr.asExprOf[Parser[t]] },
${ tailParserExpr.asExprOf[Parser[head *: tail]] }
)
}
else
'{
ConsParser[t, head *: tail](
$argumentExpr,
${ tailParserExpr.asExprOf[Parser[head *: tail]] }
)
}
recurse match {
case Some(recurseExpr) =>
'{
RecursiveConsParser[t, head *: tail](
${ headParserExpr.asExprOf[Parser[t]] },
${ tailParserExpr.asExprOf[Parser[head *: tail]] },
${ recurseExpr }
)
}
case None =>
'{
ConsParser[t, head *: tail](
$argumentExpr,
${ tailParserExpr.asExprOf[Parser[head *: tail]] }
)
}
}
}
}
(newTailType, expr)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package caseapp.core.parser

import caseapp.{HelpMessage, Name, ValueDescription}
import caseapp.{HelpMessage, Name, Recurse, ValueDescription}
import caseapp.core.argparser.ArgParser
import caseapp.core.Arg
import caseapp.core.util.Formatter
Expand Down Expand Up @@ -35,7 +35,7 @@ class ParserOps[T <: Tuple](val parser: Parser[T]) extends AnyVal {
}

def addAll[H](using headParser: Parser[H]): Parser[H *: T] =
RecursiveConsParser(headParser, parser)
RecursiveConsParser(headParser, parser, Recurse())

def as[F](using
m: Mirror.ProductOf[F],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package caseapp.core.parser

import caseapp.Name
import caseapp.{Name, Recurse}
import caseapp.core.{Arg, Error}
import caseapp.core.util.Formatter
import caseapp.core.Scala3Helpers._
import dataclass.data

case class RecursiveConsParser[H, T <: Tuple](
headParser: Parser[H],
tailParser: Parser[T]
tailParser: Parser[T],
recurse: Recurse
) extends Parser[H *: T] {

type D = headParser.D *: tailParser.D
Expand All @@ -23,7 +24,12 @@ case class RecursiveConsParser[H, T <: Tuple](
nameFormatter: Formatter[Name]
): Either[(Error, Arg, List[String]), Option[(D, Arg, List[String])]] =
headParser
.step(args, index, runtime.Tuples(d, 0).asInstanceOf[headParser.D], nameFormatter)
.step(
args,
index,
runtime.Tuples(d, 0).asInstanceOf[headParser.D],
Formatter.addRecursePrefix(recurse, nameFormatter)
)
.flatMap {
case None =>
tailParser
Expand Down
Loading

0 comments on commit 950c9da

Please sign in to comment.