diff --git a/.gitignore b/.gitignore index 2acfbbbb..d32ba5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ .scala-build *.iml tmp -out \ No newline at end of file +out +.history \ No newline at end of file diff --git a/README.md b/README.md index 72311cc1..94cb383d 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ ivy"io.github.iltotore::iron:version" | iron-cats | ✔️ | ✔️ | ✔️ | | iron-circe | ✔️ | ✔️ | ✔️ | | iron-ciris | ✔️ | ✔️ | ✔️ | +| iron-decline | ✔️ | ✔️ | ✔️ | | iron-jsoniter | ✔️ | ✔️ | ✔️ | | iron-scalacheck | ✔️ | ✔️ | ❌ | | iron-skunk | ✔️ | ✔️ | ✔️ | diff --git a/build.sc b/build.sc index 3daf9f29..9f2f041d 100644 --- a/build.sc +++ b/build.sc @@ -77,7 +77,7 @@ object docs extends BaseModule { def artifactName = "iron-docs" - val modules: Seq[ScalaModule] = Seq(main, cats, circe, upickle, ciris, jsoniter, scalacheck, skunk, zio, zioJson) + val modules: Seq[ScalaModule] = Seq(main, cats, circe, decline, upickle, ciris, jsoniter, scalacheck, skunk, zio, zioJson) def docSources = T.sources { T.traverse(modules)(_.docSources)().flatten @@ -140,7 +140,8 @@ object docs extends BaseModule { ".*zio[^\\.json].*" -> ("scaladoc3", "https://javadoc.io/doc/dev.zio/zio_3/latest/"), ".*org.scalacheck.*" -> ("scaladoc3", "https://javadoc.io/doc/org.scalacheck/scalacheck_3/latest/"), ".*org.scalacheck.*" -> ("scaladoc3", "https://javadoc.io/doc/org.scalacheck/scalacheck_3/latest/"), - ".*skunk.*" -> ("scaladoc3", "https://javadoc.io/doc/org.tpolecat/skunk-docs_3/latest/") + ".*skunk.*" -> ("scaladoc3", "https://javadoc.io/doc/org.tpolecat/skunk-docs_3/latest/"), + ".*com.monovore.decline.*" -> ("scaladoc3", "https://javadoc.io/doc/com.monovore/decline_3/latest/") ) def scalaDocOptions = { @@ -434,3 +435,18 @@ object doobie extends SubModule { object test extends Tests } + +object decline extends SubModule { + + def artifactName = "iron-decline" + + def ivyDeps = Agg( + ivy"com.monovore::decline::2.4.1" + ) + + object test extends Tests + + object js extends JSCrossModule + + object native extends NativeCrossModule +} diff --git a/decline/src/io/github/iltotore/iron/decline.scala b/decline/src/io/github/iltotore/iron/decline.scala new file mode 100644 index 00000000..0911d526 --- /dev/null +++ b/decline/src/io/github/iltotore/iron/decline.scala @@ -0,0 +1,23 @@ +package io.github.iltotore.iron + +import _root_.com.monovore.decline.Argument +import _root_.io.github.iltotore.iron.* +import cats.data.Validated +import cats.data.Validated.Valid +import cats.data.Validated.Invalid +import cats.data.ValidatedNel // Add this import + +object decline: + inline given [A, B](using inline argument: Argument[A], inline constraint: Constraint[A, B]): Argument[A :| B] = + new Argument[A :| B]: + def read(string: String): ValidatedNel[String, A :| B] = + argument.read(string) match + case Valid(a) => a.refineEither[B] match + case Left(value) => Validated.invalidNel(value) + case Right(value) => Validated.validNel(value) + case Invalid(e) => Validated.invalid(e) + + def defaultMetavar: String = argument.defaultMetavar + + inline given [T](using mirror: RefinedTypeOps.Mirror[T], argument: Argument[mirror.IronType]): Argument[T] = + argument.asInstanceOf[Argument[T]] diff --git a/decline/test/src/io/github/iltotore/iron/DeclineSuite.scala b/decline/test/src/io/github/iltotore/iron/DeclineSuite.scala new file mode 100644 index 00000000..a1f0450c --- /dev/null +++ b/decline/test/src/io/github/iltotore/iron/DeclineSuite.scala @@ -0,0 +1,23 @@ +package io.github.iltotore.iron + +import _root_.com.monovore.decline.Argument +import _root_.cats.data.Validated.Valid +import io.github.iltotore.iron.decline.given +import io.github.iltotore.iron.constraint.numeric.Positive +import utest.* + +object DeclineSuite extends TestSuite: + val tests: Tests = Tests { + + test("Argument") { + test("ironType") { + test("success") - assert(summon[Argument[Int :| Positive]].read("5") == Valid(5)) + test("failure") - assert(summon[Argument[Int :| Positive]].read("-5").isInvalid) + } + + test("newType") { + test("success") - assert(summon[Argument[Temperature]].read("5") == Valid(5)) + test("failure") - assert(summon[Argument[Temperature]].read("-5").isInvalid) + } + } + } diff --git a/decline/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala b/decline/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala new file mode 100644 index 00000000..4b0c33c5 --- /dev/null +++ b/decline/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala @@ -0,0 +1,6 @@ +package io.github.iltotore.iron + +import io.github.iltotore.iron.constraint.numeric.Positive + +opaque type Temperature = Int :| Positive +object Temperature extends RefinedTypeOps[Int, Positive, Temperature] \ No newline at end of file diff --git a/docs/_docs/modules/decline.md b/docs/_docs/modules/decline.md new file mode 100644 index 00000000..62d3a6ef --- /dev/null +++ b/docs/_docs/modules/decline.md @@ -0,0 +1,72 @@ +--- +title: "Decline Support" +--- + +# Skunk Support + +This module provides refined types Argument instances for [Decline](https://ben.kirw.in/decline/). + +## Dependency + +SBT: + +```scala +libraryDependencies += "io.github.iltotore" %% "iron-decline" % "version" +``` + +Mill: + +```scala +ivy"io.github.iltotore::iron-decline:version" +``` + +### Following examples' dependencies + +SBT: + +```scala +libraryDependencies += "com.monovore" %% "decline" % "2.4.1" +``` + +Mill: + +```scala +ivy"com.monovore::decline::2.4.1" +``` + +## Argument instances + +Iron provides `Argument` instances for refined types: + +```scala +import cats.implicits.* +import com.monovore.decline.* +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* +import io.github.iltotore.iron.decline.given + +type Person = String :| Not[Blank] + +opaque type PositiveInt <: Int = Int :| Positive +object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt] + +object HelloWorld extends CommandApp( + name = "hello-world", + header = "Says hello!", + main = { + // Defining an option for a constrainted type + val userOpt = + Opts.option[Person]("target", help = "Person to greet.") + .withDefault("world") + + // Defining an option for a refined opaque type + val nOpt = + Opts.option[PositiveInt]("quiet", help = "Number of times message is printed.") + .withDefault(PositiveInt(1)) + + (userOpt, nOpt).mapN { (user, n) => + (1 to n).map(_ => println(s"Hello $user!")) + } + } +) +``` diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 2e7dd4e6..c7b233c1 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -21,6 +21,7 @@ subsection: - page: modules/cats.md - page: modules/circe.md - page: modules/ciris.md + - page: modules/decline.md - page: modules/jsoniter.md - page: modules/skunk.md - page: modules/scalacheck.md