From 5a6df3d823a9d3f1720527537f976220b14b9e36 Mon Sep 17 00:00:00 2001 From: Massimo Siani Date: Sat, 10 Dec 2022 20:18:26 +0100 Subject: [PATCH] add integration for cats typeclasses --- build.sbt | 43 +++++++++-- .../integrations/DerivedCatsInstances.scala | 48 ++++++++++++ .../DerivedCatsInstancesSuite.scala | 73 +++++++++++++++++++ site/src/mdoc/docs/cats.md | 37 ++++++++++ site/src/resources/microsite/data/menu.yml | 4 + 5 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 integration-cats/all/shared/src/main/scala/monix/newtypes/integrations/DerivedCatsInstances.scala create mode 100644 integration-cats/all/shared/src/test/scala/monix/newtypes/integrations/DerivedCatsInstancesSuite.scala create mode 100644 site/src/mdoc/docs/cats.md diff --git a/build.sbt b/build.sbt index 519078e..d8fc261 100644 --- a/build.sbt +++ b/build.sbt @@ -20,12 +20,13 @@ val Scala212 = "2.12.15" val Scala213 = "2.13.8" val Scala3 = "3.1.3" -val CatsVersion = "2.8.0" -val CirceVersionV0_14 = "0.14.3" -val PureConfigV0_17 = "0.17.1" -val ScalaTestVersion = "3.2.14" -val Shapeless2xVersion = "2.3.9" -val Shapeless3xVersion = "3.2.0" +val CatsVersion = "2.9.0" +val CirceVersionV0_14 = "0.14.3" +val PureConfigV0_17 = "0.17.1" +val ScalaTestVersion = "3.2.14" +val ScalaTestPlusVersion = "3.2.14.0" +val Shapeless2xVersion = "2.3.9" +val Shapeless3xVersion = "3.2.0" // --------------------------------------------------------------------------- @@ -216,8 +217,10 @@ lazy val root = project.in(file(".")) .enablePlugins(ScalaUnidocPlugin) .disablePlugins(MimaPlugin) .aggregate( - coreJVM, - coreJS, + coreJVM, + coreJS, + integrationCatsV2JVM, + integrationCatsV2JS, integrationCirceV014JVM, integrationCirceV014JS, integrationPureConfigV017JVM, @@ -253,6 +256,7 @@ lazy val site = project.in(file("site")) .settings(sharedSettings) .settings(doNotPublishArtifact) .dependsOn(coreJVM) + .dependsOn(integrationCatsV2JVM) .dependsOn(integrationCirceV014JVM) .dependsOn(integrationPureConfigV017JVM) .settings { @@ -377,6 +381,29 @@ def integrationSharedSettings(other: Setting[_]*) = } } +// --- +def catsSharedSettings(ver: String) = + integrationSharedSettings( + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-core" % ver, + "org.scalatestplus" %%% "scalacheck-1-17" % ScalaTestPlusVersion % Test, + ) + ) + +lazy val integrationCatsV2 = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Full) + .in(file("integration-cats/v2")) + .jsConfigure(_.disablePlugins(MimaPlugin)) + .configureCross(defaultCrossProjectConfiguration(JSPlatform, JVMPlatform)) + .dependsOn(core) + .settings(catsSharedSettings(CatsVersion)) + .settings(name := "newtypes-cats-v2") + .jvmSettings(mimaSettings("newtypes-cats-v2")) + +lazy val integrationCatsV2JVM = integrationCatsV2.jvm +lazy val integrationCatsV2JS = integrationCatsV2.js + +// --- def circeSharedSettings(ver: String) = integrationSharedSettings( libraryDependencies ++= Seq( diff --git a/integration-cats/all/shared/src/main/scala/monix/newtypes/integrations/DerivedCatsInstances.scala b/integration-cats/all/shared/src/main/scala/monix/newtypes/integrations/DerivedCatsInstances.scala new file mode 100644 index 0000000..e2deda6 --- /dev/null +++ b/integration-cats/all/shared/src/main/scala/monix/newtypes/integrations/DerivedCatsInstances.scala @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021-2022 the Newtypes contributors. + * See the project homepage at: https://newtypes.monix.io/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package monix.newtypes +package integrations + +import cats.{Eq, Hash, Order, Show} + +trait DerivedCatsInstances extends DerivedCatsEq with DerivedCatsHash with DerivedCatsOrder with DerivedCatsShow + +trait DerivedCatsEq { + implicit def catsEq[T, S](implicit extractor: HasExtractor.Aux[T, S], eqs: Eq[S]): Eq[T] = new Eq[T] { + override def eqv(x: T, y: T): Boolean = eqs.eqv(extractor.extract(x), extractor.extract(y)) + } +} + +trait DerivedCatsHash { self: DerivedCatsEq => + implicit def catsHash[T, S](implicit extractor: HasExtractor.Aux[T, S], hashS: Hash[S]): Hash[T] = new Hash[T] { + override def eqv(x: T, y: T): Boolean = self.catsEq.eqv(x, y) + override def hash(x: T): Int = hashS.hash(extractor.extract(x)) + } +} + +trait DerivedCatsOrder { + def catsOrder[T, S](implicit extractor: HasExtractor.Aux[T, S], orderS: Order[S]): Order[T] = new Order[T] { + override def compare(x: T, y: T): Int = orderS.compare(extractor.extract(x), extractor.extract(y)) + } +} + +trait DerivedCatsShow { + implicit def catsShow[T, S](implicit extractor: HasExtractor.Aux[T, S], showS: Show[S]): Show[T] = new Show[T] { + override def show(t: T): String = showS.show(extractor.extract(t)) + } +} diff --git a/integration-cats/all/shared/src/test/scala/monix/newtypes/integrations/DerivedCatsInstancesSuite.scala b/integration-cats/all/shared/src/test/scala/monix/newtypes/integrations/DerivedCatsInstancesSuite.scala new file mode 100644 index 0000000..11d4759 --- /dev/null +++ b/integration-cats/all/shared/src/test/scala/monix/newtypes/integrations/DerivedCatsInstancesSuite.scala @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021-2022 the Newtypes contributors. + * See the project homepage at: https://newtypes.monix.io/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package monix.newtypes +package integrations + +import cats.{Eq, Hash, Order, Show} +import cats.syntax.all._ +import monix.newtypes.NewtypeWrapped +import monix.newtypes.integrations.DerivedCatsInstancesSuite._ +import org.scalacheck.Prop._ +import org.scalatest.funsuite.AnyFunSuite +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks + +class DerivedCatsInstancesSuite extends AnyFunSuite with ScalaCheckPropertyChecks { + + test("an instance of Eq exists") { + implicitly[Eq[SomeNewtype]] + } + test("the Eq instances produce the same result") { + forAll { (x: Internal, y: Internal) => + x.eqv(y) == SomeNewtype(x).eqv(SomeNewtype(y)) + } + } + + test("an instance of Hash exists") { + implicitly[Hash[SomeNewtype]] + } + test("the Hash instances produce the same result") { + forAll { (x: Internal) => + x.hash == SomeNewtype(x).hash + } + } + + test("an instance of Show exists") { + implicitly[Show[SomeNewtype]] + } + test("the Show instances produce the same result") { + forAll { (x: Internal) => + x.show == SomeNewtype(x).show + } + } + + test("an instance of Order exists") { + assert(SomeNewtype.catsOrder[SomeNewtype, Internal].isInstanceOf[Order[_]]) + } + test("the Order instances produce the same result") { + forAll { (x: Internal, y: Internal) => + x.compare(y) == SomeNewtype.catsOrder[SomeNewtype, Internal].compare(SomeNewtype(x), SomeNewtype(y)) + } + } +} + +object DerivedCatsInstancesSuite { + final type Internal = String + + final type SomeNewtype = SomeNewtype.Type + object SomeNewtype extends NewtypeWrapped[Internal] with DerivedCatsInstances +} diff --git a/site/src/mdoc/docs/cats.md b/site/src/mdoc/docs/cats.md new file mode 100644 index 0000000..35d8087 --- /dev/null +++ b/site/src/mdoc/docs/cats.md @@ -0,0 +1,37 @@ +--- +layout: docs +title: "Integration with Cats typeclasses (Eq, Show, Hash, Order)" +--- + +# Integration with Cats typeclasses (Eq, Show, Hash, Order) + +Importing the [Cats](https://github.com/typelevel/cats) integration: + +```scala +// For Cats version 2.x.x +libraryDependencies += "io.monix" %% "newtypes-cats-v2" % "@VERSION@" +``` + +Usage: + +```scala mdoc:silent +import monix.newtypes._ +import monix.newtypes.integrations.DerivedCatsInstances + +type Email = Email.Type +object Email extends NewtypeValidated[String] with DerivedCatsInstances { + def apply(v: String): Either[BuildFailure[Type], Type] = + if (v.contains("@")) + Right(unsafeCoerce(v)) + else + Left(BuildFailure("missing @")) +} +``` + +You can now use the syntax from cats: + +```scala mdoc +import cats.syntax.all._ + +Email.unsafe("noreply@alexn.org").show +``` diff --git a/site/src/resources/microsite/data/menu.yml b/site/src/resources/microsite/data/menu.yml index 3f2879f..f3d38a0 100644 --- a/site/src/resources/microsite/data/menu.yml +++ b/site/src/resources/microsite/data/menu.yml @@ -12,6 +12,10 @@ options: url: docs/core.html menu_section: docs + - title: Cats Integration + url: docs/cats.html + menu_section: docs + - title: Circe JSON Integration url: docs/circe.html menu_section: docs