Skip to content

Commit

Permalink
Wizard mode (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
jorge-vasquez-2301 authored Nov 6, 2022
1 parent d167a81 commit 9e913f3
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 22 deletions.
41 changes: 41 additions & 0 deletions zio-cli/shared/src/main/scala/zio/cli/Args.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ sealed trait Args[+A] { self =>

final def between(min: Int, max: Int): Args[List[A]] = Args.Variadic(self, Some(min), Some(max))

def generateArgs: UIO[List[String]]

def helpDoc: HelpDoc

final def map[B](f: A => B): Args[B] = Args.Map(self, (a: A) => Right(f(a)))
Expand All @@ -63,6 +65,8 @@ sealed trait Args[+A] { self =>

def synopsis: UsageSynopsis

def uid: Option[String]

def validate(args: List[String], conf: CliConfig): IO[ValidationError, (List[String], A)]
}

Expand Down Expand Up @@ -100,6 +104,11 @@ object Args {
}).mapError(ValidationError(ValidationErrorType.InvalidArgument, _))

private def name: String = "<" + self.pseudoName.getOrElse(self.primType.typeName) + ">"

def generateArgs: UIO[List[String]] =
(Console.print(s"${self.uid.getOrElse("")} (${self.primType.typeName}): ") *> Console.readLine).orDie.map(List(_))

def uid: Option[String] = Some(self.name)
}

case object Empty extends Args[Unit] {
Expand All @@ -115,6 +124,10 @@ object Args {

def validate(args: List[String], conf: CliConfig): UIO[(List[String], Unit)] =
ZIO.succeed((args, ()))

def generateArgs: UIO[List[String]] = ZIO.succeed(List.empty)

def uid: Option[String] = None
}

final case class Both[+A, +B](head: Args[A], tail: Args[B]) extends Args[(A, B)] { self =>
Expand All @@ -135,6 +148,13 @@ object Args {
tuple <- self.tail.validate(args, conf)
(args, b) = tuple
} yield (args, (a, b))

def generateArgs: UIO[List[String]] = self.head.generateArgs.zipWith(self.tail.generateArgs)(_ ++ _)

def uid: Option[String] = self.head.uid.toList ++ self.tail.uid.toList match {
case Nil => None
case list => Some(list.mkString(", "))
}
}

final case class Variadic[+A](value: Args[A], min: Option[Int], max: Option[Int]) extends Args[List[A]] { self =>
Expand Down Expand Up @@ -178,6 +198,23 @@ object Args {

loop(args, Nil).map { case (args, list) => (args, list.reverse) }
}

def generateArgs: UIO[List[String]] = {
val repetitionsString =
(self.min, self.max) match {
case (Some(min), Some(max)) => s"$min - $max repetitions"
case (Some(1), _) => "1 repetition minimum"
case (Some(min), _) => s"$min repetitions minimum"
case (_, Some(1)) => "1 repetition maximum"
case (_, Some(max)) => s"$max repetitions maximum"
case _ => ""
}
(Console.print(s"${self.uid.getOrElse("")} ($repetitionsString): ") *> Console.readLine).orDie.map { input =>
input.split(" ").toList
}
}

def uid: Option[String] = self.value.uid
}

final case class Map[A, B](value: Args[A], f: A => Either[HelpDoc, B]) extends Args[B] { self =>
Expand All @@ -198,6 +235,10 @@ object Args {
case Right(value) => ZIO.succeed((r, value))
}
}

def generateArgs: UIO[List[String]] = self.value.generateArgs

def uid: Option[String] = self.value.uid
}

