├── .envrc ├── project ├── build.properties └── plugins.sbt ├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── CODE_OF_CONDUCT.md ├── .gitignore ├── shared └── src │ ├── test │ └── scala │ │ └── cats │ │ └── scalatest │ │ ├── TestBase.scala │ │ ├── EitherMatchersSpec.scala │ │ ├── EitherValuesSpec.scala │ │ ├── NonEmptyListScalaTestInstancesSpec.scala │ │ ├── ValidatedMatchersSpec.scala │ │ └── ValidatedValuesSpec.scala │ └── main │ └── scala │ └── cats │ └── scalatest │ ├── NonEmptyListScalaTestInstances.scala │ ├── EitherMatchers.scala │ ├── EitherValues.scala │ ├── ValidatedValues.scala │ └── ValidatedMatchers.scala ├── flake.nix ├── .scalafmt.conf ├── flake.lock ├── scalastyle-config.xml ├── README.md └── LICENSE /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.9.7 2 | 113e467082dc892e94e7160f7906fca2a3a770f5 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | # Check for updates to GitHub Actions every weekday 9 | interval: "monthly" 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - name: Restore caches 15 | uses: coursier/cache-action@v7 16 | - uses: coursier/setup-action@v1 17 | with: 18 | jvm: adopt:11 19 | apps: sbt 20 | - name: Run tests 21 | run: sbt +compile +test scalafmtCheckAll scalastyle 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ["*"] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v6 11 | with: 12 | fetch-depth: 0 13 | - uses: olafurpg/setup-scala@v13 14 | - run: sbt ci-release 15 | env: 16 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 17 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 18 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 19 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 20 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 2 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") 3 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.3") 4 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") 5 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.20.1") 6 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") 7 | 8 | // workaround for conflict between sbt-scoverage and scalastyle-sbt-plugin 9 | // https://github.com/scala/bug/issues/12632 10 | ThisBuild / libraryDependencySchemes ++= Seq( 11 | "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always 12 | ) 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, safe and welcoming 4 | environment for all, regardless of level of experience, gender, gender 5 | identity and expression, sexual orientation, disability, personal 6 | appearance, body size, race, ethnicity, age, religion, nationality, or 7 | other such characteristics. 8 | 9 | Everyone is expected to follow the [Scala Code of Conduct] when 10 | discussing the project on the available communication channels. If you 11 | are being harassed, please contact us immediately so that we can 12 | support you. 13 | 14 | ## Moderation 15 | 16 | For any questions, concerns, or moderation requests please contact a 17 | member of the project. 18 | 19 | [Scala Code of Conduct]: https://typelevel.org/code-of-conduct.html 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Scala-IDE specific 2 | # sbt specific 3 | *.iml 4 | *.ipr 5 | *.iws 6 | *.log 7 | .DS_Store 8 | .cache 9 | .cache/ 10 | .classpath 11 | .history 12 | .history/ 13 | .idea 14 | .lib/ 15 | .project 16 | .scala_dependencies 17 | .settings 18 | .target 19 | .worksheet 20 | /.classpath 21 | /.idea_modules 22 | /.project 23 | /.settings 24 | /RUNNING_PID 25 | /out 26 | /repository*.class 27 | bin/ 28 | dist 29 | dist/* 30 | lib_managed/ 31 | logs 32 | project/boot/ 33 | project/plugins/project/ 34 | project/project 35 | project/target 36 | src_managed/ 37 | target 38 | target/ 39 | tmp 40 | .cache-main 41 | .cache-tests 42 | data 43 | workbench.xmi 44 | couchbase-dev.cfg 45 | *.sublime-workspace 46 | *.sublime-project 47 | src/jekyll/_tutorials 48 | **/.bloop 49 | .vscode 50 | .bsp 51 | .metals 52 | project/metals.sbt 53 | -------------------------------------------------------------------------------- /shared/src/test/scala/cats/scalatest/TestBase.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import org.scalatest.OptionValues 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.wordspec.AnyWordSpec 6 | 7 | abstract class TestBase extends AnyWordSpec with Matchers with OptionValues { 8 | val thisRecord = "I will not buy this record, it is scratched." 9 | val thisTobacconist = "Ah! I will not buy this tobacconist's, it is scratched." 10 | val hovercraft = "Yes, cigarettes. My hovercraft is full of eels." 11 | 12 | // As advised by @sjrd: https://gitter.im/scala-js/scala-js?at=5ce51e9b9d64e537bcef6f08 13 | final val isJS: Boolean = 1.0.toString() == "1" 14 | final val isJVM: Boolean = !isJS 15 | 16 | /** 17 | * Shamelessly swiped from Scalatest. 18 | */ 19 | final def thisLineNumber: Int = { 20 | val st = Thread.currentThread.getStackTrace 21 | 22 | if (!st(2).getMethodName.contains("thisLineNumber")) 23 | st(2).getLineNumber 24 | else 25 | st(3).getLineNumber 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /shared/src/main/scala/cats/scalatest/NonEmptyListScalaTestInstances.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import scala.collection.GenTraversable 4 | import org.scalatest.enablers.Collecting 5 | import cats.data.NonEmptyList 6 | 7 | trait NonEmptyListScalaTestInstances { 8 | 9 | /** 10 | * Support for using .loneElement on NonEmptyList 11 | * http://www.scalatest.org/user_guide/using_matchers#singleElementCollections 12 | */ 13 | implicit def collectingNonEmptyList[A]: Collecting[A, NonEmptyList[A]] = 14 | new Collecting[A, NonEmptyList[A]] { 15 | override def loneElementOf(collection: NonEmptyList[A]): Option[A] = 16 | if (collection.tail.isEmpty) Some(collection.head) else None 17 | override def sizeOf(collection: NonEmptyList[A]): Int = collection.length 18 | override def genTraversableFrom(collection: NonEmptyList[A]): GenTraversable[A] = collection.toList 19 | } 20 | } 21 | 22 | /** 23 | * Companion object for easy importing – rather than mixing in. 24 | */ 25 | object NonEmptyListScalaTestInstances extends NonEmptyListScalaTestInstances 26 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Provision a dev environment"; 3 | 4 | inputs = { 5 | typelevel-nix.url = "github:typelevel/typelevel-nix"; 6 | nixpkgs.follows = "typelevel-nix/nixpkgs"; 7 | flake-utils.follows = "typelevel-nix/flake-utils"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, flake-utils, typelevel-nix }: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | pkgs = import nixpkgs { 14 | inherit system; 15 | overlays = [ typelevel-nix.overlay ]; 16 | }; 17 | 18 | mkShell = jdk: pkgs.devshell.mkShell { 19 | imports = [ typelevel-nix.typelevelShell ]; 20 | name = "cats-scalatest"; 21 | typelevelShell = { 22 | jdk.package = jdk; 23 | nodejs.enable = true; 24 | }; 25 | }; 26 | in 27 | rec { 28 | devShell = devShells."temurin@11"; 29 | 30 | devShells = { 31 | "temurin@11" = mkShell pkgs.temurin-bin-11; 32 | "temurin@17" = mkShell pkgs.temurin-bin-17; 33 | }; 34 | } 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.10.2" 2 | style = default 3 | runner.dialect = scala3 4 | docstrings.style = Asterisk 5 | maxColumn = 120 6 | continuationIndent.defnSite = 2 7 | includeCurlyBraceInSelectChains = false 8 | assumeStandardLibraryStripMargin = true 9 | align.tokens."+" = [ 10 | "<-", #Try to align simple for blocks for readability 11 | "//", #If you have many `//` on the end of lines next to each other, try and align them for readability 12 | {code = "%", owner = "Term.ApplyInfix"}, #SBT specific stuff. Format the dependencies in a way that makes them more readable. 13 | {code = "%%", owner = "Term.ApplyInfix"}, 14 | {code = "%%%", owner = "Term.ApplyInfix"} 15 | ] 16 | 17 | rewrite.rules = [ 18 | AvoidInfix 19 | RedundantBraces 20 | RedundantParens 21 | AsciiSortImports 22 | PreferCurlyFors 23 | ] 24 | 25 | rewrite.neverInfix.excludeFilters = [ 26 | until 27 | to 28 | by 29 | eq 30 | ne 31 | "should.*" 32 | "contain.*" 33 | "must.*" 34 | in 35 | be 36 | taggedAs 37 | thrownBy 38 | synchronized 39 | have 40 | when 41 | size 42 | theSameElementsAs 43 | ] 44 | -------------------------------------------------------------------------------- /shared/src/test/scala/cats/scalatest/EitherMatchersSpec.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import scala.util.{Left, Right} 4 | 5 | class EitherMatchersSpec extends TestBase with EitherMatchers { 6 | val goodHovercraft: Right[Nothing, String] = Right(hovercraft) 7 | val badTobacconist: Left[String, Nothing] = Left(thisTobacconist) 8 | val badRecord: Left[String, Nothing] = Left(thisRecord) 9 | 10 | "EitherMatchers" should { 11 | "Match 'blind' invalid (i.e. not with specific element)" in { 12 | badTobacconist should be(left) 13 | } 14 | "Match 'valued' left disjunction syntax" in { 15 | badTobacconist should beLeft(thisTobacconist) 16 | } 17 | "Match 'valued' right disjunction syntax" in { 18 | goodHovercraft should beRight(hovercraft) 19 | } 20 | "Match 'blind' right disjunction syntax (i.e. with no specific element)" in { 21 | goodHovercraft should be(right) 22 | } 23 | "Match negation of left when it's right" in { 24 | goodHovercraft should not be left 25 | } 26 | "Match negation of right when it's left" in { 27 | badRecord should not be right 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /shared/src/test/scala/cats/scalatest/EitherValuesSpec.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import org.scalatest.exceptions.TestFailedException 4 | import scala.util.{Either, Left, Right} 5 | 6 | class EitherValuesSpec extends TestBase { 7 | import EitherValues._ 8 | 9 | "value on Either" should { 10 | "return the value inside a Right if that Either is Right" in { 11 | val r: String Either String = Right(thisRecord) 12 | r.value should ===(thisRecord) 13 | } 14 | 15 | "should throw TestFailedException if that Either is a left " in { 16 | val r: String Either String = Left(thisTobacconist) 17 | val caught = 18 | intercept[TestFailedException] { 19 | r.value should ===(thisRecord) 20 | } 21 | if (isJVM) 22 | caught.failedCodeLineNumber.value should equal(thisLineNumber - 3) 23 | caught.failedCodeFileName.value should be("EitherValuesSpec.scala") 24 | } 25 | } 26 | 27 | "leftValue on Either" should { 28 | "return the value if it's left" in { 29 | val r = Left(thisRecord) 30 | r.leftValue should ===(thisRecord) 31 | } 32 | 33 | "throw TestFailedException if the Either is right" in { 34 | val r = Right(thisRecord) 35 | val caught = intercept[TestFailedException] { 36 | r.leftValue 37 | } 38 | if (isJVM) 39 | caught.failedCodeLineNumber.value should equal(thisLineNumber - 3) 40 | caught.failedCodeFileName.value should be("EitherValuesSpec.scala") 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shared/src/test/scala/cats/scalatest/NonEmptyListScalaTestInstancesSpec.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import org.scalatest.exceptions.TestFailedException 4 | import org.scalatest.enablers.Collecting 5 | import org.scalatest.LoneElement._ 6 | import org.scalatest.Inspectors._ 7 | import cats.data.NonEmptyList 8 | 9 | class NonEmptyListScalaTestInstancesSpec extends TestBase { 10 | import NonEmptyListScalaTestInstances._ 11 | 12 | "loneElement" should { 13 | "apply an assertion when there is a single element" in { 14 | val nel: NonEmptyList[Int] = NonEmptyList.one(10) 15 | nel.loneElement should be <= 10 16 | } 17 | 18 | "should throw TestFailedException if the NonEmptyList has more elements" in { 19 | val nel: NonEmptyList[Int] = NonEmptyList.of(10, 16) 20 | val caught = 21 | intercept[TestFailedException] { 22 | nel.loneElement should ===(thisRecord) 23 | } 24 | if (isJVM) 25 | caught.failedCodeLineNumber.value should equal(thisLineNumber - 3) 26 | caught.failedCodeFileName.value should be("NonEmptyListScalaTestInstancesSpec.scala") 27 | } 28 | } 29 | 30 | "inspectors" should { 31 | "state something about all elements" in { 32 | val nel: NonEmptyList[Int] = NonEmptyList.of(1, 2, 3, 4, 5) 33 | forAll(nel)(_ should be > 0) 34 | } 35 | } 36 | 37 | "sizeOf" should { 38 | "return the size of the collection" in { 39 | val nel: NonEmptyList[Int] = NonEmptyList.of(1, 2) 40 | implicitly[Collecting[Int, NonEmptyList[Int]]].sizeOf(nel) shouldBe 2 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /shared/src/test/scala/cats/scalatest/ValidatedMatchersSpec.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import cats.data.Validated.{Invalid, Valid} 4 | import cats.data.{NonEmptyList, ValidatedNel} 5 | 6 | class ValidatedMatchersSpec extends TestBase with ValidatedMatchers { 7 | "ValidatedMatchers" should { 8 | val simpleFailureNel: ValidatedNel[String, Nothing] = Invalid(NonEmptyList.of(thisRecord, thisTobacconist)) 9 | 10 | "Match one specific element in an Invalid NEL" in { 11 | simpleFailureNel should haveInvalid(thisRecord) 12 | } 13 | 14 | "Match multiple specific elements in an Invalid NEL" in { 15 | simpleFailureNel should haveInvalid(thisRecord).and(haveInvalid(thisTobacconist)) 16 | } 17 | 18 | "Match a specific element of a single Invalid" in { 19 | Invalid(thisRecord) should beInvalid(thisRecord) 20 | } 21 | 22 | "Test whether a Validated instance is a Invalid w/o specific element value" in { 23 | Invalid(thisTobacconist) should be(invalid) 24 | } 25 | 26 | "By negating 'invalid', test whether a Validated instance is a Valid" in { 27 | Valid(hovercraft) should not be invalid 28 | } 29 | 30 | "Test whether a Validated instance is a Valid" in { 31 | Valid(hovercraft) should be(valid) 32 | } 33 | 34 | "By negating 'valid', test whether a Validated instance is an invalid" in { 35 | Invalid(thisTobacconist) should not be valid 36 | } 37 | 38 | "Match a specific element of a single Valid" in { 39 | Valid(hovercraft) should beValid(hovercraft) 40 | } 41 | 42 | "Match one specific type in an Invalid NEL" in { 43 | sealed abstract class Color(value: Int) 44 | case object Red extends Color(0xff0000) 45 | case object Green extends Color(0x00ff00) 46 | simpleFailureNel should haveAnInvalid[String] 47 | 48 | val nel: ValidatedNel[Color, _] = Invalid(NonEmptyList.of(Red)) 49 | nel shouldNot haveAnInvalid[String] 50 | nel should haveAnInvalid[Red.type] 51 | nel shouldNot haveAnInvalid[Green.type] 52 | 53 | val nel2: ValidatedNel[String, Unit] = Valid(()) 54 | nel2 shouldNot haveAnInvalid[String] 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /shared/src/main/scala/cats/scalatest/EitherMatchers.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import org.scalatest.matchers.{BeMatcher, MatchResult, Matcher} 4 | 5 | import scala.util.Either 6 | import cats.syntax.either._ 7 | 8 | trait EitherMatchers { 9 | 10 | /** 11 | * Checks to see if `scala.util.Either` is a specific Left element. 12 | */ 13 | def beLeft[E](element: E): Matcher[E Either _] = new BeCatsLeftEither[E](element) 14 | 15 | /** 16 | * Checks to see if `scala.util.Either` is a `Left`. 17 | */ 18 | def left[E]: BeMatcher[E Either _] = new IsCatsLeftEitherMatcher[E] 19 | 20 | /** 21 | * Checks to see if `scala.util.Either` is a specific Right element. 22 | */ 23 | def beRight[T](element: T): Matcher[_ Either T] = new BeCatsRightEitherMatcher[T](element) 24 | 25 | /** 26 | * Checks to see if `scala.util.Either` is a `Right`. 27 | */ 28 | def right[T]: BeMatcher[_ Either T] = new IsCatsRightEitherMatcher[T] 29 | } 30 | 31 | /** 32 | * Import singleton in case you prefer to import rather than mix in. 33 | * {{{ 34 | * import EitherMatchers._ 35 | * result should beRight(100) 36 | * }}} 37 | */ 38 | object EitherMatchers extends EitherMatchers 39 | 40 | final private[scalatest] class BeCatsRightEitherMatcher[T](element: T) extends Matcher[_ Either T] { 41 | def apply(either: _ Either T): MatchResult = 42 | MatchResult( 43 | either.fold(_ => false, _ == element), 44 | s"'$either' did not contain an Right element matching '$element'.", 45 | s"'$either' contained an Right element matching '$element', but should not have." 46 | ) 47 | } 48 | 49 | final private[scalatest] class BeCatsLeftEither[E](element: E) extends Matcher[E Either _] { 50 | def apply(either: E Either _): MatchResult = 51 | MatchResult( 52 | either.fold(_ == element, _ => false), 53 | s"'$either' did not contain an Left element matching '$element'.", 54 | s"'$either' contained an Left element matching '$element', but should not have." 55 | ) 56 | } 57 | 58 | final private[scalatest] class IsCatsLeftEitherMatcher[E] extends BeMatcher[E Either _] { 59 | def apply(either: E Either _): MatchResult = 60 | MatchResult( 61 | either.isLeft, 62 | s"'$either' was not an Left, but should have been.", 63 | s"'$either' was an Left, but should *NOT* have been." 64 | ) 65 | } 66 | 67 | final private[scalatest] class IsCatsRightEitherMatcher[T] extends BeMatcher[_ Either T] { 68 | def apply(either: _ Either T): MatchResult = 69 | MatchResult( 70 | either.isRight, 71 | s"'$either' was not an Right, but should have been.", 72 | s"'$either' was an Right, but should *NOT* have been." 73 | ) 74 | } 75 | -------------------------------------------------------------------------------- /shared/src/test/scala/cats/scalatest/ValidatedValuesSpec.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import org.scalatest.exceptions.TestFailedException 4 | import cats.data.Validated 5 | 6 | class ValidatedValuesSpec extends TestBase { 7 | import ValidatedValues._ 8 | 9 | "value on Validated" should { 10 | "return the value inside a Validated.Right if that Validated is Validated.Right" in { 11 | val r: String Validated String = Validated.Valid(thisRecord) 12 | r.value should ===(thisRecord) 13 | } 14 | 15 | "should throw TestFailedException if that Validated is a left " in { 16 | val r: String Validated String = Validated.Invalid(thisTobacconist) 17 | val caught = 18 | intercept[TestFailedException] { 19 | r.value should ===(thisRecord) 20 | } 21 | if (isJVM) 22 | caught.failedCodeLineNumber.value should equal(thisLineNumber - 3) 23 | caught.failedCodeFileName.value should be("ValidatedValuesSpec.scala") 24 | } 25 | } 26 | 27 | "invalidValue on Validated" should { 28 | "return the value if it's invalid" in { 29 | val r = Validated.Invalid(thisRecord) 30 | r.invalidValue should ===(thisRecord) 31 | } 32 | 33 | "throw TestFailedException if the Validated is Valid" in { 34 | val r = Validated.Valid(thisRecord) 35 | val caught = intercept[TestFailedException] { 36 | r.invalidValue 37 | } 38 | if (isJVM) 39 | caught.failedCodeLineNumber.value should equal(thisLineNumber - 3) 40 | caught.failedCodeFileName.value should be("ValidatedValuesSpec.scala") 41 | } 42 | } 43 | 44 | "valid on Validated" should { 45 | "return the valid if it's a Valid" in { 46 | val r = Validated.Valid(thisRecord) 47 | r.valid should ===(r) 48 | } 49 | 50 | "throw TestFailedException if the Validated is Invalid" in { 51 | val r: String Validated String = Validated.Invalid(thisTobacconist) 52 | val caught = 53 | intercept[TestFailedException] { 54 | r.valid should ===(r) 55 | } 56 | if (isJVM) 57 | caught.failedCodeLineNumber.value should equal(thisLineNumber - 3) 58 | caught.failedCodeFileName.value should be("ValidatedValuesSpec.scala") 59 | } 60 | } 61 | 62 | "invalid on Validated" should { 63 | "return the invalid if it's a Invalid" in { 64 | val r = Validated.Invalid(thisTobacconist) 65 | r.invalid should ===(r) 66 | } 67 | 68 | "throw TestFailedException if the Validated is Valid" in { 69 | val r: String Validated String = Validated.Valid(thisRecord) 70 | val caught = 71 | intercept[TestFailedException] { 72 | r.invalid should ===(r) 73 | } 74 | if (isJVM) 75 | caught.failedCodeLineNumber.value should equal(thisLineNumber - 3) 76 | caught.failedCodeFileName.value should be("ValidatedValuesSpec.scala") 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /shared/src/main/scala/cats/scalatest/EitherValues.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import org.scalatest.exceptions.{StackDepthException, TestFailedException} 4 | 5 | import org.scalactic.source 6 | import scala.util.{Either, Left, Right} 7 | 8 | trait EitherValues { 9 | import scala.language.implicitConversions 10 | 11 | /** 12 | * Implicit conversion that adds a `value` method to `scala.util.Either` 13 | * 14 | * @param either 15 | * the `scala.util.Either` on which to add the `value` method 16 | */ 17 | implicit def convertEitherToEitherable[E, T](either: E Either T)(implicit pos: source.Position): Eitherable[E, T] = 18 | new Eitherable(either, pos) 19 | 20 | /** 21 | * Container class for matching success type stuff in `scala.util.Either` containers, similar to 22 | * `org.scalatest.OptionValues.Valuable` 23 | * 24 | * Meant to allow you to make statements like: 25 | * 26 | *
result.value should be > 1527 | * 28 | * Where it only matches if result is `Valid` and is also greater than 15. 29 | * 30 | * Otherwise your test will fail, indicating that it was left instead of right 31 | * 32 | * @param either 33 | * A `scala.util.Either` object to try converting to a `Eitherable` 34 | * 35 | * @see 36 | * org.scalatest.OptionValues.Valuable 37 | */ 38 | final class Eitherable[E, T](either: E Either T, pos: source.Position) { 39 | 40 | /** 41 | * Extract the `Right` from the Either. If the value is not a right the test will fail. 42 | */ 43 | def value: T = 44 | either match { 45 | case Right(right) => right 46 | case Left(left) => 47 | throw new TestFailedException( 48 | (_: StackDepthException) => Some(s"'$left' is a Left, expected a Right."), 49 | None, 50 | pos 51 | ) 52 | } 53 | 54 | /** 55 | * Use .leftValue on an Either to extract the left side. Like .value, but for the left. If the value is a right, the 56 | * test will fail. 57 | */ 58 | def leftValue: E = 59 | either match { 60 | case Right(right) => 61 | throw new TestFailedException( 62 | (_: StackDepthException) => Some(s"'$right' is a Right, expected a Left."), 63 | None, 64 | pos 65 | ) 66 | case Left(left) => left 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Companion object for easy importing – rather than mixing in – to allow `EitherValues` operations. 73 | * 74 | * This will permit you to invoke a `value` method on an instance of a `scala.util.Either`, which attempts to unwrap the 75 | * `Valid`. 76 | * 77 | * Similar to `org.scalatest.OptionValues.Valuable` 78 | * 79 | * Meant to allow you to make statements like: 80 | * 81 | *
result.value should be > 1582 | * 83 | * Where it only matches if result is both an `Either.Vaild` and has a value > 15. 84 | * 85 | * Otherwise your test will fail, indicating that it was an Invalid instead of Valid 86 | * 87 | * @see 88 | * EitherValues.EitherValuable 89 | */ 90 | object EitherValues extends EitherValues 91 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devshell": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "typelevel-nix", 7 | "nixpkgs" 8 | ] 9 | }, 10 | "locked": { 11 | "lastModified": 1741473158, 12 | "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", 13 | "owner": "numtide", 14 | "repo": "devshell", 15 | "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "numtide", 20 | "repo": "devshell", 21 | "type": "github" 22 | } 23 | }, 24 | "flake-utils": { 25 | "inputs": { 26 | "systems": "systems" 27 | }, 28 | "locked": { 29 | "lastModified": 1731533236, 30 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 31 | "owner": "numtide", 32 | "repo": "flake-utils", 33 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 34 | "type": "github" 35 | }, 36 | "original": { 37 | "owner": "numtide", 38 | "repo": "flake-utils", 39 | "type": "github" 40 | } 41 | }, 42 | "nixpkgs": { 43 | "locked": { 44 | "lastModified": 1750386251, 45 | "narHash": "sha256-1ovgdmuDYVo5OUC5NzdF+V4zx2uT8RtsgZahxidBTyw=", 46 | "owner": "nixos", 47 | "repo": "nixpkgs", 48 | "rev": "076e8c6678d8c54204abcb4b1b14c366835a58bb", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nixos", 53 | "ref": "nixpkgs-unstable", 54 | "repo": "nixpkgs", 55 | "type": "github" 56 | } 57 | }, 58 | "root": { 59 | "inputs": { 60 | "flake-utils": [ 61 | "typelevel-nix", 62 | "flake-utils" 63 | ], 64 | "nixpkgs": [ 65 | "typelevel-nix", 66 | "nixpkgs" 67 | ], 68 | "typelevel-nix": "typelevel-nix" 69 | } 70 | }, 71 | "systems": { 72 | "locked": { 73 | "lastModified": 1681028828, 74 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 75 | "owner": "nix-systems", 76 | "repo": "default", 77 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 78 | "type": "github" 79 | }, 80 | "original": { 81 | "owner": "nix-systems", 82 | "repo": "default", 83 | "type": "github" 84 | } 85 | }, 86 | "typelevel-nix": { 87 | "inputs": { 88 | "devshell": "devshell", 89 | "flake-utils": "flake-utils", 90 | "nixpkgs": "nixpkgs" 91 | }, 92 | "locked": { 93 | "lastModified": 1750691079, 94 | "narHash": "sha256-QwwgWVXXpeYqRr1vUEniiNR2/kTeGd1/pdEgY8r6eY8=", 95 | "owner": "typelevel", 96 | "repo": "typelevel-nix", 97 | "rev": "de2bec8af6abc879ce8544771248936d6e9f81b9", 98 | "type": "github" 99 | }, 100 | "original": { 101 | "owner": "typelevel", 102 | "repo": "typelevel-nix", 103 | "type": "github" 104 | } 105 | } 106 | }, 107 | "root": "root", 108 | "version": 7 109 | } 110 | -------------------------------------------------------------------------------- /shared/src/main/scala/cats/scalatest/ValidatedValues.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import org.scalatest.exceptions.{StackDepthException, TestFailedException} 4 | 5 | import cats.data.Validated 6 | import Validated.{Invalid, Valid} 7 | import org.scalactic.source 8 | 9 | trait ValidatedValues { 10 | import scala.language.implicitConversions 11 | 12 | /** 13 | * Implicit conversion that adds a `value` method to `cats.data.Validated` 14 | * 15 | * @param validated 16 | * the `cats.data.Validated` on which to add the `value` method 17 | */ 18 | implicit def convertValidatedToValidatable[E, T]( 19 | validated: Validated[E, T] 20 | )(implicit pos: source.Position): Validatable[E, T] = 21 | new Validatable(validated, pos) 22 | 23 | /** 24 | * Container class for matching success type stuff in `cats.data.Validated` containers, similar to 25 | * `org.scalatest.OptionValues.Valuable` 26 | * 27 | * Meant to allow you to make statements like: 28 | * 29 | *
result.value should be > 15 result.valid.value should be(Valid(15))30 | * 31 | * Where it only matches if result is `Valid(9)` 32 | * 33 | * Otherwise your test will fail, indicating that it was left instead of right 34 | * 35 | * @param validated 36 | * A `cats.data.Validated` object to try converting to a `Validatable` 37 | * 38 | * @see 39 | * org.scalatest.OptionValues.Valuable 40 | */ 41 | final class Validatable[E, T](validated: Validated[E, T], pos: source.Position) { 42 | def value: T = 43 | validated match { 44 | case Valid(valid) => valid 45 | case Invalid(left) => 46 | throw new TestFailedException( 47 | (_: StackDepthException) => Some(s"'$left' is Invalid, expected Valid."), 48 | None, 49 | pos 50 | ) 51 | } 52 | 53 | /** 54 | * Allow .invalidValue on an validated to extract the invalid side. Like .value, but for the `Invalid`. 55 | */ 56 | def invalidValue: E = 57 | validated match { 58 | case Valid(valid) => 59 | throw new TestFailedException( 60 | (_: StackDepthException) => Some(s"'$valid' is Valid, expected Invalid."), 61 | None, 62 | pos 63 | ) 64 | case Invalid(left) => left 65 | } 66 | 67 | /** 68 | * Returns the
Validated passed to the constructor as a Valid, if it is a
69 | * Valid, else throws TestFailedException with a detail message indicating the
70 | * Validated was not a Valid.
71 | */
72 | def valid: Valid[T] =
73 | validated match {
74 | case valid: Valid[T] => valid
75 | case _ =>
76 | throw new TestFailedException(
77 | (_: StackDepthException) => Some("The Validated on which valid was invoked was not a Valid."),
78 | None,
79 | pos
80 | )
81 | }
82 |
83 | /**
84 | * Returns the Validated passed to the constructor as an Invalid, if it is an
85 | * Invalid, else throws TestFailedException with a detail message indicating the
86 | * Validated was not an Invalid.
87 | */
88 | def invalid: Invalid[E] =
89 | validated match {
90 | case invalid: Invalid[E] => invalid
91 | case _ =>
92 | throw new TestFailedException(
93 | (_: StackDepthException) => Some("The Validated on which invalid was invoked was not an Invalid."),
94 | None,
95 | pos
96 | )
97 | }
98 | }
99 | }
100 |
101 | /**
102 | * Companion object for easy importing – rather than mixing in – to allow `ValidatedValues` operations.
103 | *
104 | * This will permit you to invoke a `value` method on an instance of a `cats.data.Validated`, which attempts to unwrap
105 | * the Validated.Valid
106 | *
107 | * Similar to `org.scalatest.OptionValues.Valuable`
108 | *
109 | * Meant to allow you to make statements like:
110 | *
111 | * result.value should be > 15112 | * 113 | * Where it only matches if result is both valid and greater than 15. 114 | * 115 | * Otherwise your test will fail, indicating that it was an Invalid instead of Valid 116 | * 117 | * @see 118 | * org.scalatest.OptionValues.Valuable 119 | */ 120 | object ValidatedValues extends ValidatedValues 121 | -------------------------------------------------------------------------------- /shared/src/main/scala/cats/scalatest/ValidatedMatchers.scala: -------------------------------------------------------------------------------- 1 | package cats.scalatest 2 | 3 | import cats.data.{NonEmptyList => NEL, Validated, ValidatedNel} 4 | import org.scalatest.matchers.{BeMatcher, MatchResult, Matcher} 5 | import shapeless3.typeable.Typeable 6 | import shapeless3.typeable.syntax.typeable.cast 7 | 8 | trait ValidatedMatchers { 9 | 10 | /** 11 | * Checks if a `cats.data.ValidatedNel` contains a specific failure element Usage: 12 | * {{{ 13 | * validationObj should haveInvalid (someErrorMessageOrObject) 14 | * }}} 15 | * Can also be used to test multiple elements: ` 16 | * {{{ 17 | * validationObj should (haveInvalid (someErrorMessageOrObject) and 18 | * haveInvalid (someOtherErrorMessageOrObject)) 19 | * }}} 20 | */ 21 | def haveInvalid[E](element: E): Matcher[ValidatedNel[E, ?]] = new HasCatsValidatedFailure[E](element) 22 | 23 | /** 24 | * Checks if a `cats.data.ValidatedNel` contains a failure element matching a specific type Usage: 25 | * {{{ 26 | * validationObj should haveAnInvalid[someErrorType] 27 | * }}} 28 | * Can also be used to test multiple elements: ` 29 | * {{{ 30 | * validationObj should (haveAnInvalid[someErrorType] and 31 | * haveAnInvalid[someOtherErrorType]) 32 | * }}} 33 | */ 34 | def haveAnInvalid[E: Typeable]: Matcher[ValidatedNel[Any, ?]] = new HasACatsValidatedFailure[E] 35 | 36 | /** 37 | * Checks if a `cats.data.Validated` is a specific `Invalid` element. 38 | */ 39 | def beInvalid[E](element: E): Matcher[Validated[E, ?]] = new BeCatsInvalidMatcher[E](element) 40 | 41 | /** 42 | * Checks if the `cats.data.Validated` is an `Invalid`. 43 | */ 44 | def invalid[E]: BeMatcher[Validated[E, ?]] = new IsCatsInvalidMatcher[E] 45 | 46 | /** 47 | * Checks if a `cats.data.Validated` is a `Valid`. 48 | */ 49 | def valid[T]: BeMatcher[Validated[?, T]] = new IsCatsValidMatcher[T] 50 | 51 | /** 52 | * Checks if a `cats.data.Validated` is an instance of `Valid`. 53 | */ 54 | def beValid[T](element: T): Matcher[Validated[?, T]] = new BeValidMatcher[T](element) 55 | } 56 | 57 | /** 58 | * Import singleton in case you prefer to import rather than mix in. 59 | * {{{ 60 | * import ValidatedMatchers._ 61 | * result should beValid (100) 62 | * }}} 63 | */ 64 | object ValidatedMatchers extends ValidatedMatchers 65 | 66 | //Classes used above 67 | final private[scalatest] class HasCatsValidatedFailure[E](element: E) extends Matcher[ValidatedNel[E, ?]] { 68 | def apply(validated: ValidatedNel[E, ?]): MatchResult = 69 | MatchResult( 70 | validated.fold(n => (n.head :: n.tail).contains(element), ? => false), 71 | s"'$validated' did not contain an Invalid element matching '$element'.", 72 | s"'$validated' contained an Invalid element matching '$element', but should not have." 73 | ) 74 | } 75 | 76 | final private[scalatest] class HasACatsValidatedFailure[+T: Typeable] extends Matcher[ValidatedNel[Any, ?]] { 77 | def apply(validated: ValidatedNel[Any, ?]): MatchResult = { 78 | val expected: String = Typeable[T].describe 79 | 80 | MatchResult( 81 | validated.fold( 82 | (n: NEL[_]) => 83 | (n.head :: n.tail).exists { e => 84 | e.cast[T].isDefined 85 | }, 86 | _ => false 87 | ), 88 | s"'$validated' did not contain an Invalid element matching '$expected'.", 89 | s"'$validated' contained an Invalid element matching '$expected' but " + 90 | s"should not have." 91 | ) 92 | } 93 | } 94 | 95 | final private[scalatest] class BeCatsInvalidMatcher[E](element: E) extends Matcher[Validated[E, ?]] { 96 | def apply(validated: Validated[E, ?]): MatchResult = 97 | MatchResult( 98 | validated.fold(_ == element, ? => false), 99 | s"'$validated' did not contain an Invalid element matching '$element'.", 100 | s"'$validated' contained an Invalid element matching '$element', but should not have." 101 | ) 102 | } 103 | 104 | final private[scalatest] class BeValidMatcher[T](element: T) extends Matcher[Validated[?, T]] { 105 | def apply(validated: Validated[?, T]): MatchResult = 106 | MatchResult( 107 | validated.fold(_ => false, _ == element), 108 | s"'$validated' did not contain a Valid element matching '$element'.", 109 | s"'$validated' contained a Valid element matching '$element', but should not have." 110 | ) 111 | } 112 | 113 | final private[scalatest] class IsCatsValidMatcher[T] extends BeMatcher[Validated[?, T]] { 114 | def apply(validated: Validated[?, T]): MatchResult = 115 | MatchResult( 116 | validated.isValid, 117 | s"'$validated' was not Valid, but should have been.", 118 | s"'$validated' was Valid, but should not have been." 119 | ) 120 | } 121 | 122 | final private[scalatest] class IsCatsInvalidMatcher[E] extends BeMatcher[Validated[E, ?]] { 123 | def apply(validated: Validated[E, ?]): MatchResult = 124 | MatchResult( 125 | validated.isInvalid, 126 | s"'$validated' was not an Invalid, but should have been.", 127 | s"'$validated' was an Invalid, but should not have been." 128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /scalastyle-config.xml: -------------------------------------------------------------------------------- 1 |