From c39a35d20713ea65ffeef021002d5162f4e5038a Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sat, 4 Nov 2023 14:09:17 +0100 Subject: [PATCH 1/7] Add support for skunk --- README.md | 1 + build.sc | 21 +++++++ docs/_docs/modules/skunk.md | 62 +++++++++++++++++++ docs/sidebar.yml | 1 + skunk/src/io.github.iltotore.iron/skunk.scala | 33 ++++++++++ .../github/iltotore/iron/SkunkExample.scala | 21 +++++++ .../io/github/iltotore/iron/SkunkSuite.scala | 53 ++++++++++++++++ 7 files changed, 192 insertions(+) create mode 100644 docs/_docs/modules/skunk.md create mode 100644 skunk/src/io.github.iltotore.iron/skunk.scala create mode 100644 skunk/test/src/io/github/iltotore/iron/SkunkExample.scala create mode 100644 skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala diff --git a/README.md b/README.md index f0bccb0f..4ecad444 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ ivy"io.github.iltotore::iron:version" | iron-circe | ✔️ | ✔️ | ✔️ | | iron-ciris | ✔️ | ✔️ | ✔️ | | iron-jsoniter | ✔️ | ✔️ | ✔️ | +| iron-skunk | ✔️ | ✔️ | ✔️ | | iron-scalacheck | ✔️ | ✔️ | ❌ | | iron-zio | ✔️ | ✔️ | ❌ | | iron-zio-json | ✔️ | ✔️ | ❌ | diff --git a/build.sc b/build.sc index c62e6f78..091120e8 100644 --- a/build.sc +++ b/build.sc @@ -389,6 +389,27 @@ object jsoniter extends SubModule { } } +object skunk extends SubModule { + + def artifactName = "iron-skunk" + + def ivyDeps = Agg( + ivy"org.tpolecat::skunk-core::0.6.1" + ) + + object test extends Tests { + def ivyDeps = Agg( + ivy"com.lihaoyi::utest:0.8.1", + ivy"org.tpolecat::skunk-core::0.6.1" + ) + } + + object js extends JSCrossModule + + object native extends NativeCrossModule + +} + object scalacheck extends SubModule { def artifactName = "iron-scalacheck" diff --git a/docs/_docs/modules/skunk.md b/docs/_docs/modules/skunk.md new file mode 100644 index 00000000..a5d0ee05 --- /dev/null +++ b/docs/_docs/modules/skunk.md @@ -0,0 +1,62 @@ +--- +title: "Skunk Support" +--- + +# Skunk Support + +This module provides refined types Codec/Encoder/Decoder instances for [Skunk](https://typelevel.org/skunk). + +## Dependency + +SBT: + +```scala +libraryDependencies += "io.github.iltotore" %% "iron-skunk" % "version" +``` + +Mill: + +```scala +ivy"io.github.iltotore::iron-skunk:version" +``` + +### Following examples' dependencies + +SBT: + +```scala +libraryDependencies += "org.tpolecat" %% "skunk-core" % "0.6.1" +``` + +Mill: + +```scala +ivy"org.tpolecat::skunk-core::0.6.1" +``` + +## Codec instances + +Iron provides `Codec` instances for refined types: + +```scala +import skunk.* +import skunk.implicits.* +import skunk.codec.all.* +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* +import io.github.iltotore.iron.skunk.* +import io.github.iltotore.iron.skunk.given + +type Username = String :| Not[Blank] + +// refine a codec implicitly +val a: Query[Void, Username] = sql"SELECT name FROM users".query(varchar) + +// refine a codec explictly +val b: Query[Void, Username] = sql"SELECT name FROM users".query(varchar.refined) + +// defining a codec for a refined case class +final case class User(name: Username, age: Int :| Positive) +given Codec[User] = (varchar.refined[Not[Blank]] *: int4.refined[Positive]).to[User] + +``` diff --git a/docs/sidebar.yml b/docs/sidebar.yml index 6f32d724..2e7dd4e6 100644 --- a/docs/sidebar.yml +++ b/docs/sidebar.yml @@ -22,6 +22,7 @@ subsection: - page: modules/circe.md - page: modules/ciris.md - page: modules/jsoniter.md + - page: modules/skunk.md - page: modules/scalacheck.md - page: modules/zio.md - page: modules/zio-json.md diff --git a/skunk/src/io.github.iltotore.iron/skunk.scala b/skunk/src/io.github.iltotore.iron/skunk.scala new file mode 100644 index 00000000..271dbe60 --- /dev/null +++ b/skunk/src/io.github.iltotore.iron/skunk.scala @@ -0,0 +1,33 @@ +package io.github.iltotore.iron + +import _root_.skunk.* + +import scala.Conversion + +object skunk: + + /** + * Explicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. + * + * @param constraint the [[Constraint]] implementation to test the decoded value + */ + extension [A](codec: Codec[A]) + inline def refined[C](using inline constraint: Constraint[A, C]): Codec[A :| C] = + codec.eimap[A :| C](_.refineEither[C])(_.asInstanceOf[A]) + + /** + * Implicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. + * + * @param constraint the [[Constraint]] implementation to test the decoded value + */ + inline given [A, C](using inline constraint: Constraint[A, C]): Conversion[Codec[A], Codec[A :| C]] = + new Conversion[Codec[A], Codec[A :| C]]: + override def apply(codec: Codec[A]): Codec[A :| C] = codec.refined + + /** + * A [[Codec]] for refined types. Decodes to the underlying type then checks the constraint. + * + * @param codec the [[Codec]] of the underlying type + * @param constraint the [[Constraint]] implementation to test the decoded value + */ + inline given [A, C](using inline codec: Codec[A], inline constraint: Constraint[A, C]): Codec[A :| C] = codec.refined diff --git a/skunk/test/src/io/github/iltotore/iron/SkunkExample.scala b/skunk/test/src/io/github/iltotore/iron/SkunkExample.scala new file mode 100644 index 00000000..7f0e5cc3 --- /dev/null +++ b/skunk/test/src/io/github/iltotore/iron/SkunkExample.scala @@ -0,0 +1,21 @@ +package io.github.iltotore.iron + +import _root_.skunk.* +import _root_.skunk.implicits.* +import _root_.skunk.codec.all.* +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* +import io.github.iltotore.iron.skunk.* +import io.github.iltotore.iron.skunk.given + +type Username = String :| Not[Blank] + +// refine a codec implicitly +val a: Query[Void, Username] = sql"SELECT name FROM users".query(varchar) + +// refine a codec explictly +val b: Query[Void, Username] = sql"SELECT name FROM users".query(varchar.refined) + +// defining a codec for a refined case class +final case class User(name: Username, age: Int :| Positive) +given Codec[User] = (varchar.refined[Not[Blank]] *: int4.refined[Positive]).to[User] diff --git a/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala b/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala new file mode 100644 index 00000000..ce879428 --- /dev/null +++ b/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala @@ -0,0 +1,53 @@ +package io.github.iltotore.iron + +import _root_.skunk.* +import _root_.skunk.given +import _root_.skunk.codec.all.* +import io.github.iltotore.iron.constraint.all.* +import io.github.iltotore.iron.skunk.given +import io.github.iltotore.iron.* +import utest.* + +opaque type PositiveInt = Int :| Positive +object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt] + +given Codec[Int] = int4 + +val tests: Tests = Tests { + + test("codec") { + test("ironType") { + test("success") - assert(summon[Codec[Int :| Positive]].decode(0, List(Some("5"))) == Right(5)) + test("failure") - assert(summon[Codec[Int :| Positive]].decode(0, List(Some("-5"))).isLeft) + test("success") - assert(summon[Codec[Int :| Positive]].encode(5) == List(Some("5"))) + } + + test("newType") { + test("success") - assert(summon[Codec[PositiveInt]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) + test("failure") - assert(summon[Codec[PositiveInt]].decode(0, List(Some("-5"))).isLeft) + test("success") - assert(summon[Codec[PositiveInt]].encode(PositiveInt(5)) == List(Some("5"))) + } + } + + test("encoder") { + test("ironType") { + test("success") - assert(summon[Encoder[Int :| Positive]].encode(5) == List(Some("5"))) + } + + test("newType") { + test("success") - assert(summon[Encoder[PositiveInt]].encode(PositiveInt(5)) == List(Some("5"))) + } + } + + test("decoder") { + test("ironType") { + test("success") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) + test("failure") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("-5"))).isLeft) + } + + test("newType") { + test("success") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) + test("failure") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("-5"))).isLeft) + } + } +} From c3ad98d6c8762cd2a193a8da4a59915d239451c8 Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sat, 18 Nov 2023 10:57:57 +0100 Subject: [PATCH 2/7] Fix doc module missing --- build.sc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sc b/build.sc index 091120e8..4b827e5d 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, zio, zioJson) + val modules: Seq[ScalaModule] = Seq(main, cats, circe, upickle, ciris, jsoniter, scalacheck, skunk, zio, zioJson) def docSources = T.sources { T.traverse(modules)(_.docSources)().flatten From 9cc68212bb4de95a8342edfbcf4f6681279113c0 Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sat, 18 Nov 2023 10:58:10 +0100 Subject: [PATCH 3/7] Fix doc order --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ecad444..72311cc1 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ ivy"io.github.iltotore::iron:version" | iron-circe | ✔️ | ✔️ | ✔️ | | iron-ciris | ✔️ | ✔️ | ✔️ | | iron-jsoniter | ✔️ | ✔️ | ✔️ | -| iron-skunk | ✔️ | ✔️ | ✔️ | | iron-scalacheck | ✔️ | ✔️ | ❌ | +| iron-skunk | ✔️ | ✔️ | ✔️ | | iron-zio | ✔️ | ✔️ | ❌ | | iron-zio-json | ✔️ | ✔️ | ❌ | From 241491055adc390d70795e99bba6294dbf27b11e Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sat, 18 Nov 2023 10:58:50 +0100 Subject: [PATCH 4/7] Fix test checking on wrong type --- skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala b/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala index ce879428..0db26fbe 100644 --- a/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala +++ b/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala @@ -41,8 +41,8 @@ val tests: Tests = Tests { test("decoder") { test("ironType") { - test("success") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) - test("failure") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("-5"))).isLeft) + test("success") - assert(summon[Decoder[Int :| Positive]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) + test("failure") - assert(summon[Decoder[Int :| Positive]].decode(0, List(Some("-5"))).isLeft) } test("newType") { From 279f312698c9a5bcd3543d9b44f83d3c92340d51 Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sat, 18 Nov 2023 11:10:39 +0100 Subject: [PATCH 5/7] Fix test suite not declared correctly --- .../io/github/iltotore/iron/SkunkSuite.scala | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala b/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala index 0db26fbe..0bfc0ed3 100644 --- a/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala +++ b/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala @@ -13,41 +13,43 @@ object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt] given Codec[Int] = int4 -val tests: Tests = Tests { - - test("codec") { - test("ironType") { - test("success") - assert(summon[Codec[Int :| Positive]].decode(0, List(Some("5"))) == Right(5)) - test("failure") - assert(summon[Codec[Int :| Positive]].decode(0, List(Some("-5"))).isLeft) - test("success") - assert(summon[Codec[Int :| Positive]].encode(5) == List(Some("5"))) +object SkunkSuite extends TestSuite: + + val tests: Tests = Tests { + + test("codec") { + test("ironType") { + test("success") - assert(summon[Codec[Int :| Positive]].decode(0, List(Some("5"))) == Right(5)) + test("failure") - assert(summon[Codec[Int :| Positive]].decode(0, List(Some("-5"))).isLeft) + test("success") - assert(summon[Codec[Int :| Positive]].encode(5) == List(Some("5"))) + } + + test("newType") { + test("success") - assert(summon[Codec[PositiveInt]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) + test("failure") - assert(summon[Codec[PositiveInt]].decode(0, List(Some("-5"))).isLeft) + test("success") - assert(summon[Codec[PositiveInt]].encode(PositiveInt(5)) == List(Some("5"))) + } } - test("newType") { - test("success") - assert(summon[Codec[PositiveInt]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) - test("failure") - assert(summon[Codec[PositiveInt]].decode(0, List(Some("-5"))).isLeft) - test("success") - assert(summon[Codec[PositiveInt]].encode(PositiveInt(5)) == List(Some("5"))) - } - } + test("encoder") { + test("ironType") { + test("success") - assert(summon[Encoder[Int :| Positive]].encode(5) == List(Some("5"))) + } - test("encoder") { - test("ironType") { - test("success") - assert(summon[Encoder[Int :| Positive]].encode(5) == List(Some("5"))) + test("newType") { + test("success") - assert(summon[Encoder[PositiveInt]].encode(PositiveInt(5)) == List(Some("5"))) + } } - test("newType") { - test("success") - assert(summon[Encoder[PositiveInt]].encode(PositiveInt(5)) == List(Some("5"))) - } - } - - test("decoder") { - test("ironType") { - test("success") - assert(summon[Decoder[Int :| Positive]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) - test("failure") - assert(summon[Decoder[Int :| Positive]].decode(0, List(Some("-5"))).isLeft) - } + test("decoder") { + test("ironType") { + test("success") - assert(summon[Decoder[Int :| Positive]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) + test("failure") - assert(summon[Decoder[Int :| Positive]].decode(0, List(Some("-5"))).isLeft) + } - test("newType") { - test("success") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) - test("failure") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("-5"))).isLeft) + test("newType") { + test("success") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("5"))) == Right(PositiveInt(5))) + test("failure") - assert(summon[Decoder[PositiveInt]].decode(0, List(Some("-5"))).isLeft) + } } } -} From a6a5b4e0c1e13bbb85ae3dbfc14fe3184186b4ea Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sat, 18 Nov 2023 11:26:56 +0100 Subject: [PATCH 6/7] Get rid of implicit conversion --- docs/_docs/modules/skunk.md | 13 +++++++------ skunk/src/io.github.iltotore.iron/skunk.scala | 11 ----------- .../src/io/github/iltotore/iron/SkunkExample.scala | 12 +++++++----- .../src/io/github/iltotore/iron/SkunkSuite.scala | 14 ++++++++------ 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/docs/_docs/modules/skunk.md b/docs/_docs/modules/skunk.md index a5d0ee05..a37fcf56 100644 --- a/docs/_docs/modules/skunk.md +++ b/docs/_docs/modules/skunk.md @@ -49,14 +49,15 @@ import io.github.iltotore.iron.skunk.given type Username = String :| Not[Blank] -// refine a codec implicitly -val a: Query[Void, Username] = sql"SELECT name FROM users".query(varchar) +// refining a codec at usage site +val a: Query[Void, Username] = sql"SELECT name FROM users".query(varchar.refined) -// refine a codec explictly -val b: Query[Void, Username] = sql"SELECT name FROM users".query(varchar.refined) +// defining a codec for a refined opaque type +opaque type PositiveInt = Int :| Positive +object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]: + given codec: Codec[PositiveInt] = int4.refined[Positive] // defining a codec for a refined case class final case class User(name: Username, age: Int :| Positive) -given Codec[User] = (varchar.refined[Not[Blank]] *: int4.refined[Positive]).to[User] - +given Codec[User] = (varchar.refined[Not[Blank]] *: PositiveInt.codec).to[User] ``` diff --git a/skunk/src/io.github.iltotore.iron/skunk.scala b/skunk/src/io.github.iltotore.iron/skunk.scala index 271dbe60..76184b26 100644 --- a/skunk/src/io.github.iltotore.iron/skunk.scala +++ b/skunk/src/io.github.iltotore.iron/skunk.scala @@ -2,8 +2,6 @@ package io.github.iltotore.iron import _root_.skunk.* -import scala.Conversion - object skunk: /** @@ -15,15 +13,6 @@ object skunk: inline def refined[C](using inline constraint: Constraint[A, C]): Codec[A :| C] = codec.eimap[A :| C](_.refineEither[C])(_.asInstanceOf[A]) - /** - * Implicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. - * - * @param constraint the [[Constraint]] implementation to test the decoded value - */ - inline given [A, C](using inline constraint: Constraint[A, C]): Conversion[Codec[A], Codec[A :| C]] = - new Conversion[Codec[A], Codec[A :| C]]: - override def apply(codec: Codec[A]): Codec[A :| C] = codec.refined - /** * A [[Codec]] for refined types. Decodes to the underlying type then checks the constraint. * diff --git a/skunk/test/src/io/github/iltotore/iron/SkunkExample.scala b/skunk/test/src/io/github/iltotore/iron/SkunkExample.scala index 7f0e5cc3..ab4d84a0 100644 --- a/skunk/test/src/io/github/iltotore/iron/SkunkExample.scala +++ b/skunk/test/src/io/github/iltotore/iron/SkunkExample.scala @@ -10,12 +10,14 @@ import io.github.iltotore.iron.skunk.given type Username = String :| Not[Blank] -// refine a codec implicitly -val a: Query[Void, Username] = sql"SELECT name FROM users".query(varchar) +// refining a codec at usage site +val a: Query[Void, Username] = sql"SELECT name FROM users".query(varchar.refined) -// refine a codec explictly -val b: Query[Void, Username] = sql"SELECT name FROM users".query(varchar.refined) +// defining a codec for a refined opaque type +opaque type PositiveInt = Int :| Positive +object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]: + given codec: Codec[PositiveInt] = int4.refined[Positive] // defining a codec for a refined case class final case class User(name: Username, age: Int :| Positive) -given Codec[User] = (varchar.refined[Not[Blank]] *: int4.refined[Positive]).to[User] +given Codec[User] = (varchar.refined[Not[Blank]] *: PositiveInt.codec).to[User] diff --git a/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala b/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala index 0bfc0ed3..e7552678 100644 --- a/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala +++ b/skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala @@ -1,19 +1,21 @@ package io.github.iltotore.iron import _root_.skunk.* -import _root_.skunk.given +import _root_.skunk.implicits.* import _root_.skunk.codec.all.* +import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.all.* +import io.github.iltotore.iron.skunk.* import io.github.iltotore.iron.skunk.given -import io.github.iltotore.iron.* import utest.* -opaque type PositiveInt = Int :| Positive -object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt] +object SkunkSuite extends TestSuite: -given Codec[Int] = int4 + given Codec[Int] = int4 -object SkunkSuite extends TestSuite: + opaque type PositiveInt = Int :| Positive + object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]: + given Codec[PositiveInt] = summon[Codec[Int]].refined val tests: Tests = Tests { From 2cf9a000ffc8d1e70a34f9477102440ed69e6a62 Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sat, 18 Nov 2023 11:29:47 +0100 Subject: [PATCH 7/7] Add missing skunk Javadoc mapping --- build.sc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sc b/build.sc index 4b827e5d..3fd5a151 100644 --- a/build.sc +++ b/build.sc @@ -139,7 +139,8 @@ object docs extends BaseModule { ".*zio.prelude.*" -> ("scaladoc3", "https://javadoc.io/doc/dev.zio/zio-prelude-docs_3/latest/"), ".*zio[^\\.json].*" -> ("scaladoc3", "https://javadoc.io/doc/dev.zio/zio_3/latest/"), ".*org.scalacheck.*" -> ("scaladoc3", "https://javadoc.io/doc/org.scalacheck/scalacheck_3/latest/"), - ".*scala.*" -> ("scaladoc3", "https://scala-lang.org/api/3.x/") + ".*org.scalacheck.*" -> ("scaladoc3", "https://javadoc.io/doc/org.scalacheck/scalacheck_3/latest/"), + ".*skunk.*" -> ("scaladoc3", "https://javadoc.io/doc/org.tpolecat/skunk-docs_3/latest/") ) def scalaDocOptions = {