-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* basic support for cats-effect IO * refactor caseapp to use shared parse result -> what to do next method * refactor to get rid of shared AppRunners * add IO versions of all case-app apps * caseapp.cats.app -> caseapp.cats, the .app felt redundant * add cats-effect module usage to readme * add basic tests for IOCaseApp * rename Tests as utest defines Tests * add tests for IO command app update scalajs due to errors related to RecordedApp null refs * add tests for command help/usage * fix io apps docs * Disable compatibility checks for new module Co-authored-by: Alexandre Archambault <[email protected]>
- Loading branch information
1 parent
7592fd8
commit aa91353
Showing
12 changed files
with
399 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package caseapp.cats | ||
|
||
import caseapp.core.Error | ||
import caseapp.core.help.{Help, WithHelp} | ||
import caseapp.core.parser.Parser | ||
import caseapp.core.RemainingArgs | ||
import caseapp.Name | ||
import caseapp.core.util.Formatter | ||
import cats.effect.{ExitCode, IO, IOApp} | ||
|
||
abstract class IOCaseApp[T](implicit val parser0: Parser[T], val messages: Help[T]) extends IOApp { | ||
|
||
def parser: Parser[T] = { | ||
val p = parser0.nameFormatter(nameFormatter) | ||
if (stopAtFirstUnrecognized) | ||
p.stopAtFirstUnrecognized | ||
else | ||
p | ||
} | ||
|
||
def run(options: T, remainingArgs: RemainingArgs): IO[ExitCode] | ||
|
||
def error(message: Error): IO[ExitCode] = | ||
IO(Console.err.println(message.message)) | ||
.as(ExitCode.Error) | ||
|
||
def helpAsked: IO[ExitCode] = | ||
println(messages.withHelp.help) | ||
.as(ExitCode.Success) | ||
|
||
def usageAsked: IO[ExitCode] = | ||
println(messages.withHelp.usage) | ||
.as(ExitCode.Success) | ||
|
||
def println(x: String): IO[Unit] = | ||
IO(Console.println(x)) | ||
|
||
/** | ||
* Arguments are expanded then parsed. By default, argument expansion is the identity function. | ||
* Overriding this method allows plugging in an arbitrary argument expansion logic. | ||
* | ||
* One such expansion logic involves replacing each argument of the form '@<file>' with the | ||
* contents of that file where each line in the file becomes a distinct argument. | ||
* To enable this behavior, override this method as shown below. | ||
* @example | ||
* {{{ | ||
* import caseapp.core.parser.PlatformArgsExpander | ||
* override def expandArgs(args: List[String]): List[String] | ||
* = PlatformArgsExpander.expand(args) | ||
* }}} | ||
* | ||
* @param args | ||
* @return | ||
*/ | ||
def expandArgs(args: List[String]): List[String] = args | ||
|
||
/** | ||
* Whether to stop parsing at the first unrecognized argument. | ||
* | ||
* That is, stop parsing at the first non option (not starting with "-"), or | ||
* the first unrecognized option. The unparsed arguments are put in the `args` | ||
* argument of `run`. | ||
*/ | ||
def stopAtFirstUnrecognized: Boolean = | ||
false | ||
|
||
def nameFormatter: Formatter[Name] = | ||
Formatter.DefaultNameFormatter | ||
|
||
override def run(args: List[String]): IO[ExitCode] = | ||
parser.withHelp.detailedParse(args) match { | ||
case Left(err) => error(err) | ||
case Right((WithHelp(true, _, _), _)) => usageAsked | ||
case Right((WithHelp(_, true, _), _)) => helpAsked | ||
case Right((WithHelp(_, _, Left(err)), _)) => error(err) | ||
case Right((WithHelp(_, _, Right(t)), remainingArgs)) => run(t, remainingArgs) | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
cats/shared/src/main/scala/caseapp/cats/IOCommandApp.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package caseapp.cats | ||
|
||
import caseapp.core.commandparser.CommandParser | ||
import caseapp.core.Error | ||
import caseapp.core.help.CommandsHelp | ||
import cats.effect.{ExitCode, IO} | ||
|
||
abstract class IOCommandApp[T](implicit | ||
commandParser: CommandParser[T], | ||
commandsMessages: CommandsHelp[T] | ||
) extends IOCommandAppWithPreCommand[None.type , T] { | ||
|
||
override def beforeCommand(options: None.type, remainingArgs: Seq[String]): IO[Option[ExitCode]] = { | ||
if (remainingArgs.nonEmpty) { | ||
error(Error.Other(s"Found extra arguments: ${remainingArgs.mkString(" ")}")) | ||
.map(Some(_)) | ||
} else IO.none | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
cats/shared/src/main/scala/caseapp/cats/IOCommandAppA.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package caseapp.cats | ||
|
||
import caseapp.core.commandparser.CommandParser | ||
import caseapp.core.help.CommandsHelp | ||
import caseapp.core.RemainingArgs | ||
import cats.effect.{ExitCode, IO} | ||
|
||
/* The A suffix stands for anonymous */ | ||
abstract class IOCommandAppA[T](implicit | ||
commandParser: CommandParser[T], | ||
commandsMessages: CommandsHelp[T] | ||
) extends IOCommandApp[T]()(commandParser, commandsMessages) { | ||
|
||
def runA: RemainingArgs => T => IO[ExitCode] | ||
|
||
override def run(options: T, remainingArgs: RemainingArgs): IO[ExitCode] = | ||
runA(remainingArgs)(options) | ||
} |
108 changes: 108 additions & 0 deletions
108
cats/shared/src/main/scala/caseapp/cats/IOCommandAppWithPreCommand.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package caseapp.cats | ||
|
||
import caseapp.core.Error | ||
import caseapp.core.commandparser.CommandParser | ||
import caseapp.core.help.{CommandsHelp, Help, WithHelp} | ||
import caseapp.core.parser.Parser | ||
import caseapp.core.RemainingArgs | ||
import cats.effect._ | ||
|
||
abstract class IOCommandAppWithPreCommand[D, T](implicit | ||
val beforeCommandParser: Parser[D], | ||
baseBeforeCommandMessages: Help[D], | ||
val commandParser: CommandParser[T], | ||
val commandsMessages: CommandsHelp[T] | ||
) extends IOApp { | ||
|
||
/** | ||
* Override to support conditional early exit, suppressing a run. | ||
* @param options parsed options | ||
* @param remainingArgs extra arguments | ||
* @return exit code for early exit, none to call run | ||
*/ | ||
def beforeCommand(options: D, remainingArgs: Seq[String]): IO[Option[ExitCode]] | ||
|
||
def run(options: T, remainingArgs: RemainingArgs): IO[ExitCode] | ||
|
||
def error(message: Error): IO[ExitCode] = IO { | ||
Console.err.println(message.message) | ||
ExitCode(255) | ||
} | ||
|
||
lazy val beforeCommandMessages: Help[D] = | ||
baseBeforeCommandMessages | ||
.withAppName(appName) | ||
.withAppVersion(appVersion) | ||
.withProgName(progName) | ||
.withOptionsDesc(s"[options] [command] [command-options]") | ||
.asInstanceOf[Help[D]] // circumventing data-class losing the type param :| | ||
|
||
lazy val commands: Seq[Seq[String]] = CommandsHelp[T].messages.map(_._1) | ||
|
||
def helpAsked(): IO[ExitCode] = | ||
println( | ||
s"""${beforeCommandMessages.help} | ||
|Available commands: ${commands.map(_.mkString(" ")).mkString(", ")} | ||
| | ||
|Type $progName command --help for help on an individual command""" | ||
.stripMargin) | ||
.as(ExitCode.Success) | ||
|
||
def commandHelpAsked(command: Seq[String]): IO[ExitCode] = | ||
println(commandsMessages.messagesMap(command).helpMessage(beforeCommandMessages.progName, command)) | ||
.as(ExitCode.Success) | ||
|
||
def usageAsked(): IO[ExitCode] = | ||
println( | ||
s"""${beforeCommandMessages.usage} | ||
|Available commands: ${commands.map(_.mkString(" ")).mkString(", ")} | ||
| | ||
|Type $progName command --usage for usage of an individual command""" | ||
.stripMargin) | ||
.as(ExitCode.Success) | ||
|
||
def commandUsageAsked(command: Seq[String]): IO[ExitCode] = | ||
println(commandsMessages.messagesMap(command).usageMessage(beforeCommandMessages.progName, command)) | ||
.as(ExitCode.Success) | ||
|
||
def println(x: String): IO[Unit] = | ||
IO(Console.println(x)) | ||
|
||
def appName: String = Help[D].appName | ||
def appVersion: String = Help[D].appVersion | ||
def progName: String = Help[D].progName | ||
|
||
override def run(args: List[String]): IO[ExitCode] = { | ||
commandParser.withHelp.detailedParse(args.toVector)(beforeCommandParser.withHelp) match { | ||
case Left(err) => | ||
error(err) | ||
case Right((WithHelp(true, _, _), _, _)) => | ||
usageAsked() | ||
case Right((WithHelp(_, true, _), _, _)) => | ||
helpAsked() | ||
case Right((WithHelp(false, false, Left(err)), _, _)) => | ||
error(err) | ||
case Right((WithHelp(false, false, Right(d)), dArgs, optCmd)) => | ||
beforeCommand(d, dArgs).flatMap { | ||
case Some(exitCode) => IO.pure(exitCode) | ||
case None => | ||
optCmd | ||
.map { | ||
case Left(err) => | ||
error(err) | ||
case Right((c, WithHelp(true, _, _), _)) => | ||
commandUsageAsked(c) | ||
case Right((c, WithHelp(_, true, _), _)) => | ||
commandHelpAsked(c) | ||
case Right((_, WithHelp(_, _, t), commandArgs)) => | ||
t.fold( | ||
error, | ||
run(_, commandArgs) | ||
) | ||
} | ||
.getOrElse(IO(ExitCode.Success)) | ||
} | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.