diff --git a/README.md b/README.md index 48ac0c46..f0bccb0f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ ivy"io.github.iltotore::iron:version" | iron | ✔️ | ✔️ | ✔️ | | iron-cats | ✔️ | ✔️ | ✔️ | | iron-circe | ✔️ | ✔️ | ✔️ | +| iron-ciris | ✔️ | ✔️ | ✔️ | | iron-jsoniter | ✔️ | ✔️ | ✔️ | | iron-scalacheck | ✔️ | ✔️ | ❌ | | iron-zio | ✔️ | ✔️ | ❌ | diff --git a/build.sc b/build.sc index 8ca0c5ef..423fcb7a 100644 --- a/build.sc +++ b/build.sc @@ -133,6 +133,7 @@ object docs extends BaseModule { def externalMappings = Map( ".*cats.*" -> ("scaladoc3", "https://javadoc.io/doc/org.typelevel/cats-docs_3/latest/"), ".*io.circe.*" -> ("scaladoc2", "https://circe.github.io/circe/api/"), + ".*ciris.*" -> ("scaladoc2", "https://cir.is/api/"), ".*com.github.plokhotnyuk.jsoniter_scala.core.*" -> ("scaladoc3", "https://www.javadoc.io/doc/com.github.plokhotnyuk.jsoniter-scala/jsoniter-scala-core_3/latest/"), ".*zio.json.*" -> ("scaladoc3", "https://javadoc.io/doc/dev.zio/zio-json_3/latest/"), ".*zio.prelude.*" -> ("scaladoc3", "https://javadoc.io/doc/dev.zio/zio-prelude-docs_3/latest/"), diff --git a/ciris/src/io.github.iltotore.iron/ciris.scala b/ciris/src/io.github.iltotore.iron/ciris.scala index 33ed4db4..25bcbe89 100644 --- a/ciris/src/io.github.iltotore.iron/ciris.scala +++ b/ciris/src/io.github.iltotore.iron/ciris.scala @@ -1,11 +1,23 @@ package io.github.iltotore.iron -import _root_.ciris.ConfigDecoder -import cats.Show - +import _root_.ciris.{ConfigDecoder, ConfigError} object ciris: - inline given [T,A,B](using inline decoder: ConfigDecoder[T,A], inline constraint: _root_.io.github.iltotore.iron.Constraint[A, B], inline show: Show[A]): ConfigDecoder[T, A :| B] = - decoder.mapOption("")(_.refineOption) + /** + * A [[ConfigDecoder]] for refined types. Decodes to the underlying type then checks the constraint. + * + * @param decoder the [[ConfigDecoder]] of the underlying type + * @param constraint the [[Constraint]] implementation to test the decoded value + */ + inline given [In, A, C](using inline decoder: ConfigDecoder[In, A], inline constraint: Constraint[A, C]): ConfigDecoder[In, A :| C] = + decoder.mapEither((_, value) => value.refineEither[C].left.map(ConfigError(_))) + /** + * A [[ConfigDecoder]] for new types. Decodes to the underlying type then checks the constraint. + * + * @param decoder the [[ConfigDecoder]] of the underlying type. + * @param mirror the mirror of the [[RefinedTypeOps.Mirror]] + */ + inline given [In, T](using mirror: RefinedTypeOps.Mirror[T], decoder: ConfigDecoder[In, mirror.IronType]): ConfigDecoder[In, T] = + decoder.asInstanceOf[ConfigDecoder[In, T]] diff --git a/ciris/test/src/io/github/iltotore/iron/CirisSuite.scala b/ciris/test/src/io/github/iltotore/iron/CirisSuite.scala index 2c2e8ea4..439fad25 100644 --- a/ciris/test/src/io/github/iltotore/iron/CirisSuite.scala +++ b/ciris/test/src/io/github/iltotore/iron/CirisSuite.scala @@ -1,19 +1,22 @@ package io.github.iltotore.iron import _root_.ciris.ConfigDecoder +import io.github.iltotore.iron.ciris.given +import io.github.iltotore.iron.constraint.numeric.Positive import utest.* -import ciris.given - object CirisSuite extends TestSuite: val tests: Tests = Tests { - test("summon String => Int :| Pure") { - summon[ ConfigDecoder[String, Int :| Pure]] - } + test("decoder") { + test("ironType") { + test("success") - assert(summon[ConfigDecoder[String, Int :| Positive]].decode(None, "5") == Right(5)) + test("failure") - assert(summon[ConfigDecoder[String, Int :| Positive]].decode(None, "-5").isLeft) + } - test("summon from Int => Int :| Pure") { - summon[ConfigDecoder[Int, Int :| Pure]] + test("newType") { + test("success") - assert(summon[ConfigDecoder[String, Temperature]].decode(None, "5") == Right(Temperature(5))) + test("failure") - assert(summon[ConfigDecoder[String, Temperature]].decode(None, "-5").isLeft) + } } } - diff --git a/ciris/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala b/ciris/test/src/io/github/iltotore/iron/RefinedOpsTypes.scala new file mode 100644 index 00000000..4b0c33c5 --- /dev/null +++ b/ciris/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/ciris.md b/docs/_docs/modules/ciris.md new file mode 100644 index 00000000..0dd58f4d --- /dev/null +++ b/docs/_docs/modules/ciris.md @@ -0,0 +1,59 @@ +--- +title: "Ciris Support" +--- + +# Ciris Support + +This module provides refined types Encoder/Decoder instances for [Ciris](https://circe.github.io/circe/). + +## Dependency + +SBT: + +```scala +libraryDependencies += "io.github.iltotore" %% "iron-ciris" % "version" +``` + +Mill: + +```scala +ivy"io.github.iltotore::iron-ciris:version" +``` + +### Following examples' dependencies + +SBT: + +```scala +libraryDependencies += "is.cir" %% "ciris" % "3.1.0" +``` + +Mill: + +```scala +ivy"is.cir::ciris::3.1.0" +``` + +## ConfigDecoder instances + +Iron provides `ConfigDecoder` instances for refined types: + +```scala +import cats.syntax.all.* +import ciris.* + +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* +import io.github.iltotore.iron.cats.given +import io.github.iltotore.iron.ciris.given + +type Username = String :| (Not[Blank] & MaxLength[32]) +type Password = String :| (Not[Blank] & MinLength[9]) + +case class DatabaseConfig(username: Username, password: Secret[Password]) + +val databaseConfig: ConfigValue[Effect, DatabaseConfig] = ( + env("DB_USERNAME").as[Username], + env("DB_PASSWORD").as[Password].secret +).mapN(DatabaseConfig.apply) +``` \ No newline at end of file diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 7c172c0d..6f32d724 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -20,6 +20,7 @@ subsection: subsection: - page: modules/cats.md - page: modules/circe.md + - page: modules/ciris.md - page: modules/jsoniter.md - page: modules/scalacheck.md - page: modules/zio.md