diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1007a19..cf44116e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.12, 2.13.4] + scala: [3.0.0-RC1] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -71,7 +71,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.12] + scala: [3.0.0-RC1] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -97,22 +97,12 @@ jobs: ~/Library/Caches/Coursier/v1 key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - - name: Download target directories (2.12.12) + - name: Download target directories (3.0.0-RC1) uses: actions/download-artifact@v2 with: - name: target-${{ matrix.os }}-2.12.12-${{ matrix.java }} + name: target-${{ matrix.os }}-3.0.0-RC1-${{ matrix.java }} - - name: Inflate target directories (2.12.12) - run: | - tar xf targets.tar - rm targets.tar - - - name: Download target directories (2.13.4) - uses: actions/download-artifact@v2 - with: - name: target-${{ matrix.os }}-2.13.4-${{ matrix.java }} - - - name: Inflate target directories (2.13.4) + - name: Inflate target directories (3.0.0-RC1) run: | tar xf targets.tar rm targets.tar diff --git a/DEV.md b/DEV.md index 3e11e871..04fe0cdd 100644 --- a/DEV.md +++ b/DEV.md @@ -9,20 +9,10 @@ short or generic names if possible. * Mention in the example section and in the release notes in README.md -### Release Process - -Commit but don't push all changes. - -``` -> sbt - -sbt> release +## Tests -//manual check of console output looks correct - -sbt> sonatypeRelease - -> git push +The tests dont have good descriptive labels as at Dec 2020. I've long believed that the best label for a well written test is the source code itself. The plan is to use [Sourcecode]() to generate labels based on the test code, however Sourcecode isn't published for Scala3+ScalaJS yet. Try again in a month or two.rr +### Release Process -``` \ No newline at end of file +Releases now occur via Github Actions CI. The server uses Ben Hutchison's Sonatype credentials to publish to maven. Push a tag as per https://github.com/olafurpg/sbt-ci-release#git \ No newline at end of file diff --git a/README.md b/README.md index 655b8085..aa53b8e8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ includes convenience extension methods for Scala standard library classes, inclu The library arose from this [Cats issue](https://github.com/typelevel/cats/issues/791) and is a [Typelevel member](http://typelevel.org/projects/). -Mouse is published for Scala 2.12 and 2.13. For Scala.jvm: +Mouse 1.x is published for Scala 3.x and is the main branch. Mouse 0.26 is published for Scala 2.12 and 2.13, and will be updated for bugfixes but not new features in future. + +For Scala.jvm: `"org.typelevel" %% "mouse" % version` @@ -101,6 +103,11 @@ mapped: Map[Int,Int] = Map(2 -> 2, 6 -> 4) #### Release Notes +Version `1.0.0-M1` (Jan 20) is a rewrite to Scala 3.x syntax. It is source-compatible with earlier versions but binary-incompatible. Mouse aims to add a minimal feature set over the standard library and Cats; its ideal is to "disappear over time". Accordingly, this version deprecates a number of methods, either because good solutions are available upstream now, or to align names better with emerging standard library conventions. + +The 1.x series migrates from Scalatest and Scalacheck to Munit, in pursuit of simplicitly and low maintenance. Note in this release, there's a change to how the library's extensions are made available. The syntax `import mouse.all._` remains, but it is no longer a package object inheriting from a stack of traits. Rather `all` is a package and we use Scala 3's `export` to export extension methods via this package. For users, there should be no change required beyond recompilation. + + Version `0.26` (Dec 20) adds `applyIf` for Boolean and builds against Cats 2.3.0. Version `0.25` (Apr 20) adds Scalajs 1.x support. diff --git a/build.sbt b/build.sbt index 566488c2..70898792 100644 --- a/build.sbt +++ b/build.sbt @@ -1,83 +1,44 @@ -import ReleaseTransformations._ import sbt._ -import sbtcrossproject.CrossPlugin.autoImport.crossProject -ThisBuild / githubWorkflowPublishTargetBranches := Seq() - -ThisBuild / crossScalaVersions := Seq("2.12.12", "2.13.4") - -lazy val commonSettings = Def.settings( - scalaVersion := "2.13.1", - crossScalaVersions := (ThisBuild / crossScalaVersions).value -) - -lazy val root = project.in(file(".")).aggregate(js, jvm). - settings( - name := "mouse", - commonSettings, - publish / skip := true, - sonatypeProfileName := "org.typelevel", - releaseCrossBuild := true - ) - -lazy val cross = crossProject(JSPlatform, JVMPlatform).in(file(".")). +inThisBuild(Seq( + name := "mouse", + version := "1.0.0-M1", + scalaVersion := "3.0.0-RC1", + crossScalaVersions := Seq("3.0.0-RC1"), + organization := "org.typelevel", + sonatypeProfileName := "org.typelevel", + testFrameworks += new TestFramework("munit.Framework"), + Test / publishArtifact := false, +)) + +lazy val root = crossProject(JSPlatform, JVMPlatform).withoutSuffixFor(JVMPlatform).in(file(".")). settings( - name := "mouse", - organization := "org.typelevel", - commonSettings, - sonatypeProfileName := "org.typelevel", libraryDependencies ++= Seq( "org.typelevel" %%% "cats-core" % "2.4.2", - "org.scalatest" %%% "scalatest" % "3.2.3" % Test, - "org.scalatestplus" %%% "scalacheck-1-15" % "3.2.3.0" % Test, - compilerPlugin("org.typelevel" %% "kind-projector" % "0.11.3" cross CrossVersion.full) + "org.scalameta" %% "munit" % "0.7.22" % Test, ), + scalacOptions ++= Seq("-feature", "-deprecation", "-language:higherKinds"), + licenses += ("MIT license", url("http://opensource.org/licenses/MIT")), homepage := Some(url("https://github.com/typelevel/mouse")), developers := List(Developer("benhutchison", "Ben Hutchison", "brhutchison@gmail.com", url = url("https://github.com/benhutchison"))), scmInfo := Some(ScmInfo(url("https://github.com/typelevel/mouse"), "scm:git:https://github.com/typelevel/mouse.git")), - scalacOptions ++= Seq("-feature", "-deprecation", "-language:implicitConversions", "-language:higherKinds"), - scalacOptions ++= { - scalaVersion.value match { - case v if v.startsWith("2.13") => Nil - case _ => Seq("-Ypartial-unification") - } - }, - Test / publishArtifact := false, - pomIncludeRepository := { _ => false }, - releasePublishArtifactsAction := PgpKeys.publishSigned.value, - releaseProcess := Seq[ReleaseStep]( - checkSnapshotDependencies, - inquireVersions, - runClean, - runTest, - setReleaseVersion, - commitReleaseVersion, - tagRelease, - publishArtifacts, - setNextVersion, - commitNextVersion, - ) - ) - -ThisBuild / githubWorkflowTargetTags ++= Seq("v*") -ThisBuild / githubWorkflowPublishTargetBranches := - Seq(RefPredicate.StartsWith(Ref.Tag("v"))) - -ThisBuild / githubWorkflowPublishPreamble += - WorkflowStep.Use(UseRef.Public("olafurpg", "setup-gpg", "v3")) - -ThisBuild / githubWorkflowPublish := Seq( - WorkflowStep.Sbt( - List("ci-release"), - env = Map( - "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", - "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", - "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", - "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}" - ) ) -) -lazy val jvm = cross.jvm -lazy val js = cross.js +inThisBuild(Seq( + githubWorkflowTargetTags ++= Seq("v*"), + githubWorkflowPublishTargetBranches := Seq(RefPredicate.StartsWith(Ref.Tag("v"))), + + githubWorkflowPublishPreamble += WorkflowStep.Use(UseRef.Public("olafurpg", "setup-gpg", "v3")), + + githubWorkflowPublish := Seq( + WorkflowStep.Sbt( + List("ci-release"), + env = Map( + "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", + "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", + "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", + "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}" + ) + )) +)) \ No newline at end of file diff --git a/js/src/main/scala/mouse/js.scala b/js/src/main/scala/mouse/js.scala deleted file mode 100644 index 099bec4f..00000000 --- a/js/src/main/scala/mouse/js.scala +++ /dev/null @@ -1,3 +0,0 @@ -package mouse - -trait AllJsSyntax diff --git a/js/src/main/scala/mouse/package.scala b/js/src/main/scala/mouse/package.scala deleted file mode 100644 index 66aed84d..00000000 --- a/js/src/main/scala/mouse/package.scala +++ /dev/null @@ -1,13 +0,0 @@ -package object mouse extends MouseFunctions { - object all extends AllSharedSyntax with AllJsSyntax - object any extends AnySyntax - object anyf extends AnyFSyntax - object option extends OptionSyntax - object boolean extends BooleanSyntax - object string extends StringSyntax - object `try` extends TrySyntax - object int extends IntSyntax - object double extends DoubleSyntax - object long extends LongSyntax - object map extends MapSyntax -} diff --git a/jvm/src/main/scala/mouse/all/allJvm.scala b/jvm/src/main/scala/mouse/all/allJvm.scala new file mode 100644 index 00000000..a7df118d --- /dev/null +++ b/jvm/src/main/scala/mouse/all/allJvm.scala @@ -0,0 +1,3 @@ +package mouse.all + +export mouse.stringJvm._ \ No newline at end of file diff --git a/jvm/src/main/scala/mouse/jvm.scala b/jvm/src/main/scala/mouse/jvm.scala deleted file mode 100644 index 138ee547..00000000 --- a/jvm/src/main/scala/mouse/jvm.scala +++ /dev/null @@ -1,4 +0,0 @@ -package mouse - -trait AllJvmSyntax - extends StringJvmSyntax diff --git a/jvm/src/main/scala/mouse/package.scala b/jvm/src/main/scala/mouse/package.scala deleted file mode 100644 index c717451d..00000000 --- a/jvm/src/main/scala/mouse/package.scala +++ /dev/null @@ -1,13 +0,0 @@ -package object mouse extends MouseFunctions { - object all extends AllSharedSyntax with AllJvmSyntax - object any extends AnySyntax - object anyf extends AnyFSyntax - object option extends OptionSyntax - object boolean extends BooleanSyntax - object string extends StringSyntax with StringJvmSyntax - object `try` extends TrySyntax - object int extends IntSyntax - object double extends DoubleSyntax - object long extends LongSyntax - object map extends MapSyntax -} diff --git a/jvm/src/main/scala/mouse/string.scala b/jvm/src/main/scala/mouse/string.scala new file mode 100644 index 00000000..4807a3bd --- /dev/null +++ b/jvm/src/main/scala/mouse/string.scala @@ -0,0 +1,22 @@ +package mouse + +import java.net.{MalformedURLException, URI, URISyntaxException, URL} + +import cats.data.Validated +import cats.syntax.either._ + +object stringJvm: + extension (s: String) + + @inline def parseURL: Either[MalformedURLException, URL] = Either.catchOnly[MalformedURLException](new URL(s)) + + @inline def parseURLValidated: Validated[MalformedURLException, URL] = parseURL.toValidated + + @inline def parseURLOption: Option[URL] = parseURL.toOption + + @inline def parseURI: Either[URISyntaxException, URI] = Either.catchOnly[URISyntaxException](new URI(s)) + + @inline def parseURIValidated: Validated[URISyntaxException, URI] = parseURI.toValidated + + @inline def parseURIOption: Option[URI] = parseURI.toOption + diff --git a/jvm/src/main/scala/mouse/stringJvm.scala b/jvm/src/main/scala/mouse/stringJvm.scala deleted file mode 100644 index 3df0e172..00000000 --- a/jvm/src/main/scala/mouse/stringJvm.scala +++ /dev/null @@ -1,26 +0,0 @@ -package mouse - -import java.net.{MalformedURLException, URI, URISyntaxException, URL} - -import cats.data.Validated -import cats.syntax.either._ - -trait StringJvmSyntax { - implicit def stringJvmSyntaxMouse(s: String): JvmStringOps = new JvmStringOps(s) -} - -final class JvmStringOps(private val s: String) extends AnyVal { - - @inline def parseURL: MalformedURLException Either URL = Either.catchOnly[MalformedURLException](new URL(s)) - - @inline def parseURLValidated: Validated[MalformedURLException, URL] = parseURL.toValidated - - @inline def parseURLOption: Option[URL] = parseURL.toOption - - @inline def parseURI: URISyntaxException Either URI = Either.catchOnly[URISyntaxException](new URI(s)) - - @inline def parseURIValidated: Validated[URISyntaxException, URI] = parseURI.toValidated - - @inline def parseURIOption: Option[URI] = parseURI.toOption - -} diff --git a/jvm/src/test/scala/mouse/StringJvmTests.scala b/jvm/src/test/scala/mouse/StringJvmTests.scala index e4d7d992..730b18e1 100644 --- a/jvm/src/test/scala/mouse/StringJvmTests.scala +++ b/jvm/src/test/scala/mouse/StringJvmTests.scala @@ -4,38 +4,14 @@ import java.net.{MalformedURLException, URI, URISyntaxException, URL} import cats.Eq import cats.syntax.all._ -import mouse.string._ +import mouse.stringJvm._ -class StringJvmTests extends MouseSuite { +class StringJvmTests extends MouseSuite: + + given Eq[URL] = Eq.fromUniversalEquals + given Eq[URI] = Eq.fromUniversalEquals - test("parseFloat") { - "123.1".parseFloat should ===(123.1f.asRight[NumberFormatException]) - } - - test("parseURL") { - implicit val urlEq: Eq[URL] = Eq.fromUniversalEquals - implicit val malformedURLExceptionEq: Eq[MalformedURLException] = - new Eq[MalformedURLException] { - override def eqv(x: MalformedURLException, y: MalformedURLException): Boolean = - x.getMessage == y.getMessage - } - - "http://example.com".parseURL should ===(new URL("http://example.com").asRight[MalformedURLException]) - - "blah".parseURL should ===(new MalformedURLException("no protocol: blah").asLeft) - } - - test("parseURI") { - implicit val urlEq: Eq[URI] = Eq.fromUniversalEquals - implicit val malformedURIExceptionEq: Eq[URISyntaxException] = - new Eq[URISyntaxException] { - override def eqv(x: URISyntaxException, y: URISyntaxException): Boolean = - x.getMessage == y.getMessage - } - - "http://example.com".parseURI should ===(new URI("http://example.com").asRight[URISyntaxException]) - - "invalid uri".parseURI should ===(new URISyntaxException("invalid uri", "Illegal character in path at index 7").asLeft) - } - -} + testEquals("http://example.com".parseURL, URL("http://example.com").asRight[MalformedURLException]) + testEquals("blah".parseURL, new MalformedURLException("no protocol: blah").asLeft) + testEquals("http://example.com".parseURI, new URI("http://example.com").asRight[URISyntaxException]) + testEquals("invalid uri".parseURI, new URISyntaxException("invalid uri", "Illegal character in path at index 7").asLeft) diff --git a/project/plugins.sbt b/project/plugins.sbt index cdcc89b4..056c7193 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,7 +1,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.5.0") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.5") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") -addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.11") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.10.1") addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.5") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.3") diff --git a/shared/src/main/scala/mouse/MouseFunctions.scala b/shared/src/main/scala/mouse/MouseFunctions.scala index fb2986e2..c72e0f36 100644 --- a/shared/src/main/scala/mouse/MouseFunctions.scala +++ b/shared/src/main/scala/mouse/MouseFunctions.scala @@ -1,13 +1,6 @@ package mouse -trait MouseFunctions { +@deprecated("Use `: Unit` type ascription instead, see also https://github.com/scala/scala/pull/7563", "1.0.0") +def ignore(a: Any): Unit = () - /** - * Evaluate but ignore the provided argument. This function makes value discarding an explicit operation, - * helpful when the `-Ywarn-discard-values` compiler flag is enable to explicitly satisfy warnings. - * - * @param a - the value to be evaluated and ignored. - */ - def ignore(a: Any): Unit = () -} diff --git a/shared/src/main/scala/mouse/all.scala b/shared/src/main/scala/mouse/all.scala deleted file mode 100644 index 2291c044..00000000 --- a/shared/src/main/scala/mouse/all.scala +++ /dev/null @@ -1,14 +0,0 @@ -package mouse - -trait AllSharedSyntax - extends AnySyntax - with AnyFSyntax - with OptionSyntax - with BooleanSyntax - with StringSyntax - with TrySyntax - with IntSyntax - with LongSyntax - with DoubleSyntax - with PartialFunctionLift - with MapSyntax diff --git a/shared/src/main/scala/mouse/all/all.scala b/shared/src/main/scala/mouse/all/all.scala new file mode 100644 index 00000000..643822e7 --- /dev/null +++ b/shared/src/main/scala/mouse/all/all.scala @@ -0,0 +1,15 @@ +package mouse.all + +//Scala 3 migration note: Previously an object, `all` is now a package with exports of mouse's component objects + +export mouse.any._ +export mouse.anyf._ +export mouse.boolean._ +export mouse.double._ +export mouse.`try`._ +export mouse.int._ +export mouse.long._ +export mouse.map._ +export mouse.option._ +export mouse.partialFunction._ +export mouse.string._ diff --git a/shared/src/main/scala/mouse/any.scala b/shared/src/main/scala/mouse/any.scala index 90db30ad..87de7051 100644 --- a/shared/src/main/scala/mouse/any.scala +++ b/shared/src/main/scala/mouse/any.scala @@ -1,18 +1,16 @@ package mouse -trait AnySyntax { - implicit final def anySyntaxMouse[A](oa: A): AnyOps[A] = new AnyOps(oa) -} -final class AnyOps[A](private val oa: A) extends AnyVal { - @inline def |>[B] (f: A => B) = f(oa) - @inline def thrush[B] (f: A => B) = f(oa) - @inline def <|(f: A => Unit): A = { - f(oa) - oa - } - @inline def unsafeTap(f: A => Unit): A = { - f(oa) - oa - } -} +object any: + extension [A](oa: A) + @inline def <|(f: A => Unit): A = + f(oa) + oa + + @inline def unsafeTap(f: A => Unit): A = + f(oa) + oa + + extension [A, B](oa: A) + @inline def |>(f: A => B) = f(oa) + @inline def thrush(f: A => B) = f(oa) diff --git a/shared/src/main/scala/mouse/anyf.scala b/shared/src/main/scala/mouse/anyf.scala index c4bbc474..fc5e40da 100644 --- a/shared/src/main/scala/mouse/anyf.scala +++ b/shared/src/main/scala/mouse/anyf.scala @@ -1,11 +1,8 @@ package mouse import cats.~> -trait AnyFSyntax { - implicit final def anyfSyntaxMouse[F[_], A](fa: F[A]): AnyFOps[F, A] = new AnyFOps(fa) -} +object anyf: + extension [F[_], G[_], A](fa: F[A]) + @inline def ||>(f: F ~> G): G[A] = f(fa) + @inline def thrushK(f: F ~> G): G[A] = f(fa) -final class AnyFOps[F[_], A](private val fa: F[A]) extends AnyVal { - @inline def ||>[G[_]](f: F ~> G): G[A] = f(fa) - @inline def thrushK[G[_]](f: F ~> G): G[A] = f(fa) -} diff --git a/shared/src/main/scala/mouse/boolean.scala b/shared/src/main/scala/mouse/boolean.scala index 8a19867d..bc46c616 100644 --- a/shared/src/main/scala/mouse/boolean.scala +++ b/shared/src/main/scala/mouse/boolean.scala @@ -2,38 +2,36 @@ package mouse import cats.{ Applicative, Monoid } -trait BooleanSyntax { - implicit final def booleanSyntaxMouse(b: Boolean): BooleanOps = new BooleanOps(b) -} -class ApplyIfPartiallyApplied[A](b: Boolean, a: A) { - @inline def apply[B >: A](f: B => B): B = if (b) f(a) else a -} +object boolean: + extension [A](b: Boolean) + @inline def option(a: => A): Option[A] = fold(Some(a), None) -final class BooleanOps(private val b: Boolean) extends AnyVal { + @inline def fold(t: => A, f: => A): A = if (b) t else f - @inline def option[A](a: => A): Option[A] = fold(Some(a), None) + @inline def valueOrZero(a: => A)(using M: Monoid[A]): A = if (b) a else M.empty - @deprecated("Use `either` instead", "0.6") - @inline def xor[L, R](l: =>L, r: =>R): Either[L, R] = either(l, r) + @inline def zeroOrValue(a: => A)(using M: Monoid[A]): A = if (b) M.empty else a - @inline def either[L, R](l: =>L, r: =>R): Either[L, R] = fold(Right(r), Left(l)) + @inline def ??(a: => A)(using M: Monoid[A]): A = valueOrZero(a) - @inline def fold[A](t: => A, f: => A): A = if (b) t else f + @inline def !?(a: => A)(using M: Monoid[A]): A = zeroOrValue(a) - @inline def valueOrZero[A](a: => A)(implicit M: Monoid[A]): A = if (b) a else M.empty + @inline def applyIf(a: A): ApplyIfPartiallyApplied[A] = new ApplyIfPartiallyApplied[A](b, a) - @inline def zeroOrValue[A](a: => A)(implicit M: Monoid[A]): A = if (b) M.empty else a + extension [L, R](b: Boolean) + @inline def either(l: =>L, r: =>R): Either[L, R] = b.fold(Right(r), Left(l)) - @inline def ??[A](a: => A)(implicit M: Monoid[A]): A = valueOrZero(a) - @inline def !?[A](a: => A)(implicit M: Monoid[A]): A = zeroOrValue(a) + extension [F[_], A](b: Boolean) + @inline def valueOrPure(fa: =>F[A])(a: =>A)(using F: Applicative[F]) = if (b) fa else F.pure(a) - @inline def valueOrPure[F[_], A](fa: =>F[A])(a: =>A)(implicit F: Applicative[F]) = if (b) fa else F.pure(a) + @inline def whenA(fa: F[A])(using F: Applicative[F]): F[Unit] = + F.whenA(b)(fa) - @inline def applyIf[A](a: A): ApplyIfPartiallyApplied[A] = new ApplyIfPartiallyApplied[A](b, a) + @inline def unlessA(fa: F[A])(using F: Applicative[F]): F[Unit] = + F.unlessA(b)(fa) - @inline def whenA[F[_], A](fa: F[A])(implicit F: Applicative[F]): F[Unit] = F.whenA(b)(fa) - @inline def unlessA[F[_], A](fa: F[A])(implicit F: Applicative[F]): F[Unit] = F.unlessA(b)(fa) -} +class ApplyIfPartiallyApplied[A](b: Boolean, a: A): + @inline def apply[B >: A](f: B => B): B = if (b) f(a) else a diff --git a/shared/src/main/scala/mouse/double.scala b/shared/src/main/scala/mouse/double.scala index 0d157599..ede174c9 100644 --- a/shared/src/main/scala/mouse/double.scala +++ b/shared/src/main/scala/mouse/double.scala @@ -1,14 +1,8 @@ package mouse -trait DoubleSyntax { - implicit final def doubleSyntaxMouse(n: Double): DoubleOps = new DoubleOps(n) -} +object double: + extension (n: Double) + @inline def toByteArray: Array[Byte] = java.nio.ByteBuffer.allocate(8).putDouble(n).array() -final class DoubleOps(private val n: Double) extends AnyVal { - - @inline def toByteArray: Array[Byte] = java.nio.ByteBuffer.allocate(8).putDouble(n).array() - - @inline def squared: Double = n * n - -} + @inline def squared: Double = n * n diff --git a/shared/src/main/scala/mouse/int.scala b/shared/src/main/scala/mouse/int.scala index 1a0937c4..b7d2f841 100644 --- a/shared/src/main/scala/mouse/int.scala +++ b/shared/src/main/scala/mouse/int.scala @@ -1,17 +1,11 @@ package mouse -trait IntSyntax { - implicit final def intSyntaxMouse(n: Int): IntOps = new IntOps(n) -} +object int: + extension (n: Int) + @inline def toByteArray: Array[Byte] = java.nio.ByteBuffer.allocate(4).putInt(n).array() -final class IntOps(private val n: Int) extends AnyVal { + /** Base64 encoding (without final terminator). Shortcut for `java.util.Base64.getEncoder.withoutPadding.encodeToString`*/ + @inline def toBase64: String = java.util.Base64.getEncoder.withoutPadding.encodeToString(toByteArray) - @inline def toByteArray: Array[Byte] = java.nio.ByteBuffer.allocate(4).putInt(n).array() - - /** Base64 encoding (without final terminator). Shortcut for `java.util.Base64.getEncoder.withoutPadding.encodeToString`*/ - @inline def toBase64: String = java.util.Base64.getEncoder.withoutPadding.encodeToString(toByteArray) - - @inline def squared: Int = n * n - -} + @inline def squared: Int = n * n \ No newline at end of file diff --git a/shared/src/main/scala/mouse/long.scala b/shared/src/main/scala/mouse/long.scala index 0c0ec098..f6dfdbae 100644 --- a/shared/src/main/scala/mouse/long.scala +++ b/shared/src/main/scala/mouse/long.scala @@ -1,17 +1,10 @@ package mouse +object long: + extension (n: Long) + @inline def toByteArray: Array[Byte] = java.nio.ByteBuffer.allocate(8).putLong(n).array() -trait LongSyntax { - implicit final def longSyntaxMouse(n: Long): LongOps = new LongOps(n) -} + /** Base64 encoding (without final terminator). Shortcut for `java.util.Base64.getEncoder.withoutPadding.encodeToString`*/ + @inline def toBase64: String = java.util.Base64.getEncoder.withoutPadding.encodeToString(toByteArray) -final class LongOps(private val n: Long) extends AnyVal { - - @inline def toByteArray: Array[Byte] = java.nio.ByteBuffer.allocate(8).putLong(n).array() - - /** Base64 encoding (without final terminator). Shortcut for `java.util.Base64.getEncoder.withoutPadding.encodeToString`*/ - @inline def toBase64: String = java.util.Base64.getEncoder.withoutPadding.encodeToString(toByteArray) - - @inline def squared: Long = n * n - -} + @inline def squared: Long = n * n diff --git a/shared/src/main/scala/mouse/map.scala b/shared/src/main/scala/mouse/map.scala index 82ce53db..f098ab1b 100644 --- a/shared/src/main/scala/mouse/map.scala +++ b/shared/src/main/scala/mouse/map.scala @@ -2,19 +2,16 @@ package mouse import cats.{Applicative, Semigroup} -trait MapSyntax { - implicit final def mapSyntaxMouse[A, B](map: Map[A, B]): MapOps[A, B] = new MapOps(map) -} +object map: + extension [A, B](map: Map[A, B]) + def updateAtKey(key: A, f: B => B): Map[A, B] = map.get(key).fold(map)(value => map.updated(key, f(value))) -final class MapOps[A, B](private val map: Map[A, B]) extends AnyVal { + def updateAtKeyCombine(key: A, add: B)(using sg: Semigroup[B]): Map[A, B] = + map.get(key).fold(map + (key -> add))(value => map.updated(key, sg.combine(value, add))) - def mapKeys[C](f: A => C): Map[C, B] = map.map { case (a, b) => (f(a), b) } + extension [A, B, C](map: Map[A, B]) + def mapKeys(f: A => C): Map[C, B] = map.map((a, b) => (f(a), b)) - def updateAtKey(key: A, f: B => B): Map[A, B] = map.get(key).fold(map)(value => map.updated(key, f(value))) - - def updateAtKeyF[F[_]](key: A, f: B => F[B])(implicit F: Applicative[F]): F[Map[A, B]] = - map.get(key).fold(F.pure(map))(value => F.map(f(value))(result => map.updated(key, result))) - - def updateAtKeyCombine(key: A, add: B)(implicit sg: Semigroup[B]): Map[A, B] = - map.get(key).fold(map + (key -> add))(value => map.updated(key, sg.combine(value, add))) -} + extension [A, B, F[_]](map: Map[A, B]) + def updateAtKeyF(key: A, f: B => F[B])(using F: Applicative[F]): F[Map[A, B]] = + map.get(key).fold(F.pure(map))(value => F.map(f(value))(result => map.updated(key, result))) \ No newline at end of file diff --git a/shared/src/main/scala/mouse/option.scala b/shared/src/main/scala/mouse/option.scala index ce52d611..037d80ed 100644 --- a/shared/src/main/scala/mouse/option.scala +++ b/shared/src/main/scala/mouse/option.scala @@ -2,27 +2,17 @@ package mouse import scala.util.{Failure, Success, Try} -trait OptionSyntax { - implicit final def optionSyntaxMouse[A](oa: Option[A]): OptionOps[A] = new OptionOps(oa) -} +object option: + extension [A](oa: Option[A]) + @inline def toTry(ex: =>Throwable): Try[A] = oa.cata(Success(_), Failure(ex)) -final class OptionOps[A](private val oa: Option[A]) extends AnyVal { + @inline def toTryMsg(msg: =>String): Try[A] = oa.toTry(new RuntimeException(msg)) - @inline def cata[B](some: A => B, none: => B): B = oa.fold[B](none)(some) + extension [A, B](oa: Option[A]) + @inline def cata(some: A => B, none: => B): B = oa.fold[B](none)(some) - @inline def toTry(ex: =>Throwable): Try[A] = cata(Success(_), Failure(ex)) - - @inline def toTryMsg(msg: =>String): Try[A] = toTry(new RuntimeException(msg)) - - /** - * Same as oa.toRight except that it fixes the type to Either[B, A] - * On Scala prior to 2.12, toRight returns `Serializable with Product with Either[B, A]` - */ - @inline def right[B](b: => B) : Either[B, A] = oa.toRight(b) - - /** - * Same as oa.toLeft except that it fixes the type to Either[A, B] - * On Scala prior to 2.12, toLeft returns `Serializable with Product with Either[A, B]` - */ - @inline def left[B](b: => B) : Either[A, B] = oa.toLeft(b) -} + @deprecated("use `toRight` instead", "1.0.0") + @inline def right(b: => B) : Either[B, A] = oa.toRight(b) + + @deprecated("use `toLeft` instead", "1.0.0") + @inline def left(b: => B) : Either[A, B] = oa.toLeft(b) diff --git a/shared/src/main/scala/mouse/partialFunction.scala b/shared/src/main/scala/mouse/partialFunction.scala index 99e8348d..12dbc3f2 100644 --- a/shared/src/main/scala/mouse/partialFunction.scala +++ b/shared/src/main/scala/mouse/partialFunction.scala @@ -1,17 +1,10 @@ package mouse -trait PartialFunctionLift { - - def liftEither[A]: PartialFunctionLift.LiftEitherPartiallyApplied[A] = new PartialFunctionLift.LiftEitherPartiallyApplied() - -} -object PartialFunctionLift { +object partialFunction: + def liftEither[A]: LiftEitherPartiallyApplied[A] = new LiftEitherPartiallyApplied() //https://typelevel.org/cats/guidelines.html#partially-applied-type-params - private[mouse] final class LiftEitherPartiallyApplied[A](private val dummy: Boolean = true ) extends AnyVal { + private[mouse] final class LiftEitherPartiallyApplied[A](private val dummy: Boolean = true) extends AnyVal: def apply[B, C](pf: PartialFunction[A, B], orElse: A => C): A => Either[C, B] = (a: A) => pf.lift(a).toRight(orElse(a)) - } - -} diff --git a/shared/src/main/scala/mouse/string.scala b/shared/src/main/scala/mouse/string.scala index 7ccec007..d54050f2 100644 --- a/shared/src/main/scala/mouse/string.scala +++ b/shared/src/main/scala/mouse/string.scala @@ -1,68 +1,129 @@ package mouse import cats.data.Validated -import cats.syntax.either._ +import cats.syntax.all._ -trait StringSyntax { - implicit def stringSyntaxMouse(s: String): StringOps = new StringOps(s) -} +object string: + extension (s: String) -final class StringOps(private val s: String) extends AnyVal { + @inline def toBooleanEither: Either[String, Boolean] = s.toBooleanOption.toRight(s) + @inline def toBooleanValidated: Validated[String, Boolean] = s.toBooleanOption.toValid(s) - @inline def parseBoolean: IllegalArgumentException Either Boolean = Either.catchOnly[IllegalArgumentException](s.toBoolean) + @deprecated("Use `toBooleanOption` from the std lib instead", "1.0.0") + @inline def parseBooleanOption: Option[Boolean] = s.toBooleanOption - @inline def parseBooleanValidated: Validated[IllegalArgumentException, Boolean] = parseBoolean.toValidated + @deprecated("Use `toBooleanEither` instead", "1.0.0") + @inline def parseBoolean: Either[IllegalArgumentException, Boolean] = + Either.catchOnly[IllegalArgumentException](s.toBoolean) - @inline def parseBooleanOption: Option[Boolean] = parseBoolean.toOption + @deprecated("Use `toBooleanValidated` instead", "1.0.0") + @inline def parseBooleanValidated: Validated[IllegalArgumentException, Boolean] = parseBoolean.toValidated - @inline def parseByte: NumberFormatException Either Byte = parse[Byte](_.toByte) - @inline def parseByteValidated: Validated[NumberFormatException, Byte] = parseByte.toValidated + @inline def toByteEither: Either[String, Byte] = s.toByteOption.toRight(s) + @inline def toByteValidated: Validated[String, Byte] = s.toByteOption.toValid(s) - @inline def parseByteOption: Option[Byte] = parseByte.toOption + @deprecated("Use `toByteOption` from the std lib instead", "1.0.0") + @inline def parseByteOption: Option[Byte] = parseByte.toOption - @inline def parseDouble: NumberFormatException Either Double = parse[Double](_.toDouble) + @deprecated("Use `toByteEither` instead", "1.0.0") + @inline def parseByte: Either[NumberFormatException, Byte] = parse[Byte](_.toByte, s) - @inline def parseDoubleValidated: Validated[NumberFormatException, Double] = parseDouble.toValidated + @deprecated("Use `toByteValidated` instead", "1.0.0") + @inline def parseByteValidated: Validated[NumberFormatException, Byte] = parseByte.toValidated - @inline def parseDoubleOption: Option[Double] = parseDouble.toOption - @inline def parseFloat: NumberFormatException Either Float = parse[Float](_.toFloat) + @inline def toDoubleEither: Either[String, Double] = s.toDoubleOption.toRight(s) + @inline def toDoubleValidated: Validated[String, Double] = s.toDoubleOption.toValid(s) - @inline def parseFloatValidated: Validated[NumberFormatException, Float] = parseFloat.toValidated + @deprecated("Use `toDoubleOption` from the std lib instead", "1.0.0") + @inline def parseDoubleOption: Option[Double] = parseDouble.toOption - @inline def parseFloatOption: Option[Float] = parseFloat.toOption + @deprecated("Use `toDoubleEither` instead", "1.0.0") + @inline def parseDouble: Either[NumberFormatException, Double] = parse[Double](_.toDouble, s) - @inline def parseInt: NumberFormatException Either Int = parse[Int](_.toInt) + @deprecated("Use `toDoubleValidated` instead", "1.0.0") + @inline def parseDoubleValidated: Validated[NumberFormatException, Double] = parseDouble.toValidated - @inline def parseIntValidated: Validated[NumberFormatException, Int] = parseInt.toValidated - @inline def parseIntOption: Option[Int] = parseInt.toOption - @inline def parseLong: NumberFormatException Either Long = parse[Long](_.toLong) + @inline def toFloatEither: Either[String, Float] = s.toFloatOption.toRight(s) + @inline def toFloatValidated: Validated[String, Float] = s.toFloatOption.toValid(s) + + @deprecated("Use `toFloatOption` from the std lib instead", "1.0.0") + @inline def parseFloatOption: Option[Float] = parseFloat.toOption + + @deprecated("Use `toFloatEither` instead", "1.0.0") + @inline def parseFloat: Either[NumberFormatException, Float] = parse[Float](_.toFloat, s) + + @deprecated("Use `toFloatValidated` instead", "1.0.0") + @inline def parseFloatValidated: Validated[NumberFormatException, Float] = parseFloat.toValidated - @inline def parseLongValidated: Validated[NumberFormatException, Long] = parseLong.toValidated + + @inline def toIntEither: Either[String, Int] = s.toIntOption.toRight(s) + @inline def toIntValidated: Validated[String, Int] = s.toIntOption.toValid(s) + + @deprecated("Use `toIntOption` from the std lib instead", "1.0.0") + @inline def parseIntOption: Option[Int] = parseInt.toOption + + @deprecated("Use `toIntEither` instead", "1.0.0") + @inline def parseInt: Either[NumberFormatException, Int] = parse[Int](_.toInt, s) + + @deprecated("Use `toIntValidated` instead", "1.0.0") + @inline def parseIntValidated: Validated[NumberFormatException, Int] = parseInt.toValidated - @inline def parseLongOption: Option[Long] = parseLong.toOption - @inline def parseShort: NumberFormatException Either Short = parse[Short](_.toShort) + @inline def toLongEither: Either[String, Long] = s.toLongOption.toRight(s) + @inline def toLongValidated: Validated[String, Long] = s.toLongOption.toValid(s) + + @deprecated("Use `toLongOption` from the std lib instead", "1.0.0") + @inline def parseLongOption: Option[Long] = parseLong.toOption + + @deprecated("Use `toLongEither` instead", "1.0.0") + @inline def parseLong: Either[NumberFormatException, Long] = parse[Long](_.toLong, s) + + @deprecated("Use `toLongValidated` instead", "1.0.0") + @inline def parseLongValidated: Validated[NumberFormatException, Long] = parseLong.toValidated - @inline def parseShortValidated: Validated[NumberFormatException, Short] = parseShort.toValidated - @inline def parseShortOption: Option[Short] = parseShort.toOption + @inline def toShortEither: Either[String, Short] = s.toShortOption.toRight(s) + @inline def toShortValidated: Validated[String, Short] = s.toShortOption.toValid(s) + + @deprecated("Use `toShortOption` from the std lib instead", "1.0.0") + @inline def parseShortOption: Option[Short] = parseShort.toOption + + @deprecated("Use `toShortEither` instead", "1.0.0") + @inline def parseShort: Either[NumberFormatException, Short] = parse[Short](_.toShort, s) + + @deprecated("Use `toShortValidated` instead", "1.0.0") + @inline def parseShortValidated: Validated[NumberFormatException, Short] = parseShort.toValidated - @inline def parseBigInt: NumberFormatException Either BigInt = parse(BigInt.apply) - @inline def parseBigIntValidated: Validated[NumberFormatException, BigInt] = parseBigInt.toValidated + @inline def toBigIntOption: Option[BigInt] = parse[BigInt](BigInt.apply, s).toOption + @inline def toBigIntEither: Either[String, BigInt] = s.toBigIntOption.toRight(s) + @inline def toBigIntValidated: Validated[String, BigInt] = s.toBigIntOption.toValid(s) - @inline def parseBigIntOption: Option[BigInt] = parseBigInt.toOption + @deprecated("Use `toBigIntOption` from the std lib instead", "1.0.0") + @inline def parseBigIntOption: Option[BigInt] = parseBigInt.toOption - @inline def parseBigDecimal: NumberFormatException Either BigDecimal = parse(BigDecimal.apply) + @deprecated("Use `toBigIntEither` instead", "1.0.0") + @inline def parseBigInt: Either[NumberFormatException, BigInt] = parse[BigInt](BigInt.apply, s) - @inline def parseBigDecimalValidated: Validated[NumberFormatException, BigDecimal] = parseBigDecimal.toValidated - - @inline def parseBigDecimalOption: Option[BigDecimal] = parseBigDecimal.toOption - - private def parse[A](f: String => A): NumberFormatException Either A = Either.catchOnly[NumberFormatException](f(s)) - -} + @deprecated("Use `toBigIntValidated` instead", "1.0.0") + @inline def parseBigIntValidated: Validated[NumberFormatException, BigInt] = parseBigInt.toValidated + + + @inline def toBigDecimalOption: Option[BigDecimal] = parse[BigDecimal](BigDecimal.apply, s).toOption + @inline def toBigDecimalEither: Either[String, BigDecimal] = s.toBigDecimalOption.toRight(s) + @inline def toBigDecimalValidated: Validated[String, BigDecimal] = s.toBigDecimalOption.toValid(s) + + @deprecated("Use `toBigDecimalOption` aligning with the std lib instead", "1.0.0") + @inline def parseBigDecimalOption: Option[BigDecimal] = s.toBigDecimalOption + + @deprecated("Use `toBigDecimalEither` instead", "1.0.0") + @inline def parseBigDecimal: Either[NumberFormatException, BigDecimal] = parse[BigDecimal](BigDecimal.apply, s) + + @deprecated("Use `toBigDecimalValidated` instead", "1.0.0") + @inline def parseBigDecimalValidated: Validated[NumberFormatException, BigDecimal] = parseBigDecimal.toValidated + + private def parse[A](f: String => A, s: String): NumberFormatException Either A = Either.catchOnly[NumberFormatException](f(s)) diff --git a/shared/src/main/scala/mouse/try.scala b/shared/src/main/scala/mouse/try.scala index 68e0177e..40f73e7e 100644 --- a/shared/src/main/scala/mouse/try.scala +++ b/shared/src/main/scala/mouse/try.scala @@ -5,22 +5,14 @@ import cats.data.EitherT import scala.util.{Failure, Success, Try} -trait TrySyntax { - @inline implicit final def trySyntaxMouse[A](ta: Try[A]): TryOps[A] = new TryOps(ta) -} +object `try`: + extension [A, B] (ta: Try[A]) + @deprecated("Use `fold` instead", "1.0.0") + @inline def cata(success: A => B, failure: Throwable => B): B = ta.fold(failure, success) -final class TryOps[A](private val ta: Try[A]) extends AnyVal { + //TODO migrate to extension method if partial application supported + import scala.language.implicitConversions + @inline implicit final def tryOps[A](ta: Try[A]): TryOps[A] = TryOps(ta) - @inline def cata[B](success: A => B, failure: Throwable => B): B = ta match { - case Success(value) => success(value) - case Failure(error) => failure(error) - } - - @inline def toEither: Either[Throwable, A] = cata[Either[Throwable, A]](Right(_), Left(_)) - - /** - * Converts a `Try[A]` to a `EitherT[F, Throwable, A]`. - */ - @inline def toEitherT[F[_]](implicit F: Applicative[F]): EitherT[F, Throwable, A] = - EitherT.fromEither[F](toEither) -} + final class TryOps[A](private val ta: Try[A]) extends AnyVal: + def toEitherT[F[_]: Applicative]: EitherT[F, Throwable, A] = EitherT.fromEither[F](ta.toEither) diff --git a/shared/src/test/scala/mouse/AnyFSyntaxTest.scala b/shared/src/test/scala/mouse/AnyFSyntaxTest.scala index 10ed6e8d..18cbd25a 100644 --- a/shared/src/test/scala/mouse/AnyFSyntaxTest.scala +++ b/shared/src/test/scala/mouse/AnyFSyntaxTest.scala @@ -1,24 +1,22 @@ package mouse -import cats.syntax.option._ -import cats.syntax.functor._ -import cats.{Id, ~>} +import cats.syntax.all._ +import cats.{Id} -class AnyFSyntaxTest extends MouseSuite { +import mouse.anyf._ - val emptyK: List ~> List = λ[List ~> List](_ => Nil) +class AnyFSyntaxTest extends MouseSuite: - val double: List ~> List = λ[List ~> List](list => list ::: list) + type ~>[F[_], G[_]] = [A] => F[A] => G[A] - List(1, 2, 3) thrushK emptyK shouldEqual Nil + val emptyK: List ~> List = [A] => (_: List[A]) => Nil - List(5, 10) thrushK double shouldEqual List(5, 10, 5, 10) - - "thing".some thrushK (λ[Option ~> Either[String, *]](_.toRight("foo"))) shouldEqual Right( - "thing" - ) - - (List("This") ||> double - ||> λ[List ~> Option](_.headOption) - ||> λ[Option ~> Id](_.head)) shouldEqual "This" -} +// val double: List ~> List = [T] => ((list) => list ::: list) +// +// testEquals(List(1, 2, 3) thrushK emptyK, Nil, "thrushK emptyK") +// +// testEquals(List(5, 10) thrushK double, List(5, 10, 5, 10), "thrushK double") +// +// testEquals("thing".some thrushK ((_.toRight("foo")): Option ~> Either[String, *]), Right("thing")) +// +// testEquals((List("This") ||> double ||> List ~> Option(_.headOption) ||> Option ~> Id(_.head)), "This") diff --git a/shared/src/test/scala/mouse/AnySyntaxTest.scala b/shared/src/test/scala/mouse/AnySyntaxTest.scala index 42d9b384..5ae52430 100644 --- a/shared/src/test/scala/mouse/AnySyntaxTest.scala +++ b/shared/src/test/scala/mouse/AnySyntaxTest.scala @@ -1,20 +1,19 @@ package mouse -class AnySyntaxTest extends MouseSuite { +import mouse.any._ - true |> (!_) shouldEqual false +class AnySyntaxTest extends MouseSuite: - 5 |> (_ + 44) shouldEqual 49 + testEquals(true |> (!_), false) - Some("thing") |> (_ getOrElse "that") shouldEqual "thing" + testEquals(5 |> (_ + 44), 49) - ("This" |> Function.const("that") - |> (_.capitalize) - |> Function.const("at bat")) shouldEqual "at bat" + testEquals(Some("thing") |> (_ getOrElse "that"), "thing") - mouse.ignore(true) shouldBe (()) + testEquals(("This" |> Function.const("that") + |> (_.capitalize) + |> Function.const("at bat")), "at bat") - 1200 |> (_*2) |> (_-5) |> (_/3) shouldBe (((1200 * 2) - 5) / 3) + testEquals(1200 |> (_*2) |> (_-5) |> (_/3), (((1200 * 2) - 5) / 3)) - "anythingAtAll" |> mouse.ignore shouldBe (()) -} + testEquals("anythingAtAll" |> mouse.ignore, ()) diff --git a/shared/src/test/scala/mouse/BooleanSyntaxTest.scala b/shared/src/test/scala/mouse/BooleanSyntaxTest.scala index f94d83a8..909f9a1d 100644 --- a/shared/src/test/scala/mouse/BooleanSyntaxTest.scala +++ b/shared/src/test/scala/mouse/BooleanSyntaxTest.scala @@ -2,47 +2,48 @@ package mouse import cats.syntax.either._ -class BooleanSyntaxTest extends MouseSuite { +import mouse.boolean._ - true.option(1) shouldEqual Option(1) +class BooleanSyntaxTest extends MouseSuite: - false.option(1) shouldEqual Option.empty[Int] + testEquals(true.option(1), Option(1)) - true.either("error", 1) shouldEqual Either.right(1) + testEquals(false.option(1), Option.empty[Int]) - false.either("error", 1) shouldEqual Either.left("error") + testEquals(true.either("error", 1), Either.right(1)) - true.fold("t", "f") shouldEqual "t" - - false.fold("t", "f") shouldEqual "f" + testEquals(false.either("error", 1), Either.left("error")) - true.valueOrZero(Option(())) shouldEqual Option(()) + testEquals(true.fold("t", "f"), "t") - false.valueOrZero(Option(())) shouldEqual Option.empty[Unit] + testEquals(false.fold("t", "f"), "f") - true.valueOrZero("Yellow") shouldEqual "Yellow" + testEquals(true.valueOrZero(Option(())), Option(())) - false.valueOrZero("Yellow") shouldEqual "" + testEquals(false.valueOrZero(Option(())), Option.empty[Unit]) - true.zeroOrValue("Yellow") shouldEqual "" + testEquals(true.valueOrZero("Yellow"), "Yellow") - false.zeroOrValue("Yellow") shouldEqual "Yellow" + testEquals(false.valueOrZero("Yellow"), "") - true.??("Yellow") shouldEqual "Yellow" + testEquals(true.zeroOrValue("Yellow"), "") - true.!?("Yellow") shouldEqual "" + testEquals(false.zeroOrValue("Yellow"), "Yellow") - true.valueOrPure(Option(1))(2) shouldEqual Some(1) + testEquals(true.??("Yellow"), "Yellow") - false.valueOrPure(Option(1))(2) shouldEqual Some(2) - - def mutilate(x: CharSequence): CharSequence = x.subSequence(1, 2) - true.applyIf("foo")(mutilate) shouldEqual "o" - false.applyIf("foo")(mutilate) shouldEqual "foo" + testEquals(true.!?("Yellow"), "") - true.whenA("foo".asLeft[Int]) shouldEqual Left("foo") - false.whenA("foo".asLeft[Int]) shouldEqual Right(()) + testEquals(true.valueOrPure(Option(1))(2), Some(1)) - true.unlessA("foo".asLeft[Int]) shouldEqual Right(()) - false.unlessA("foo".asLeft[Int]) shouldEqual Left("foo") -} + testEquals(false.valueOrPure(Option(1))(2), Some(2)) + + def mutilate(x: String): String = x.substring(1, 2) + testEquals(true.applyIf("foo")(mutilate), "o") + testEquals(false.applyIf("foo")(mutilate), "foo") + + testEquals(true.whenA("foo".asLeft[Int]), Left("foo")) + testEquals(false.whenA("foo".asLeft[Int]), Right(())) + + testEquals(true.unlessA("foo".asLeft[Int]), Right(())) + testEquals(false.unlessA("foo".asLeft[Int]), Left("foo")) diff --git a/shared/src/test/scala/mouse/DoubleSyntaxTest.scala b/shared/src/test/scala/mouse/DoubleSyntaxTest.scala index c66f83a1..c8ae6806 100644 --- a/shared/src/test/scala/mouse/DoubleSyntaxTest.scala +++ b/shared/src/test/scala/mouse/DoubleSyntaxTest.scala @@ -1,9 +1,13 @@ package mouse -class DoubleSyntaxTest extends MouseSuite { +import cats.Eq - 123456789.123456789.toByteArray shouldEqual Array(65, -99, 111, 52, 84, 126, 107, 117) +import mouse.double._ + +class DoubleSyntaxTest extends MouseSuite: + + testEquals(123456789.123456789.toByteArray, Array[Byte](65, -99, 111, 52, 84, 126, 107, 117)) + + testEquals(1.5.squared, 2.25) - 1.5.squared shouldEqual 2.25 -} diff --git a/shared/src/test/scala/mouse/IntSyntaxTest.scala b/shared/src/test/scala/mouse/IntSyntaxTest.scala index d7f8ecdb..c4be6c6f 100644 --- a/shared/src/test/scala/mouse/IntSyntaxTest.scala +++ b/shared/src/test/scala/mouse/IntSyntaxTest.scala @@ -1,11 +1,11 @@ package mouse -class IntSyntaxTest extends MouseSuite { +import mouse.int._ - 123456789.toByteArray shouldEqual Array(7, 91, -51, 21) +class IntSyntaxTest extends MouseSuite: - 123456789.toBase64 shouldEqual "B1vNFQ" + testEquals(123456789.toByteArray, Array[Byte](7, 91, -51, 21)) - 7.squared shouldEqual 49 + testEquals(123456789.toBase64, "B1vNFQ") -} + testEquals(7.squared, 49) \ No newline at end of file diff --git a/shared/src/test/scala/mouse/LongSyntaxTest.scala b/shared/src/test/scala/mouse/LongSyntaxTest.scala index 1eabe006..2109c891 100644 --- a/shared/src/test/scala/mouse/LongSyntaxTest.scala +++ b/shared/src/test/scala/mouse/LongSyntaxTest.scala @@ -1,11 +1,11 @@ package mouse -class LongSyntaxTest extends MouseSuite { +import mouse.long._ - 123456789123456789L.toByteArray shouldEqual Array(1, -74, -101, 75, -84, -48, 95, 21) +class LongSyntaxTest extends MouseSuite: - 123456789123456789L.toBase64 shouldEqual "AbabS6zQXxU" + testEquals(123456789123456789L.toByteArray, Array[Byte](1, -74, -101, 75, -84, -48, 95, 21)) - 7L.squared shouldEqual 49L + testEquals(123456789123456789L.toBase64, "AbabS6zQXxU") -} + testEquals(7L.squared, 49L) diff --git a/shared/src/test/scala/mouse/MapSyntaxTest.scala b/shared/src/test/scala/mouse/MapSyntaxTest.scala index dd696a44..2bb862b6 100644 --- a/shared/src/test/scala/mouse/MapSyntaxTest.scala +++ b/shared/src/test/scala/mouse/MapSyntaxTest.scala @@ -1,29 +1,29 @@ package mouse -class MapSyntaxTest extends MouseSuite { +class MapSyntaxTest extends MouseSuite: + import map._ - Map(1 -> 2, 3 -> 4).mapKeys(_ * 2) shouldEqual Map(2 -> 2, 6 -> 4) + testEquals(Map(1 -> 2, 3 -> 4).mapKeys(_ * 2), Map(2 -> 2, 6 -> 4)) - Map(1 -> 2, 3 -> 4).mapKeys(identity) shouldEqual Map(1 -> 2, 3 -> 4) + testEquals(Map(1 -> 2, 3 -> 4).mapKeys(identity), Map(1 -> 2, 3 -> 4)) - Map[Int, Int]().mapKeys(identity) shouldEqual Map[Int, Int]() + testEquals(Map[Int, Int]().mapKeys(identity) , Map[Int, Int]()) - Map(1 -> 2, 3 -> 4).updateAtKey(3, _ * 2) shouldEqual Map(1 -> 2, 3 -> 8) + testEquals(Map(1 -> 2, 3 -> 4).updateAtKey(3, _ * 2) , Map(1 -> 2, 3 -> 8)) - Map(1 -> 2, 3 -> 4).updateAtKey(42, _ * 2) shouldEqual Map(1 -> 2, 3 -> 4) + testEquals(Map(1 -> 2, 3 -> 4).updateAtKey(42, _ * 2), Map(1 -> 2, 3 -> 4)) - Map(1 -> 2, 3 -> 4).updateAtKey(3, identity) shouldEqual Map(1 -> 2, 3 -> 4) + testEquals(Map(1 -> 2, 3 -> 4).updateAtKey(3, identity), Map(1 -> 2, 3 -> 4)) - Map(1 -> 2, 3 -> 4).updateAtKeyCombine(3, 5) shouldEqual Map(1 -> 2, 3 -> 9) + testEquals(Map(1 -> 2, 3 -> 4).updateAtKeyCombine(3, 5), Map(1 -> 2, 3 -> 9)) + testEquals(Map(1 -> 2, 3 -> 4).updateAtKeyCombine(42, 5), Map(1 -> 2, 3 -> 4, 42 -> 5)) - Map(1 -> 2, 3 -> 4).updateAtKeyCombine(42, 5) shouldEqual Map(1 -> 2, 3 -> 4, 42 -> 5) - Map(1 -> 2, 3 -> 4).updateAtKeyF(3, Option(_)) shouldEqual Some(Map(1 -> 2, 3 -> 4)) + testEquals(Map(1 -> 2, 3 -> 4).updateAtKeyF(3, Option(_)) , Option(Map(1 -> 2, 3 -> 4))) - Map(1 -> 2, 3 -> 4).updateAtKeyF(3, i => Option(i * 2)) shouldEqual Some(Map(1 -> 2, 3 -> 8)) + testEquals(Map(1 -> 2, 3 -> 4).updateAtKeyF(3, i => Option(i * 2)) , Option(Map(1 -> 2, 3 -> 8))) - Map(1 -> 2, 3 -> 4).updateAtKeyF(42, Option(_)) shouldEqual Some(Map(1 -> 2, 3 -> 4)) -} + testEquals(Map(1 -> 2, 3 -> 4).updateAtKeyF(42, Option(_)) , Option(Map(1 -> 2, 3 -> 4))) diff --git a/shared/src/test/scala/mouse/MouseSuite.scala b/shared/src/test/scala/mouse/MouseSuite.scala index f8fe54e1..d5eccd9c 100644 --- a/shared/src/test/scala/mouse/MouseSuite.scala +++ b/shared/src/test/scala/mouse/MouseSuite.scala @@ -1,33 +1,62 @@ package mouse import cats._ +import cats.syntax.all._ import cats.instances.AllInstances -import org.scalactic.TripleEqualsSupport.BToAEquivalenceConstraint -import org.scalactic.{CanEqual, Equivalence} -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks - -trait MouseSuite - extends AnyFunSuite - with Matchers - with ScalaCheckDrivenPropertyChecks - with AllSharedSyntax - with AllInstances { - implicit val eq0 = new Eq[NumberFormatException] { - override def eqv(x: NumberFormatException, y: NumberFormatException): Boolean = - x.getMessage == y.getMessage - } - - implicit val eq1 = new Eq[IllegalArgumentException] { - override def eqv(x: IllegalArgumentException, y: IllegalArgumentException): Boolean = - x.getMessage == y.getMessage - } - - final class MouseEquivalence[T](T: Eq[T]) extends Equivalence[T] { - def areEquivalent(a: T, b: T): Boolean = T.eqv(a, b) - } - - implicit def mouseCanEqual[A, B](implicit A: Eq[A], ev: B <:< A): CanEqual[A, B] = - new BToAEquivalenceConstraint[A, B](new MouseEquivalence(A), ev) -} + +import munit._ + +import munit.internal.console.{Lines, Printers, StackTraces} +import munit.internal.difflib.Diffs + +import scala.reflect.ClassTag +import scala.util.control.NonFatal +import scala.collection.mutable +import munit.internal.console.AnsiColors +import org.junit.AssumptionViolatedException +import munit.internal.MacroCompat + +trait MouseSuite extends munit.FunSuite: + + given Eq[Array[Byte]] = Eq.instance(_ sameElements _) + + given [T <: Throwable]: Eq[T] = Eq.instance(_.getMessage == _.getMessage) + + def testEquals[T: Eq](obtained: T, expected: T): Unit = + test(getClass.getSimpleName)(assertEq(obtained, expected)) + + def assertEq[T: Eq]( + obtained: T, + expected: T, + clue: => Any = "values are not the same" + )(implicit loc: munit.Location): Unit = { + StackTraces.dropInside { + if (obtained =!= expected) { + Diffs.assertNoDiff( + munitPrint(obtained), + munitPrint(expected), + new munit.internal.difflib.ComparisonFailExceptionHandler { + def handle( + message: String, + obtained: String, + expected: String, + loc: Location + ): Nothing = fail(message) + }, + munitPrint(clue), + printObtainedAsStripMargin = false + ) + // try with `.toString` in case `munitPrint()` produces identical formatting for both values. + Diffs.assertNoDiff( + obtained.toString(), + expected.toString(), + message => fail(message), + munitPrint(clue), + printObtainedAsStripMargin = false + ) + fail( + s"values are not equal even if they have the same `toString()`: $obtained" + ) + } + } + } \ No newline at end of file diff --git a/shared/src/test/scala/mouse/OptionSyntaxTest.scala b/shared/src/test/scala/mouse/OptionSyntaxTest.scala index 55bc80f0..88941b4b 100644 --- a/shared/src/test/scala/mouse/OptionSyntaxTest.scala +++ b/shared/src/test/scala/mouse/OptionSyntaxTest.scala @@ -1,33 +1,20 @@ package mouse +import mouse.option._ import scala.util.{Failure, Success} -class OptionSyntaxTest extends MouseSuite { +class OptionSyntaxTest extends MouseSuite: - Option(1).cata(_.toString, "") shouldEqual "1" + testEquals(Option(1).cata(_.toString, ""), "1") - Option.empty[Int].cata(_.toString, "") shouldEqual "" + testEquals(Option.empty[Int].cata(_.toString, ""), "") - Option(1).toTry(new RuntimeException("Err")) shouldEqual Success(1) + testEquals(Option(1).toTry(new RuntimeException("Err")), Success(1)) val ex = new RuntimeException("Err") - Option.empty[Int].toTry(ex) shouldEqual Failure(ex) + testEquals(Option.empty[Int].toTry(ex), Failure(ex)) - Option(3).right("S") shouldEqual Option(3).toRight("S") + testEquals(Option(3).right("S"), Option(3).toRight("S")) - Option(3).left("S") shouldEqual Option(3).toLeft("S") - - None.right("S") shouldEqual None.toRight("S") - - None.left("S") shouldEqual None.toLeft("S") - - implicit class ExtraTest[A](a: A) { - def shouldBeA[T](implicit ev: T =:= A) = succeed - } - - Option(3).right("S").shouldBeA[Either[String, Int]] - - Option(3).left("S").shouldBeA[Either[Int, String]] - -} + testEquals(Option(3).left("S"), Option(3).toLeft("S")) diff --git a/shared/src/test/scala/mouse/PartialFunctionLiftTest.scala b/shared/src/test/scala/mouse/PartialFunctionLiftTest.scala index 2229becf..7f46984b 100644 --- a/shared/src/test/scala/mouse/PartialFunctionLiftTest.scala +++ b/shared/src/test/scala/mouse/PartialFunctionLiftTest.scala @@ -1,11 +1,11 @@ package mouse -class PartialFunctionLiftTest extends MouseSuite { +import mouse.partialFunction._ - val f = liftEither[Option[Int]]({case Some(n) => n}, a => s"Unexpected: $a") +class PartialFunctionLiftTest extends MouseSuite: - f(Some(6)) shouldEqual Right(6) + val f = liftEither[Option[Int]]({case Some(n) => n}, a => s"Unexpected: $a") - f(None) shouldEqual Left("Unexpected: None") + testEquals(f(Some(6)), Right(6)) -} + testEquals(f(None), Left("Unexpected: None")) diff --git a/shared/src/test/scala/mouse/StringSyntaxTests.scala b/shared/src/test/scala/mouse/StringSyntaxTests.scala index c0ee747b..c340d873 100644 --- a/shared/src/test/scala/mouse/StringSyntaxTests.scala +++ b/shared/src/test/scala/mouse/StringSyntaxTests.scala @@ -1,184 +1,51 @@ package mouse -import org.scalacheck.Arbitrary._ -import org.scalacheck.{Arbitrary, Gen} +import mouse.string._ import cats.syntax.all._ -class StringSyntaxTests extends MouseSuite { - - //FIXME fixed in master, remove post 0.8.2 - implicit def catsSyntaxEitherId[A](a: A): EitherIdOps[A] = new EitherIdOps(a) - - test("parseInt") { - "123".parseInt should ===(123.asRight[NumberFormatException]) - - "blah".parseInt should ===(new NumberFormatException("For input string: \"blah\"").asLeft) - - forAll { i: Int => - i.toString.parseInt should ===(i.asRight[NumberFormatException]) - } - - forAll { i: Int => - i.toString.parseInt.toOption should ===(i.toString.parseIntOption) - } - - forAll { i: Int => - i.toString.parseInt.toValidated should ===(i.toString.parseIntValidated) - } - - } - - test("parseLong") { - "123".parseLong should ===(123L.asRight[NumberFormatException]) - - "blah".parseLong should ===(new NumberFormatException("For input string: \"blah\"").asLeft) - - forAll { i: Long => - i.toString.parseLong should ===(i.asRight[NumberFormatException]) - } - - forAll { i: Long => - i.toString.parseLong.toOption should ===(i.toString.parseLongOption) - } - - forAll { i: Long => - i.toString.parseLong.toValidated should ===(i.toString.parseLongValidated) - } - - } - - test("parseShort") { - "123".parseShort should ===(123.toShort.asRight[NumberFormatException]) - - "blah".parseShort should ===(new NumberFormatException("For input string: \"blah\"").asLeft) - - forAll { i: Short => - i.toString.parseShort should ===(i.asRight[NumberFormatException]) - } - - forAll { i: Short => - i.toString.parseShort.toOption should ===(i.toString.parseShortOption) - } - - forAll { i: Short => - i.toString.parseShort.toValidated should ===(i.toString.parseShortValidated) - } - - } - - test("parseDouble") { - "123.1".parseDouble should ===(123.1.asRight[NumberFormatException]) - - "blah".parseDouble should ===(new NumberFormatException("For input string: \"blah\"").asLeft) - - forAll { i: Double => - i.toString.parseDouble should ===(i.asRight[NumberFormatException]) - } - - forAll { i: Double => - i.toString.parseDouble.toOption should ===(i.toString.parseDoubleOption) - } - - forAll { i: Double => - i.toString.parseDouble.toValidated should ===(i.toString.parseDoubleValidated) - } - } - - test("parseFloat") { - "blah".parseFloat should ===(new NumberFormatException("For input string: \"blah\"").asLeft) - - forAll { i: Float => - i.toString.parseFloat should ===(i.asRight[NumberFormatException]) - } - - forAll { i: Float => - i.toString.parseFloat.toOption should ===(i.toString.parseFloatOption) - } - - forAll { i: Float => - i.toString.parseFloat.toValidated should ===(i.toString.parseFloatValidated) - } - } - - test("parseByte") { - "123".parseByte should ===(123.toByte.asRight[NumberFormatException]) - - "blah".parseByte should ===(new NumberFormatException("For input string: \"blah\"").asLeft) - - forAll { i: Byte => - i.toString.parseByte should ===(i.asRight[NumberFormatException]) - } - - forAll { i: Byte => - i.toString.parseByte.toOption should ===(i.toString.parseByteOption) - } - - forAll { i: Byte => - i.toString.parseByte.toValidated should ===(i.toString.parseByteValidated) - } - } - - test("parseBigInt") { - "123".parseBigInt should ===(BigInt("123").asRight[NumberFormatException]) - - "blah".parseBigInt should ===(new NumberFormatException("For input string: \"blah\"").asLeft) - - forAll { i: BigInt => - i.toString.parseBigInt should ===(i.asRight[NumberFormatException]) - } - - forAll { i: BigInt => - i.toString.parseBigInt.toOption should ===(i.toString.parseBigIntOption) - } - - forAll { i: BigInt => - i.toString.parseBigInt.toValidated should ===(i.toString.parseBigIntValidated) - } - } - - test("parseBigDecimal") { - "123.45".parseBigDecimal should ===(BigDecimal("123.45").asRight[NumberFormatException]) - - // We assert the class of the exception because the error message differs between the JVM and JS - "blah".parseBigDecimal.leftMap(_.getClass) should ===(classOf[NumberFormatException].asLeft) - - forAll { i: BigDecimal => - i.toString.parseBigDecimal should ===(i.asRight[NumberFormatException]) - } - - forAll { i: BigDecimal => - i.toString.parseBigDecimal.toOption should ===(i.toString.parseBigDecimalOption) - } - - forAll { i: BigDecimal => - i.toString.parseBigDecimal.toValidated should ===(i.toString.parseBigDecimalValidated) - } - } - - test("parseBoolean") { - "true".parseBoolean should ===(true.asRight[IllegalArgumentException]) - "true".parseBoolean.toOption should ===("true".parseBooleanOption) - "true".parseBoolean.toValidated should ===("true".parseBooleanValidated) - "false".parseBoolean should ===(false.asRight[IllegalArgumentException]) - "false".parseBoolean.toOption should ===("false".parseBooleanOption) - "false".parseBoolean.toValidated should ===("false".parseBooleanValidated) - - "TRUE".parseBoolean should ===("true".parseBoolean) - "FALSE".parseBoolean should ===("false".parseBoolean) - - val stringGen: Gen[String] = Arbitrary.arbString.arbitrary.filter(s => !s.equalsIgnoreCase("true") && !s.equalsIgnoreCase("false")) - - forAll(stringGen) { s: String => - s.parseBoolean should ===(new IllegalArgumentException("For input string: \"" + s + "\"").asLeft) - } - } - -} - -final class EitherIdOps[A](val obj: A) extends AnyVal { - /** Wrap a value in `Left`. */ - def asLeft[B]: Either[A, B] = Left(obj) - - /** Wrap a value in `Right`. */ - def asRight[B]: Either[B, A] = Right(obj) -} +class StringSyntaxTests extends MouseSuite: + + testEquals("123".toIntEither, 123.asRight) + testEquals("123".toIntValidated, 123.valid) + testEquals("blah".toIntEither, "blah".asLeft) + testEquals("blah".toIntValidated, "blah".invalid) + + testEquals("123".toLongEither, 123L.asRight) + testEquals("123".toLongValidated, 123L.valid) + testEquals("blah".toLongEither, "blah".asLeft) + testEquals("blah".toLongValidated, "blah".invalid) + + testEquals("true".toBooleanEither, true.asRight) + testEquals("true".toBooleanValidated, true.valid) + testEquals("blah".toBooleanEither, "blah".asLeft) + testEquals("blah".toBooleanValidated, "blah".invalid) + + testEquals("123".toShortEither, (123: Short).asRight) + testEquals("123".toShortValidated, (123: Short).valid) + testEquals("blah".toShortEither, "blah".asLeft) + testEquals("blah".toShortValidated, "blah".invalid) + + testEquals("123".toByteEither, (123: Byte).asRight) + testEquals("123".toByteValidated, (123: Byte).valid) + testEquals("blah".toByteEither, "blah".asLeft) + testEquals("blah".toByteValidated, "blah".invalid) + + testEquals("123.4".toFloatEither, 123.4f.asRight) + testEquals("123.4".toFloatValidated, 123.4f.valid) + testEquals("blah".toFloatEither, "blah".asLeft) + testEquals("blah".toFloatValidated, "blah".invalid) + + testEquals("123.4".toDoubleEither, 123.4.asRight) + testEquals("123.4".toDoubleValidated, 123.4.valid) + testEquals("blah".toDoubleEither, "blah".asLeft) + testEquals("blah".toDoubleValidated, "blah".invalid) + + testEquals("123.4".toBigDecimalEither, BigDecimal("123.4").asRight) + testEquals("123.4".toBigDecimalValidated, BigDecimal("123.4").valid) + testEquals("blah".toBigDecimalEither, "blah".asLeft) + testEquals("blah".toBigDecimalValidated, "blah".invalid) + + testEquals("123".toBigIntEither, BigInt(123).asRight) + testEquals("123".toBigIntValidated, BigInt(123).valid) + testEquals("blah".toBigIntEither, "blah".asLeft) + testEquals("blah".toBigIntValidated, "blah".invalid) \ No newline at end of file diff --git a/shared/src/test/scala/mouse/TrySyntaxTest.scala b/shared/src/test/scala/mouse/TrySyntaxTest.scala index 04ad1761..3b12dc03 100644 --- a/shared/src/test/scala/mouse/TrySyntaxTest.scala +++ b/shared/src/test/scala/mouse/TrySyntaxTest.scala @@ -1,64 +1,9 @@ package mouse +import mouse.`try`._ import scala.util.{Failure, Success, Try} -import org.scalacheck.Gen +import cats.data.EitherT -class TrySyntaxTest extends MouseSuite { +class TrySyntaxTest extends MouseSuite: - private val exceptionList = - Seq( - new RuntimeException(_: String), - new IllegalArgumentException(_: String), - new UnsupportedOperationException(_: String), - new ArrayIndexOutOfBoundsException(_: String), - new NumberFormatException(_: String) - ) - - private def genThrowable: Gen[Throwable] = for { - message <- Gen.alphaStr - throwable <- Gen.oneOf(exceptionList) - } yield throwable(message) - - private def genTrySuccess[T](genT: => Gen[T]): Gen[(T, Try[T])] = for { - n <- genT - tr <- Gen.oneOf(Try(n), Success(n)) - } yield (n, tr) - - private def genTryInt = genTrySuccess[Int](Gen.choose(-10000, 10000)) - - private def genTryString = genTrySuccess[String](Gen.alphaStr) - - private def genTryBoolean = genTrySuccess[Boolean](Gen.oneOf(true, false)) - - private def genTryFailure[T]: Gen[(Throwable, Try[T])] = for { - th <- genThrowable - tr <- Gen.oneOf(Try[T](throw th), Failure[T](th)) - } yield (th, tr) - - private def randomNumber(min: Int, max: Int): Int = Gen.choose(min, max).sample.get - - forAll(genTryInt) { case (n, t) => - t.cata(identity, _ => n * n) shouldEqual n - } - - forAll(genTryFailure[Int]) { case (th, tr) => - val rnd = randomNumber(50000, 60000) - tr.cata(identity, _ => rnd) shouldEqual (rnd) - } - - forAll(genTryString) { case (msg, tr) => - tr.toEither shouldEqual Right(msg) - } - - forAll(genTryFailure[String]) { case (th, tr) => - tr.toEither shouldEqual Left(th) - } - - implicit class ExtraTest[A](a: A) { - def shouldBeA[T](implicit ev: T =:= A) = succeed - } - - forAll(genTryBoolean, minSuccessful(10)) { case (_, t) => - t.toEither.shouldBeA[Either[Throwable, Boolean]] - } -} + testEquals(Try("foo").toEitherT[Option], EitherT.fromEither[Option](Try("foo").toEither)) \ No newline at end of file