From a8fb2c570b18dd5a98de14fe41301902a4f59e99 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 3 Aug 2022 07:01:03 -0700 Subject: [PATCH 1/5] Enable Pure Cross Sqlite --- build.sbt | 42 +++++++++- .../sqlitesjs/cross/PackagePlatform.scala | 11 +++ .../cross/SqliteCrossCompanionPlatform.scala | 44 ++++++++++ .../sqlitesjs/cross/PackagePlatform.scala | 9 ++ .../cross/SqliteCrossCompanionPlatform.scala | 28 +++++++ .../sqlitesjs/cross/IntBoolean.scala | 29 +++++++ .../sqlitesjs/cross/SqliteCross.scala | 20 +++++ .../sqlitesjs/cross/package.scala | 5 ++ examples-cross/src/main/scala/Example.scala | 82 +++++++++++++++++++ 9 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 cross/js/src/main/scala/io/chrisdavenport/sqlitesjs/cross/PackagePlatform.scala create mode 100644 cross/js/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCrossCompanionPlatform.scala create mode 100644 cross/jvm/src/main/scala/io/chrisdavenport/sqlitesjs/cross/PackagePlatform.scala create mode 100644 cross/jvm/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCrossCompanionPlatform.scala create mode 100644 cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala create mode 100644 cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCross.scala create mode 100644 cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/package.scala create mode 100644 examples-cross/src/main/scala/Example.scala diff --git a/build.sbt b/build.sbt index c6526e2..884d026 100644 --- a/build.sbt +++ b/build.sbt @@ -18,8 +18,8 @@ ThisBuild / scalaVersion := Scala213 ThisBuild / testFrameworks += new TestFramework("munit.Framework") val catsV = "2.7.0" -val catsEffectV = "3.3.12" -val fs2V = "3.2.7" +val catsEffectV = "3.3.14" +val fs2V = "3.2.9" val http4sV = "0.23.11" val circeV = "0.14.2" val doobieV = "1.0.0-RC2" @@ -28,7 +28,7 @@ val munitCatsEffectV = "1.0.7" // Projects lazy val `sqlite-sjs` = tlCrossRootProject - .aggregate(core, examples) + .aggregate(core, examples, cross, `examples-cross`) lazy val core = project.in(file("core")) .enablePlugins(ScalaJSPlugin) @@ -67,6 +67,42 @@ lazy val examples = project scalaJSUseMainModuleInitializer := true, ) +lazy val cross = crossProject(JVMPlatform, JSPlatform) + .crossType(CrossType.Full) + .in(file("cross")) + .settings( + name := "sqlite-sjs-cross", + libraryDependencies ++= Seq( + "io.circe" %%% "circe-core" % circeV, + ) + ).jsSettings( + scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)}, + ).jsConfigure { project => project.dependsOn(core) } + .jvmSettings( + libraryDependencies ++= Seq( + "org.tpolecat" %% "doobie-core" % doobieV, + "org.xerial" % "sqlite-jdbc" % "3.36.0.3", + ) + ) + +lazy val `examples-cross` = crossProject(JVMPlatform, JSPlatform) + .crossType(CrossType.Pure) + .in(file("examples-cross")) + .enablePlugins(NoPublishPlugin) + .settings( + libraryDependencies ++= Seq ( + "io.circe" %%% "circe-generic" % circeV, + "co.fs2" %%% "fs2-io" % fs2V, + ) + ) + .dependsOn(cross) + .jsConfigure(project => project.enablePlugins(ScalaJSBundlerPlugin)) + .jsSettings( + scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)}, + scalaJSUseMainModuleInitializer := true, + ) + + // lazy val shims = project // .in(file("shims")) // .enablePlugins(NoPublishPlugin) diff --git a/cross/js/src/main/scala/io/chrisdavenport/sqlitesjs/cross/PackagePlatform.scala b/cross/js/src/main/scala/io/chrisdavenport/sqlitesjs/cross/PackagePlatform.scala new file mode 100644 index 0000000..a2b64e7 --- /dev/null +++ b/cross/js/src/main/scala/io/chrisdavenport/sqlitesjs/cross/PackagePlatform.scala @@ -0,0 +1,11 @@ +package io.chrisdavenport.sqlitesjs.cross + +// import io.chrisdavenport.sqlitesjs.Sqlite + +trait PackagePlatform { + // Warning: SJS encodes Booleans as Ints. If using boolean it will fail, use IntBoolean directly + // for automatic or contramap from Decoder[IntBoolean] to build your decoder for use with booleans. + type Write[A] = io.circe.Encoder[A] + type Read[A] = io.circe.Decoder[A] + +} \ No newline at end of file diff --git a/cross/js/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCrossCompanionPlatform.scala b/cross/js/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCrossCompanionPlatform.scala new file mode 100644 index 0000000..7abba1d --- /dev/null +++ b/cross/js/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCrossCompanionPlatform.scala @@ -0,0 +1,44 @@ +package io.chrisdavenport.sqlitesjs.cross + +import cats.effect.kernel._ +import cats.syntax.all._ +import io.chrisdavenport.sqlitesjs.Sqlite +import io.circe.{Json, JsonNumber, JsonObject} + +trait SqliteCrossCompanionPlatform { + def impl[F[_]: Async](path: String): Resource[F, SqliteCross[F]] = { + Sqlite.fromFile(path).map(new SqliteCrossJSImpl[F](_)) + } + + private class SqliteCrossJSImpl[F[_]: Async](sqlite: Sqlite[F]) extends SqliteCross[F]{ + + private def encode[A: Write](a: A): List[Json] = { + val json = io.circe.Encoder[A].apply(a) + def toList(json: Json): List[Json] = { + json.fold( + jsonNull = List(Json.Null), + jsonBoolean = {(bool: Boolean) => List({if (bool) Json.fromInt(1) else Json.fromInt(0)})}, // Throw maybe + jsonNumber = {(number: JsonNumber) => List(Json.fromJsonNumber(number))}, + jsonString = {(s: String) => List(Json.fromString(s))}, + jsonArray = {(v: Vector[Json]) => v.toList}, // Throw Maybe + jsonObject = {(obj: JsonObject) => obj.toList.flatMap(a => toList(a._2))} // Throw maybe? + ) + } + toList(json) + } + + def exec(sql: String): F[Unit] = sqlite.exec(sql) + def get[B: Read](sql: String): F[Option[B]] = sqlite.get(sql).flatMap(_.traverse(_.as[B]).liftTo[F]) + def get[A: Write, B: Read](sql: String, write: A): F[Option[B]] = { + sqlite.get(sql, encode(write)).flatMap(_.traverse(_.as[B]).liftTo[F]) + } + + def all[B: Read](sql: String): F[List[B]] = + sqlite.all(sql).flatMap(_.traverse(_.as[B]).liftTo[F]) + def all[A: Write, B: Read](sql: String, write: A): F[List[B]] = + sqlite.all(sql, encode(write)).flatMap(_.traverse(_.as[B]).liftTo[F]) + + def run(sql: String): F[Int] = sqlite.run(sql) + def run[A: Write](sql: String, write: A): F[Int] = sqlite.run(sql, encode(write)) + } +} \ No newline at end of file diff --git a/cross/jvm/src/main/scala/io/chrisdavenport/sqlitesjs/cross/PackagePlatform.scala b/cross/jvm/src/main/scala/io/chrisdavenport/sqlitesjs/cross/PackagePlatform.scala new file mode 100644 index 0000000..ea1c0d2 --- /dev/null +++ b/cross/jvm/src/main/scala/io/chrisdavenport/sqlitesjs/cross/PackagePlatform.scala @@ -0,0 +1,9 @@ +package io.chrisdavenport.sqlitesjs.cross + + + +trait PackagePlatform { + // type SqliteConnection[F[_]] = doobie.Transactor[F] + type Write[A] = doobie.Write[A] + type Read[A] = doobie.Read[A] +} \ No newline at end of file diff --git a/cross/jvm/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCrossCompanionPlatform.scala b/cross/jvm/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCrossCompanionPlatform.scala new file mode 100644 index 0000000..5ee8265 --- /dev/null +++ b/cross/jvm/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCrossCompanionPlatform.scala @@ -0,0 +1,28 @@ +package io.chrisdavenport.sqlitesjs.cross + +import doobie.{Write => _, Read => _, _} +import doobie.syntax.all._ +import cats.effect.kernel._ +import cats.syntax.all._ + +trait SqliteCrossCompanionPlatform { + def impl[F[_]: Async](path: String): Resource[F, SqliteCross[F]] = Resource.pure[F, SqliteCross[F]]{ + val ts = Transactor.fromDriverManager[F]("org.sqlite.JDBC", s"jdbc:sqlite:${path.toString}", "", "") + new SqliteCrossJVMImpl[F](ts) + } + + private class SqliteCrossJVMImpl[F[_]: MonadCancelThrow](ts: Transactor[F]) extends SqliteCross[F]{ + def exec(sql: String): F[Unit] = Update(sql).run(()).transact(ts).void + def get[B: Read](sql: String): F[Option[B]] = Query0(sql).option.transact(ts) + def get[A: Write, B: Read](sql: String, write: A): F[Option[B]] = + Query[A, B](sql).option(write).transact(ts) + + def all[B: Read](sql: String): F[List[B]] = + Query0(sql).to[List].transact(ts) + def all[A: Write, B: Read](sql: String, write: A): F[List[B]] = + Query[A, B](sql).to[List](write).transact(ts) + + def run(sql: String): F[Int] = Update(sql).run(()).transact(ts) + def run[A: Write](sql: String, write: A): F[Int] = Update[A](sql).run(write).transact(ts) + } +} \ No newline at end of file diff --git a/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala b/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala new file mode 100644 index 0000000..c99a84e --- /dev/null +++ b/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala @@ -0,0 +1,29 @@ +package io.chrisdavenport.sqlitesjs.cross + +import io.circe._ +import cats.syntax.all._ + +// Use this rather than boolean in models, otherwise javascript platform will +// die. +case class IntBoolean(boolean: Boolean) extends AnyVal +object IntBoolean { + implicit val decoder: Decoder[IntBoolean] = new Decoder[IntBoolean]{ + def apply(c: HCursor): Decoder.Result[IntBoolean] = + Decoder[Int].emap{ + case 0 => IntBoolean(false).asRight + case 1 => IntBoolean(true).asRight + case other => s"Invalid IntBoolean: $other".asLeft + }.or(Decoder.decodeBoolean.map(IntBoolean(_)))(c) + } + + // implicit val decoder: Decoder[IntBoolean] = Decoder[Int].emap{ + // case 0 => IntBoolean(false).asRight + // case 1 => IntBoolean(true).asRight + // case other => s"Invalid IntBoolean: $other".asLeft + // } + + implicit val encoder: Encoder[IntBoolean] = Encoder[Int].contramap[IntBoolean]{ + case IntBoolean(true) => 1 + case IntBoolean(false) => 0 + } +} \ No newline at end of file diff --git a/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCross.scala b/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCross.scala new file mode 100644 index 0000000..3677540 --- /dev/null +++ b/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/SqliteCross.scala @@ -0,0 +1,20 @@ +package io.chrisdavenport.sqlitesjs.cross + + +trait SqliteCross[F[_]]{ + def exec(sql: String): F[Unit] + // Uses Positional Encoding + def get[B: Read](sql: String): F[Option[B]] + def get[A: Write, B: Read](sql: String, write: A): F[Option[B]] + + + def all[B: Read](sql: String): F[List[B]] + def all[A: Write, B: Read](sql: String, write: A): F[List[B]] + + def run(sql: String): F[Int] + def run[A: Write](sql: String, write: A): F[Int] +} + +object SqliteCross extends SqliteCrossCompanionPlatform {} + + diff --git a/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/package.scala b/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/package.scala new file mode 100644 index 0000000..253e86d --- /dev/null +++ b/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/package.scala @@ -0,0 +1,5 @@ +package io.chrisdavenport.sqlitesjs + +package object cross extends PackagePlatform { + +} diff --git a/examples-cross/src/main/scala/Example.scala b/examples-cross/src/main/scala/Example.scala new file mode 100644 index 0000000..765f07a --- /dev/null +++ b/examples-cross/src/main/scala/Example.scala @@ -0,0 +1,82 @@ +import io.chrisdavenport.sqlitesjs.cross._ + +import cats.syntax.all._ +import cats.effect._ + +object Example extends IOApp.Simple { + + def run: IO[Unit] = SqliteCross.impl[IO]("testing/testCrossExample.sqlite").use{ cross => + + for { + _ <- cross.exec(createTableStatement) + init <- cross.all[Hello](select) + _ <- if (init.isEmpty) IO.unit else IO.println("Initial State:") + _ <- init.traverse(h => IO.println(h)) + _ <- IO.println("Please Say Hello To Someone or say CLEAR:") + who <- fs2.io.stdin[IO](4096).through(fs2.text.utf8.decode).takeThrough(_.contains("\n")).compile.string.map(_.trim()) + // who <- std.Console[IO].readLine // BROKEN + _ <- if (who === "CLEAR") clear(cross) else sayHello(cross, who) + } yield () + } + + def sayHello(cross: SqliteCross[IO], name: String): IO[Unit] = for { + now <- Clock[IO].realTime.map(_.toMillis) + record = Hello(name, now, IntBoolean(name.toUpperCase() === name)) + _ <- cross.run(upsert, record) + _ <- IO.println("") >> IO.println("Current State:") + after <- cross.all[Hello](select) + _ <- after.traverse(h => IO.println(h)) + } yield () + + def clear(cross: SqliteCross[IO]): IO[Unit] = { + cross.run(del).flatMap(i => IO.println(s"Cleared $i rows")) + } + + case class Hello(name: String, lastAccessed: Long, allCaps: IntBoolean) + // IntBoolean is used to get booleans in a compatible fashion without custom encoding/generic logic. + // You can use a custom decoder which maps to Boolean from IntBoolean if you do want to prevent + // intermediates appearing in your code. + object Hello { + // Can use anything to get the decoder for this + // Generic for 2.12 + // derives Codec.AsObject for 3 + // implicit val codec: io.circe.Codec[Hello] = ??? + // In this example I use semi-auto derivation + implicit val codec: io.circe.Codec[Hello] = io.circe.generic.semiauto.deriveCodec[Hello] + + // An example of how to do this without use of custom type, but explicitly encoding/decoding + // implicit val decoder: Decoder[Hello] = new Decoder[Hello]{ + // def apply(h: HCursor) = for { + // name <- h.downField("name").as[String] + // accessed <- h.downField("lastAccessed").as[Long] + // allCaps <- h.downField("allCaps").as[IntBoolean].map(_.boolean) + // } yield Hello(name, accessed, allCaps) + // } + + // implicit val encoder: Encoder[Hello] = new Encoder[Hello]{ + // def apply(a: Hello): Json = Json.obj( + // "name" -> a.name.asJson, + // "lastAccessed" -> a.lastAccessed.asJson, + // "allCaps" -> IntBoolean(a.allCaps).asJson + // ) + // } + } + + val createTableStatement = { + """CREATE TABLE IF NOT EXISTS hello ( + |name TEXT NOT NULL PRIMARY KEY, + |lastAccessed INTEGER NOT NULL, + |allCaps INTEGER NOT NULL)""".stripMargin + } + + val select = { + "SELECT name,lastAccessed,allCaps FROM hello" + } + + val upsert = { + """INSERT OR REPLACE INTO hello (name, lastAccessed, allCaps) VALUES (?, ?, ?)""" + } + + val del = """DELETE FROM hello""" + +} \ No newline at end of file From c8aaf9a5fe60924793b89be4132dd17d267ac7c7 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 3 Aug 2022 07:03:09 -0700 Subject: [PATCH 2/5] Update Workflow --- .github/workflows/ci.yml | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c54d430..8250a55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: os: [ubuntu-latest] scala: [2.13.7, 3.1.1] java: [temurin@8] - project: [rootJS] + project: [rootJS, rootJVM] runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) @@ -86,11 +86,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p examples/target target .js/target site/target .jvm/target .native/target core/target project/target + run: mkdir -p cross/js/target examples/target target examples-cross/.js/target .js/target site/target cross/jvm/target .jvm/target .native/target core/target examples-cross/.jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar examples/target target .js/target site/target .jvm/target .native/target core/target project/target + run: tar cf targets.tar cross/js/target examples/target target examples-cross/.js/target .js/target site/target cross/jvm/target .jvm/target .native/target core/target examples-cross/.jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') @@ -153,6 +153,16 @@ jobs: tar xf targets.tar rm targets.tar + - name: Download target directories (2.13.7, rootJVM) + uses: actions/download-artifact@v2 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.7-rootJVM + + - name: Inflate target directories (2.13.7, rootJVM) + run: | + tar xf targets.tar + rm targets.tar + - name: Download target directories (3.1.1, rootJS) uses: actions/download-artifact@v2 with: @@ -163,6 +173,16 @@ jobs: tar xf targets.tar rm targets.tar + - name: Download target directories (3.1.1, rootJVM) + uses: actions/download-artifact@v2 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3.1.1-rootJVM + + - name: Inflate target directories (3.1.1, rootJVM) + run: | + tar xf targets.tar + rm targets.tar + - name: Import signing key if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' run: echo $PGP_SECRET | base64 -di | gpg --import From 81f37b51ef1a732ec2c3ecdbb143d36f8cf8c0bd Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 3 Aug 2022 07:07:26 -0700 Subject: [PATCH 3/5] Enable Derivation on Scala 3 --- .../scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala | 2 +- examples-cross/src/main/scala/Example.scala | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala b/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala index c99a84e..ad60f9f 100644 --- a/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala +++ b/cross/shared/src/main/scala/io/chrisdavenport/sqlitesjs/cross/IntBoolean.scala @@ -5,7 +5,7 @@ import cats.syntax.all._ // Use this rather than boolean in models, otherwise javascript platform will // die. -case class IntBoolean(boolean: Boolean) extends AnyVal +case class IntBoolean(boolean: Boolean) object IntBoolean { implicit val decoder: Decoder[IntBoolean] = new Decoder[IntBoolean]{ def apply(c: HCursor): Decoder.Result[IntBoolean] = diff --git a/examples-cross/src/main/scala/Example.scala b/examples-cross/src/main/scala/Example.scala index 765f07a..a091441 100644 --- a/examples-cross/src/main/scala/Example.scala +++ b/examples-cross/src/main/scala/Example.scala @@ -13,8 +13,9 @@ object Example extends IOApp.Simple { _ <- if (init.isEmpty) IO.unit else IO.println("Initial State:") _ <- init.traverse(h => IO.println(h)) _ <- IO.println("Please Say Hello To Someone or say CLEAR:") - who <- fs2.io.stdin[IO](4096).through(fs2.text.utf8.decode).takeThrough(_.contains("\n")).compile.string.map(_.trim()) - // who <- std.Console[IO].readLine // BROKEN + // Hangs on js + // who <- fs2.io.stdin[IO](4096).through(fs2.text.utf8.decode).takeThrough(_.contains("\n")).compile.string.map(_.trim()) + who <- std.Console[IO].readLine // BROKEN _ <- if (who === "CLEAR") clear(cross) else sayHello(cross, who) } yield () } From 4558fe6fd8ba059f5ec2d2a3f041deae09dd38d6 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 3 Aug 2022 07:10:04 -0700 Subject: [PATCH 4/5] We are pre-production --- build.sbt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.sbt b/build.sbt index 884d026..2bdca61 100644 --- a/build.sbt +++ b/build.sbt @@ -25,6 +25,8 @@ val circeV = "0.14.2" val doobieV = "1.0.0-RC2" val munitCatsEffectV = "1.0.7" +ThisBuild / mimaPreviousArtifacts := Set() + // Projects lazy val `sqlite-sjs` = tlCrossRootProject From 45ff49e71dbe4fd63af7d914cd81f1a04f02abe6 Mon Sep 17 00:00:00 2001 From: Christopher Davenport Date: Wed, 3 Aug 2022 07:39:10 -0700 Subject: [PATCH 5/5] Mima is grumpy --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 2bdca61..230db20 100644 --- a/build.sbt +++ b/build.sbt @@ -25,8 +25,6 @@ val circeV = "0.14.2" val doobieV = "1.0.0-RC2" val munitCatsEffectV = "1.0.7" -ThisBuild / mimaPreviousArtifacts := Set() - // Projects lazy val `sqlite-sjs` = tlCrossRootProject @@ -42,6 +40,7 @@ lazy val core = project.in(file("core")) "sqlite" -> "4.1.1", "sqlite3" -> "5.0.9" ), + mimaPreviousArtifacts := Set(), scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule)}, libraryDependencies ++= Seq( @@ -74,6 +73,7 @@ lazy val cross = crossProject(JVMPlatform, JSPlatform) .in(file("cross")) .settings( name := "sqlite-sjs-cross", + mimaPreviousArtifacts := Set(), libraryDependencies ++= Seq( "io.circe" %%% "circe-core" % circeV, )