Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for skunk #192

Merged
merged 7 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ ivy"io.github.iltotore::iron:version"
| iron-ciris | ✔️ | ✔️ | ✔️ |
| iron-jsoniter | ✔️ | ✔️ | ✔️ |
| iron-scalacheck | ✔️ | ✔️ | ❌ |
| iron-skunk | ✔️ | ✔️ | ✔️ |
| iron-zio | ✔️ | ✔️ | ❌ |
| iron-zio-json | ✔️ | ✔️ | ❌ |

Expand Down
26 changes: 24 additions & 2 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -389,6 +390,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"
Expand Down
63 changes: 63 additions & 0 deletions docs/_docs/modules/skunk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
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]

// refining a codec at usage site
val a: 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]] *: PositiveInt.codec).to[User]
```
1 change: 1 addition & 0 deletions docs/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 22 additions & 0 deletions skunk/src/io.github.iltotore.iron/skunk.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.iltotore.iron

import _root_.skunk.*

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])

/**
* 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
23 changes: 23 additions & 0 deletions skunk/test/src/io/github/iltotore/iron/SkunkExample.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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]

// refining a codec at usage site
val a: 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]] *: PositiveInt.codec).to[User]
57 changes: 57 additions & 0 deletions skunk/test/src/io/github/iltotore/iron/SkunkSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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
import utest.*

object SkunkSuite extends TestSuite:

given Codec[Int] = int4

opaque type PositiveInt = Int :| Positive
object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]:
given Codec[PositiveInt] = summon[Codec[Int]].refined

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[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)
}
}
}