Declinio is a Scala 3 library that provides seamless integration between Decline and Cats Effect avoiding the awkward constructor syntax of Decline.
It simplifies the creation of command-line applications by combining the declarative argument parsing of Decline with the powerful effect management of Cats Effect.
- Simple Integration - Combine Decline's argument parsing with Cats Effect's IO monad
- Minimal Boilerplate - Just extend a trait and define your options
- ReaderT Support - Use ReaderT for dependency injection patterns
- Configurable - Support for custom version flags, help messages, and more
- Type-Safe - Leverage Scala's type system for compile-time safety
- Effect-Aware - Full support for Cats Effect's resource management and error handling
- Custom Effect Types - Use any effect type with a natural transformation to IO
Decline is a powerful and well written library but, personally, I
find the CommandApp way of defining your application, with the configuration and main function as
constructor arguments, very awkward:
object SampleApp extends CommandApp(
name = "tail",
header = "Print the last few lines of one or more files.",
main = (linesOrDefault, fileList).mapN { (n, files) =>
println(s"LOG: Printing the last $n lines from each file in $files!")
}
)Add the following dependencies to your build.sbt:
libraryDependencies ++= Seq(
"com.colofabrix.scala" %% "declinio" % "1.0.0",
"com.monovore" %% "decline" % <version>, // Required dependency
"org.typelevel" %% "cats-effect" % <version>, // Required dependency
)Note: Declinio uses
Providedscope for its dependencies, giving you full control over the versions of Decline and Cats Effect in your project.
Create a command-line application that parses arguments using IODeclineApp:
import cats.effect.*
import com.colofabrix.scala.declinio.*
import com.monovore.decline.*
// Define your configuration case class
case class Config(
name: String,
count: Int,
verbose: Boolean
)
object MyApp extends IODeclineApp[Config] {
override def name: String =
"my-app"
override def header: String =
"A simple example application"
override def version: String =
"1.0.0"
override def options: Opts[Config] =
val nameOpt = Opts.option[String]("name", help = "Your name")
val countOpt = Opts.option[Int]("count", help = "Number of greetings").withDefault(1)
val verboseOpt = Opts.flag("verbose", help = "Verbose output").orFalse
(nameOpt, countOpt, verboseOpt).mapN(Config.apply)
override def runWithConfig(config: Config): IO[ExitCode] =
for
_ <- IO.println(s"Hello, ${config.name}!")
_ <- if (config.verbose) IO.println(s"Greeting you ${config.count} times...") else IO.unit
_ <- (1 until config.count).toList.traverse_(_ => IO.println(s"Hello ${config.name}!"))
yield ExitCode.Success
}Run with:
sbt "run --name World --count 3 --verbose"For applications that don't need command-line arguments, use IOUnitDeclineApp:
import cats.effect.*
import com.colofabrix.scala.declinio.*
object SimpleApp extends IOUnitDeclineApp {
override def name: String =
"simple-app"
override def header: String =
"A simple application without arguments"
override def version: String =
"1.0.0"
override def runNoConfig: IO[ExitCode] =
IO.println("Hello, World!").as(ExitCode.Success)
}For applications that prefer using ReaderT for dependency injection patterns, use
IODeclineReaderApp:
import cats.effect.*
import cats.data.ReaderT
import com.colofabrix.scala.declinio.*
import com.monovore.decline.*
case class AppConfig(
apiUrl: String,
timeout: Int
)
object ReaderApp extends IODeclineReaderApp[AppConfig] {
override def name: String =
"reader-app"
override def header: String =
"Application using ReaderT"
override def version: String =
"1.0.0"
override def options: Opts[AppConfig] =
val apiUrl = Opts.option[String]("api-url", help = "API endpoint URL")
val timeout = Opts.option[Int]("timeout", help = "Request timeout in seconds").withDefault(30)
(apiUrl, timeout).mapN(AppConfig.apply)
override def runWithReader: ReaderT[IO, AppConfig, ExitCode] =
ReaderT { config =>
for
_ <- IO.println(s"Connecting to ${config.apiUrl}")
_ <- IO.println(s"Timeout: ${config.timeout}s")
yield ExitCode.Success
}
}For applications using a custom effect type other than IO, extend DeclineApp or
DeclineReaderApp directly and provide a natural transformation from your effect to IO:
import cats.~>
import cats.effect.*
import cats.data.ReaderT
import com.colofabrix.scala.declinio.*
import com.monovore.decline.*
// Example: Using a custom effect type with environment
type AppIO[A] = ReaderT[IO, AppEnv, A]
case class AppEnv(logger: String => IO[Unit])
case class Config(debug: Boolean)
object CustomEffectApp extends DeclineReaderApp[AppIO, Config] {
override def name: String =
"custom-app"
override def header: String =
"Application with custom effect"
override def version: String =
"1.0.0"
override def options: Opts[Config] =
Opts
.flag("debug", "Enable debug mode", short = "d")
.orFalse
.map(Config.apply)
// Provide the natural transformation from AppIO to IO
override protected def runEffectToIO: AppIO ~> IO =
new (AppIO ~> IO):
def apply[A](fa: AppIO[A]): IO[A] =
fa.run(AppEnv(msg => IO.println(s"[LOG] $msg")))
override def runWithReader: ReaderT[AppIO, Config, ExitCode] =
ReaderT { config =>
ReaderT { env =>
for
_ <- env.logger(s"Debug mode: ${config.debug}")
_ <- IO.println("Application running")
yield ExitCode.Success
}
}
}Declinio provides a hierarchy of traits to suit different use cases:
DeclineReaderApp[F[_], A] (base trait, uses ReaderT)
├── DeclineApp[F[_], A] (uses runWithConfig method)
│ └── IODeclineApp[A] (IO-specific)
│ └── IOUnitDeclineApp (no configuration)
└── IODeclineReaderApp[A] (IO-specific, uses ReaderT)
The base trait that provides Decline integration for any effect type F[_]. Uses ReaderT to pass
the parsed configuration to the application.
| Member | Type | Description |
|---|---|---|
name |
String |
Name of the application (required) |
header |
String |
Short description of the application (required) |
version |
String |
Version of the application (optional, default: "") |
options |
Opts[A] |
Decline command-line options (required) |
helpFlag |
Boolean |
Display help on wrong arguments (default: true) |
runWithReader |
ReaderT[F, A, ExitCode] |
Main application logic using ReaderT (required) |
runEffectToIO |
F ~> IO |
Natural transformation from F to IO (required) |
Extends DeclineReaderApp and provides a simpler interface using runWithConfig instead of ReaderT.
| Member | Type | Description |
|---|---|---|
runWithConfig |
A => F[ExitCode] |
Main application logic (required) |
A convenience trait that extends DeclineReaderApp[IO, A] with a pre-defined identity
transformation for IO. Use this when you want to use ReaderT with IO.
A convenience trait that extends DeclineApp[IO, A]. Use this for standard Cats Effect IO
applications with the simple runWithConfig interface.
A convenience trait for applications that don't need command-line arguments. Extends
IODeclineApp[Unit].
| Member | Type | Description |
|---|---|---|
runNoConfig |
IO[ExitCode] |
Main application logic (required) |
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Declinio is released under the MIT license. See LICENSE for details.
- Decline - Composable command-line parsing for Scala
- Cats Effect - The pure asynchronous runtime for Scala