Skip to content

Commit

Permalink
Accept syntactic IDE suggestions and replace function calls for more …
Browse files Browse the repository at this point in the history
…direct options (zio#315)
  • Loading branch information
Kalin-Rudnicki authored Jun 4, 2024
1 parent 3a10906 commit 1d8058f
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 97 deletions.
2 changes: 1 addition & 1 deletion zio-cli/shared/src/main/scala/zio/cli/AutoCorrect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ private[cli] object AutoCorrect {
for {
row <- 1 to rowCount
col <- 1 to columnCount
} yield {
} {
val cost = if (normalFirst.charAt(row - 1) == normalSecond.charAt(col - 1)) 0 else 1

matrix(row)(col) = Seq(
Expand Down
17 changes: 7 additions & 10 deletions zio-cli/shared/src/main/scala/zio/cli/CliApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ object CliApp {
def footer(newFooter: HelpDoc): CliApp[R, E, A] =
copy(footer = self.footer + newFooter)

def printDocs(helpDoc: HelpDoc): UIO[Unit] =
private def printDocs(helpDoc: HelpDoc): UIO[Unit] =
printLine(helpDoc.toPlaintext(80)).!

def run(args: List[String]): ZIO[R, CliError[E], Option[A]] = {
Expand All @@ -83,12 +83,11 @@ object CliApp {
// TODO add rendering of built-in options such as help
printLine(
(fancyName + header + synopsisHelpDoc + helpDoc + self.footer).toPlaintext(columnWidth = 300)
).map(_ => None).mapError(CliError.IO(_))

).mapBoth(CliError.IO(_), _ => None)
case ShowCompletionScript(path, shellType) =>
printLine(
CompletionScript(path, if (self.command.names.nonEmpty) self.command.names else Set(self.name), shellType)
).map(_ => None).mapError(CliError.IO(_))
).mapBoth(CliError.IO(_), _ => None)
case ShowCompletions(index, _) =>
envs.flatMap { envMap =>
val compWords = envMap.collect {
Expand All @@ -101,8 +100,8 @@ object CliApp {
.flatMap { completions =>
ZIO.foreachDiscard(completions)(word => printLine(word))
}
}.map(_ => None).mapError(CliError.BuiltIn(_))
case ShowWizard(command) => {
}.mapBoth(CliError.BuiltIn(_), _ => None)
case ShowWizard(command) =>
val fancyName = p(code(self.figFont.render(self.name)))
val header = p(text("WIZARD of ") + text(self.name) + text(self.version) + text(" -- ") + self.summary)
val explanation = p(s"Wizard mode assist you in constructing commands for $name$version")
Expand All @@ -112,10 +111,8 @@ object CliApp {
Wizard(command, config, fancyName + header + explanation).execute.mapError(CliError.BuiltIn(_))
output <- run(parameters)
} yield output).catchSome { case CliError.BuiltIn(_) =>
ZIO.succeed(None)
ZIO.none
}
}

}

// prepend a first argument in case the CliApp's command is expected to consume it
Expand All @@ -134,7 +131,7 @@ object CliApp {
e => printDocs(e.error) *> ZIO.fail(CliError.Parsing(e)),
{
case CommandDirective.UserDefined(_, value) =>
self.execute(value).map(Some(_)).mapError(CliError.Execution(_))
self.execute(value).mapBoth(CliError.Execution(_), Some(_))
case CommandDirective.BuiltIn(x) =>
executeBuiltIn(x).catchSome { case err @ CliError.Parsing(e) =>
printDocs(e.error) *> ZIO.fail(err)
Expand Down
63 changes: 29 additions & 34 deletions zio-cli/shared/src/main/scala/zio/cli/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ object Command {
}

final case class Single[OptionsType, ArgsType](
val name: String,
name: String,
help: HelpDoc,
options: Options[OptionsType],
args: Args[ArgsType]
Expand Down Expand Up @@ -130,43 +130,38 @@ object Command {
ZIO.fail(
ValidationError(
ValidationErrorType.CommandMismatch,
HelpDoc.p(s"Missing command name: ${name}")
HelpDoc.p(s"Missing command name: $name")
)
)

val parseUserDefinedArgs =
for {
commandOptionsAndArgs <- args match {
case head :: tail =>
ZIO
.succeed(tail)
.when(conf.normalizeCase(head) == conf.normalizeCase(name))
.some
.orElseFail {
ValidationError(
ValidationErrorType.CommandMismatch,
HelpDoc.p(s"Missing command name: ${name}")
)
}
case Nil =>
ZIO.fail {
ValidationError(
ValidationErrorType.CommandMismatch,
HelpDoc.p(s"Missing command name: ${name}")
)
}
}
tuple1 = splitForcedArgs(commandOptionsAndArgs)
(optionsAndArgs, forcedCommandArgs) = tuple1
tuple2 <- Options.validate(options, optionsAndArgs, conf)
(error, commandArgs, optionsType) = tuple2
tuple <- self.args.validate(commandArgs ++ forcedCommandArgs, conf).catchSome { case e =>
error match {
case None => ZIO.fail(e)
case Some(err) => ZIO.fail(err)
}
}
(argsLeftover, argsType) = tuple
commandOptionsAndArgs <-
args match {
case head :: tail =>
ZIO
.succeed(tail)
.when(conf.normalizeCase(head) == conf.normalizeCase(name))
.someOrFail {
ValidationError(
ValidationErrorType.CommandMismatch,
HelpDoc.p(s"Missing command name: $name")
)
}
case Nil =>
ZIO.fail {
ValidationError(
ValidationErrorType.CommandMismatch,
HelpDoc.p(s"Missing command name: $name")
)
}
}
tuple1 = splitForcedArgs(commandOptionsAndArgs)
(optionsAndArgs, forcedCommandArgs) = tuple1
tuple2 <- Options.validate(options, optionsAndArgs, conf)
(optionsError, commandArgs, optionsType) = tuple2
tuple <- self.args.validate(commandArgs ++ forcedCommandArgs, conf).mapError(optionsError.getOrElse(_))
(argsLeftover, argsType) = tuple
} yield CommandDirective.userDefined(argsLeftover, (optionsType, argsType))

val exhaustiveSearch: IO[ValidationError, CommandDirective[(OptionsType, ArgsType)]] =
Expand All @@ -176,7 +171,7 @@ object Command {
ZIO.fail(
ValidationError(
ValidationErrorType.CommandMismatch,
HelpDoc.p(s"Missing command name: ${name}")
HelpDoc.p(s"Missing command name: $name")
)
)

Expand Down
89 changes: 37 additions & 52 deletions zio-cli/shared/src/main/scala/zio/cli/Options.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package zio.cli

import zio.cli.HelpDoc.Span._
import zio.cli.HelpDoc.p
import zio.{IO, ZIO, Zippable}
import zio.{IO, UIO, ZIO, Zippable}
import zio.cli.oauth2._

import java.time.{
Expand Down Expand Up @@ -43,12 +43,12 @@ sealed trait Options[+A] extends Parameter { self =>

def ??(that: String): Options[A] =
modifySingle(new SingleModifier {
override def apply[A](single: Single[A]): Single[A] = single.copy(description = single.description + p(that))
override def apply[A1](single: Single[A1]): Single[A1] = single.copy(description = single.description + p(that))
})

def alias(name: String, names: String*): Options[A] =
modifySingle(new SingleModifier {
override def apply[A](single: Single[A]): Single[A] = single.copy(aliases = single.aliases ++ (name +: names))
override def apply[A1](single: Single[A1]): Single[A1] = single.copy(aliases = single.aliases ++ (name +: names))
})

final def as[B, C, Z](f: (B, C) => Z)(implicit ev: A <:< (B, C)): Options[Z] =
Expand Down Expand Up @@ -209,31 +209,25 @@ object Options extends OptionsPlatformSpecific {
case head :: tail =>
head
.parse(input, conf)
.flatMap(parsed =>
parsed match {
case (Nil, input) =>
findOptions(input, tail, conf).map { case (otherArgs, otherOptions, map) =>
(otherArgs, head :: otherOptions, map)
}
case (name :: values, leftover) =>
ZIO.succeed((leftover, tail, Predef.Map(name -> values)))
}
)
.flatMap {
case (Nil, input) =>
findOptions(input, tail, conf).map { case (otherArgs, otherOptions, map) =>
(otherArgs, head :: otherOptions, map)
}
case (name :: values, leftover) =>
ZIO.succeed((leftover, tail, Predef.Map(name -> values)))
}
.catchSome { case e @ ValidationError(validationErrorType, _) =>
validationErrorType match {
case ValidationErrorType.UnclusteredFlag(list, tail) =>
matchUnclustered(list, tail, options, conf).catchAll { case _ =>
ZIO.fail(e)
}
matchUnclustered(list, tail, options, conf).orElseFail(e)
case ValidationErrorType.MissingFlag =>
findOptions(input, tail, conf).map { case (otherArgs, otherOptions, map) =>
(otherArgs, head :: otherOptions, map)
}
case ValidationErrorType.CorrectedFlag =>
for {
tuple <- findOptions(input, tail, conf).catchSome { case _ =>
ZIO.fail(e)
}
tuple <- findOptions(input, tail, conf).orElseFail(e)
(otherArgs, otherOptions, map) = tuple
_ <- ZIO.when(map.isEmpty)(ZIO.fail(e))
} yield (otherArgs, head :: otherOptions, map)
Expand Down Expand Up @@ -263,20 +257,20 @@ object Options extends OptionsPlatformSpecific {
}

// Sums the list associated with the same key.
@tailrec
private def merge(
map1: Predef.Map[String, List[String]],
map2: List[(String, List[String])]
): Predef.Map[String, List[String]] =
map2 match {
case Nil => map1
case head :: tail => {
case Nil => map1
case head :: tail =>
// replace with updatedWith for Scala 2.13
val newMap = map1.get(head._1) match {
case None => map1 + head
case Some(elem) => map1.updated(head._1, elem ++ head._2)
}
merge(newMap, tail)
}
}

/**
Expand All @@ -287,7 +281,7 @@ object Options extends OptionsPlatformSpecific {
input: List[String],
options: List[Options[_] with Input],
conf: CliConfig
): IO[Nothing, (Option[ValidationError], List[String], Predef.Map[String, List[String]])] =
): UIO[(Option[ValidationError], List[String], Predef.Map[String, List[String]])] =
(input, options) match {
case (Nil, _) => ZIO.succeed((None, Nil, Predef.Map.empty))
case (_, Nil) => ZIO.succeed((None, input, Predef.Map.empty))
Expand All @@ -296,7 +290,8 @@ object Options extends OptionsPlatformSpecific {
tuple1 <- findOptions(input, options, conf)
(otherArgs, otherOptions, map1) = tuple1
tuple2 <-
if (map1.isEmpty) ZIO.succeed((None, input, map1)) else matchOptions(otherArgs, otherOptions, conf)
if (map1.isEmpty) ZIO.succeed((None, input, map1))
else matchOptions(otherArgs, otherOptions, conf)
(error, otherArgs, map2) = tuple2
} yield (error, otherArgs, merge(map1, map2.toList)))
.catchAll(e => ZIO.succeed((Some(e), input, Predef.Map.empty)))
Expand All @@ -316,21 +311,15 @@ object Options extends OptionsPlatformSpecific {
for {
matched <- matchOptions(args, options.flatten, conf)
(error, commandArgs, matchedOptions) = matched
a <- options.validate(matchedOptions, conf).catchAll { case e =>
error match {
case Some(err) => ZIO.fail(err)
case None => ZIO.fail(e)
}
}
a <- options.validate(matchedOptions, conf).mapError(error.getOrElse(_))
} yield (error, commandArgs, a)

case object Empty extends Options[Unit] with Pipeline {
lazy val synopsis: UsageSynopsis = UsageSynopsis.None

override def flatten: List[Options[_] with Input] = Nil

override def validate(args: Predef.Map[String, List[String]], conf: CliConfig): IO[ValidationError, Unit] =
ZIO.succeed(())
override def validate(args: Predef.Map[String, List[String]], conf: CliConfig): IO[ValidationError, Unit] = ZIO.unit

override def modifySingle(f: SingleModifier): Options[Unit] = Empty

Expand Down Expand Up @@ -416,7 +405,7 @@ object Options extends OptionsPlatformSpecific {
} match {
case Nil =>
ZIO.fail(
ValidationError(ValidationErrorType.MissingValue, p(error(s"Expected to find ${fullName} option.")))
ValidationError(ValidationErrorType.MissingValue, p(error(s"Expected to find $fullName option.")))
)
case head :: Nil =>
head match {
Expand All @@ -434,7 +423,7 @@ object Options extends OptionsPlatformSpecific {
ZIO.fail(
ValidationError(
ValidationErrorType.InvalidValue,
p(error(s"""More than one reference to option ${fullName}."""))
p(error(s"""More than one reference to option $fullName."""))
)
)
}
Expand Down Expand Up @@ -466,19 +455,19 @@ object Options extends OptionsPlatformSpecific {
ZIO.fail(
ValidationError(
ValidationErrorType.CorrectedFlag,
p(error(s"""The flag "$head" is not recognized. Did you mean ${fullName}?"""))
p(error(s"""The flag "$head" is not recognized. Did you mean $fullName?"""))
)
)
else
ZIO.fail(
ValidationError(
ValidationErrorType.MissingFlag,
p(error(s"Expected to find ${fullName} option."))
p(error(s"Expected to find $fullName option."))
)
)
case Nil =>
ZIO.fail(
ValidationError(ValidationErrorType.MissingFlag, p(error(s"Expected to find ${fullName} option.")))
ValidationError(ValidationErrorType.MissingFlag, p(error(s"Expected to find $fullName option.")))
)
}

Expand Down Expand Up @@ -674,21 +663,17 @@ object Options extends OptionsPlatformSpecific {
argumentOption
.validate(args, conf)
.foldZIO(
err =>
err match {
case e @ ValidationError(errorType, _) =>
errorType match {
case ValidationErrorType.KeyValuesDetected(list) =>
ZIO
.foreach(list) { case keyValue =>
extractKeyValue(keyValue)
}
.map(_.toMap)
case _ =>
ZIO.fail(e)
}
},
keyValue => extractKeyValue(keyValue).map(List(_).toMap)
{ case e @ ValidationError(errorType, _) =>
errorType match {
case ValidationErrorType.KeyValuesDetected(list) =>
ZIO
.foreach(list)(extractKeyValue)
.map(_.toMap)
case _ =>
ZIO.fail(e)
}
},
extractKeyValue(_).map(List(_).toMap)
)
}

Expand Down

0 comments on commit 1d8058f

Please sign in to comment.