/**
Expand Down
22 changes: 15 additions & 7 deletions zio-cli/shared/src/main/scala/zio/cli/BuiltInOption.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,35 @@ object BuiltInOption {
final case class ShowHelp(synopsis: UsageSynopsis, helpDoc: HelpDoc) extends BuiltInOption
final case class ShowCompletionScript(pathToExecutable: JPath, shellType: ShellType) extends BuiltInOption
final case class ShowCompletions(index: Int, shellType: ShellType) extends BuiltInOption
final case class Wizard(command: Command[_]) extends BuiltInOption

final case class BuiltIn(
help: Boolean,
shellCompletionScriptPath: Option[JPath],
shellType: Option[ShellType],
shellCompletionIndex: Option[Int]
shellCompletionIndex: Option[Int],
wizard: Boolean
)

def builtInOptions(usageSynopsis: => UsageSynopsis, helpDoc: => HelpDoc): Options[Option[BuiltInOption]] = {
def builtInOptions(
command: => Command[_],
usageSynopsis: => UsageSynopsis,
helpDoc: => HelpDoc
): Options[Option[BuiltInOption]] = {
val options = (
Options.boolean("help").alias("h") ++
Options.file("shell-completion-script").optional ++
ShellType.option.optional ++
Options.integer("shell-completion-index").map(_.toInt).optional
Options.integer("shell-completion-index").map(_.toInt).optional ++
Options.boolean("wizard")
).as(BuiltIn.apply _)

options.map {
case BuiltIn(true, _, _, _) => Some(ShowHelp(usageSynopsis, helpDoc))
case BuiltIn(_, Some(path), Some(shellType), _) => Some(ShowCompletionScript(path, shellType))
case BuiltIn(_, _, Some(shellType), Some(index)) => Some(ShowCompletions(index, shellType))
case _ => None
case BuiltIn(true, _, _, _, _) => Some(ShowHelp(usageSynopsis, helpDoc))
case BuiltIn(_, _, _, _, true) => Some(Wizard(command))
case BuiltIn(_, Some(path), Some(shellType), _, _) => Some(ShowCompletionScript(path, shellType))
case BuiltIn(_, _, Some(shellType), Some(index), _) => Some(ShowCompletions(index, shellType))
case _ => None
}
}
}
29 changes: 23 additions & 6 deletions zio-cli/shared/src/main/scala/zio/cli/CliApp.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package zio.cli

import zio.Console.printLine
import zio.Console.{print, printLine, readLine}
import zio.System.envs
import zio._
import zio.cli.BuiltInOption._
Expand All @@ -16,7 +16,7 @@ import scala.annotation.tailrec
* requires environment `R`, and may fail with a value of type `E`.
*/
sealed trait CliApp[-R, +E, +Model] {
def run(args: List[String]): ZIO[R, Nothing, ExitCode]
def run(args: List[String]): ZIO[R, Any, Any]

def config(newConfig: CliConfig): CliApp[R, E, Model]

Expand Down Expand Up @@ -56,8 +56,8 @@ object CliApp {
def printDocs(helpDoc: HelpDoc): UIO[Unit] =
printLine(helpDoc.toPlaintext(80)).!

def run(args: List[String]): ZIO[R, Nothing, ExitCode] = {
def executeBuiltIn(builtInOption: BuiltInOption): Task[Unit] =
def run(args: List[String]): ZIO[R, Any, Any] = {
def executeBuiltIn(builtInOption: BuiltInOption): ZIO[R, Any, Any] =
builtInOption match {
case ShowHelp(synopsis, helpDoc) =>
val fancyName = p(code(self.figFont.render(self.name)))
Expand Down Expand Up @@ -86,6 +86,21 @@ object CliApp {
ZIO.foreachDiscard(completions)(word => printLine(word))
}
}
case Wizard(command) =>
val subcommands = command.getSubcommands

for {
subcommandName <- if (subcommands.size == 1) ZIO.succeed(subcommands.keys.head)
else
(print("Command" + subcommands.keys.mkString("(", "|", "): ")) *> readLine).orDie
subcommand <-
ZIO
.fromOption(subcommands.get(subcommandName))
.orElseFail(ValidationError(ValidationErrorType.InvalidValue, HelpDoc.p("Invalid subcommand")))
args <- subcommand.generateArgs
_ <- Console.printLine(s"Executing command: ${(prefix(self.command) ++ args).mkString(" ")}")
result <- self.run(args)
} yield result
}

// prepend a first argument in case the CliApp's command is expected to consume it
Expand All @@ -104,10 +119,12 @@ object CliApp {
e => printDocs(e.error),
{
case CommandDirective.UserDefined(_, value) => self.execute(value)
case CommandDirective.BuiltIn(x) => executeBuiltIn(x)
case CommandDirective.BuiltIn(x) =>
executeBuiltIn(x).catchSome { case e: ValidationError =>
printDocs(e.error)
}
}
)
.exitCode
}

def summary(s: HelpDoc.Span): CliApp[R, E, Model] =
Expand Down
46 changes: 43 additions & 3 deletions zio-cli/shared/src/main/scala/zio/cli/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package zio.cli

import zio.cli.HelpDoc.h1
import zio.cli.ValidationErrorType.CommandMismatch
import zio.{IO, ZIO}
import zio.{IO, UIO, ZIO}

/**
* A `Command` represents a command in a command-line application. Every command-line application
Expand Down Expand Up @@ -35,6 +35,10 @@ sealed trait Command[+A] { self =>
subcommands.copy(parent = subcommands.parent.withHelp(help)).asInstanceOf[Command[A]]
}

def generateArgs: UIO[List[String]]

def getSubcommands: Map[String, Command[_]]

def helpDoc: HelpDoc

final def map[B](f: A => B): Command[B] = Command.Map(self, f)
Expand Down Expand Up @@ -110,7 +114,7 @@ object Command {
val parseBuiltInArgs =
if (args.headOption.exists(conf.normalizeCase(_) == conf.normalizeCase(self.name)))
BuiltInOption
.builtInOptions(self.synopsis, self.helpDoc)
.builtInOptions(self, self.synopsis, self.helpDoc)
.validate(args, conf)
.mapBoth(_.error, _._2)
.some
Expand Down Expand Up @@ -150,6 +154,14 @@ object Command {

lazy val synopsis: UsageSynopsis =
UsageSynopsis.Named(List(self.name), None) + self.options.synopsis + self.args.synopsis

def generateArgs: UIO[List[String]] =
for {
options <- self.options.generateArgs
args <- self.args.generateArgs
} yield List(self.name) ++ options ++ args

def getSubcommands: Predef.Map[String, Command[_]] = Predef.Map(self.name -> self)
}

final case class Map[A, B](command: Command[A], f: A => B) extends Command[B] { self =>
Expand All @@ -164,6 +176,10 @@ object Command {
self.command.parse(args, conf).map(_.map(f))

lazy val synopsis: UsageSynopsis = self.command.synopsis

def generateArgs: UIO[List[String]] = self.command.generateArgs

def getSubcommands: Predef.Map[String, Command[_]] = self.command.getSubcommands
}

final case class OrElse[A](left: Command[A], right: Command[A]) extends Command[A] { self =>
Expand All @@ -178,6 +194,10 @@ object Command {
self.left.parse(args, conf).catchSome { case ValidationError(CommandMismatch, _) => self.right.parse(args, conf) }

lazy val synopsis: UsageSynopsis = UsageSynopsis.Mixed

def generateArgs: UIO[List[String]] = self.left.generateArgs.zipWith(self.right.generateArgs)(_ ++ _)

def getSubcommands: Predef.Map[String, Command[_]] = self.left.getSubcommands ++ self.right.getSubcommands
}

final case class Subcommands[A, B](parent: Command[A], child: Command[B]) extends Command[(A, B)] { self =>
Expand Down Expand Up @@ -244,12 +264,28 @@ object Command {
val helpDirectiveForParent =
ZIO.succeed(CommandDirective.builtIn(BuiltInOption.ShowHelp(self.synopsis, self.helpDoc)))

val wizardDirectiveForChild = {
val safeTail = args match {
case Nil => Nil
case _ :: tail => tail
}
self.child
.parse(safeTail, conf)
.collect(ValidationError(ValidationErrorType.InvalidArgument, HelpDoc.empty)) {
case directive @ CommandDirective.BuiltIn(BuiltInOption.Wizard(_)) => directive
}
}

val wizardDirectiveForParent =
ZIO.succeed(CommandDirective.builtIn(BuiltInOption.Wizard(self)))

self.parent
.parse(args, conf)
.flatMap {
case CommandDirective.BuiltIn(BuiltInOption.ShowHelp(_, _)) =>
helpDirectiveForChild orElse helpDirectiveForParent
case builtIn @ CommandDirective.BuiltIn(_) => ZIO.succeed(builtIn)
case CommandDirective.BuiltIn(_) =>
wizardDirectiveForChild orElse wizardDirectiveForParent
case CommandDirective.UserDefined(leftover, a) if leftover.nonEmpty =>
self.child.parse(leftover, conf).map(_.map((a, _)))
case _ =>
Expand All @@ -262,6 +298,10 @@ object Command {
}

lazy val synopsis: UsageSynopsis = self.parent.synopsis + self.child.synopsis

def generateArgs: UIO[List[String]] = self.parent.generateArgs.zipWith(self.child.generateArgs)(_ ++ _)

def getSubcommands: Predef.Map[String, Command[_]] = self.child.getSubcommands
}

/**
Expand Down
11 changes: 11 additions & 0 deletions zio-cli/shared/src/main/scala/zio/cli/HelpDoc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,17 @@ object HelpDoc {
case Span.URI(value) => value.toString.length
case Span.Sequence(left, right) => left.size + right.size
}

final def text: String =
self match {
case Span.Text(value) => value
case Span.Code(value) => value
case Span.Error(value) => value.text
case Span.Weak(value) => value.text
case Span.Strong(value) => value.text
case Span.URI(value) => value.toString
case Span.Sequence(left, right) => left.text + right.text
}
}
object Span {
final case class Text(value: String) extends Span
Expand Down
Loading

0 comments on commit 9e913f3

Please sign in to comment.