├── .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 > 15 
27 | * 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 > 15 
82 | * 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 > 15 
112 | * 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 | 2 | Scalastyle standard configuration 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cats-scalatest 2 | 3 | [![CI](https://github.com/IronCoreLabs/cats-scalatest/actions/workflows/ci.yml/badge.svg)](https://github.com/IronCoreLabs/cats-scalatest/actions/workflows/ci.yml) 4 | [![codecov.io](https://codecov.io/github/IronCoreLabs/cats-scalatest/coverage.svg?branch=main)](https://codecov.io/github/IronCoreLabs/cats-scalatest?branch=main) 5 | [![scaladoc](https://javadoc-badge.appspot.com/com.ironcorelabs/cats-scalatest_2.11.svg?label=scaladoc)](https://javadoc-badge.appspot.com/com.ironcorelabs/cats-scalatest_2.11) 6 | 7 | Scalatest bindings for cats. Inspired by scalaz-scalatest. 8 | 9 | Apache 2.0 licensed. 10 | 11 | **cats-scalatest** is a [Typelevel](http://typelevel.org/) project. This means we embrace pure, typeful, functional programming, and provide a safe and friendly environment for teaching, learning, and contributing as described in the [Scala Code of Conduct](http://typelevel.org/code-of-conduct.html). 12 | 13 | ## Setup 14 | 15 | We currently crossbuild for Scala 2.12 and 2.13. Prior to 3.0.4 we cross built for 2.11 as well. 16 | 17 | Because cats is such a young project the versioning is not quite following the semantic versioning guidelines (yet). Use the below table to understand 18 | which version of the cats-scalatest library you need. 19 | 20 | | Cats-Scalatest Version | Cats Version | Scalatest Version | 21 | | ---------------------- | ------------ | ----------------- | 22 | | 1.0.1 | 0.2.0 | 2.2.4 | 23 | | 1.1.0 | 0.4.0 | 2.2.4 | 24 | | 1.1.2 | 0.4.1 | 2.2.4 | 25 | | 1.3.0 | 0.6.{0,1} | 2.2.6 | 26 | | 1.4.0 | 0.7.0 | 2.2.6 | 27 | | 1.5.0 | 0.7.2 | 2.2.6 | 28 | | 2.0.0 | 0.7.2 | 3.0.0 | 29 | | 2.1.0 | 0.8.0 | 3.0.0 | 30 | | 2.1.1 | 0.8.1 | 3.0.0 | 31 | | 2.2.0 | 0.9.0 | 3.0.0 | 32 | | 2.3.0 | 1.0.0-MF | 3.0.0 | 33 | | 2.3.1 | 1.0.1 | 3.0.0 | 34 | | 2.4.0 | 1.5.0 | 3.0.5 | 35 | | 3.0.0 | 2.0.0 | 3.0.8 | 36 | | 3.0.4 | 2.0.0 | 3.1.0 | 37 | | 3.0.5 | 2.1.0 | 3.1.0 | 38 | | 3.1.1 | 2.1.1 | 3.2.3 | 39 | 40 | To include this in your project, add the dependency: 41 | 42 | ``` 43 | //For cats 2.1.0 and scalatest 3.1, see above chart for others. 44 | libraryDependencies += "com.ironcorelabs" %% "cats-scalatest" % "3.0.5" % "test" 45 | ``` 46 | 47 | ## What does this provide? 48 | 49 | Matchers & Helpers are presently offered for testing of the following cats concepts: 50 | 51 | - `Either` 52 | - `Validated` 53 | 54 | ## Usage 55 | 56 | There are two ways to use the provided matchers: 57 | 58 | You can mix them in: 59 | 60 | ```scala 61 | class MySillyWalkSpec extends FlatSpec with Matchers with EitherMatchers { 62 | // ... 63 | } 64 | ``` 65 | 66 | This makes the matchers in `EitherMatchers` available inside the scope of your test. 67 | 68 | You can also import explicitly from a provided object: 69 | 70 | ```scala 71 | import cats.scalatest.EitherMatchers 72 | 73 | class MySillyWalkSpec extends FlatSpec with Matchers { 74 | import EitherMatchers._ 75 | // ... 76 | } 77 | 78 | ``` 79 | 80 | Also brings the matchers into scope. 81 | 82 | And now, the matchers themselves. 83 | 84 | ## Either Matchers 85 | 86 | EitherMatchers supplies the following methods: 87 | 88 | ``` 89 | beLeft[E](element: E) 90 | left[E] 91 | beRight[T](element: T) 92 | right[T] 93 | ``` 94 | 95 | ### Specific Element Matchers 96 | 97 | The matchers that begin with a be prefix are for matching a specific element inside of the `Either`. 98 | 99 | Something like the following: 100 | 101 | ``` 102 | val s = "Hello World" 103 | val valueInRight = Right(s) 104 | 105 | //This passes 106 | valueInRight should beRight(s) 107 | 108 | //This fails with the following message: 109 | //Right(Hello World) did not contain an Right element matching 'goodbye'. 110 | valueInRight should beRight("goodbye") 111 | ``` 112 | 113 | The matchers work the same for `beLeft`. 114 | 115 | ### Right and Left Matchers 116 | 117 | The `left` and `right` matchers are for checking to see if the `Either` is a right or left without caring what's inside. 118 | 119 | ``` 120 | //This passes 121 | Left("uh oh") should be(left) 122 | 123 | //This fails with the following message: 124 | //Left(uh oh) was not an Right, but should have been. 125 | Left("uh oh") should be(right) 126 | ``` 127 | 128 | ## Validated Matchers 129 | 130 | cats.data.Validated also has matchers similar to the ones described above. 131 | 132 | ``` 133 | def beInvalid[E](element: E) 134 | def invalid[E] 135 | def valid[T] 136 | def beValid[T](element: T) 137 | ``` 138 | 139 | I won't repeat how they're used here. `Validated` does have some additional 140 | matchers though which allows you to describe values that are in the `Invalid` if 141 | you're using `ValidatedNel`. 142 | 143 | The first matcher is `haveInvalid` and can be used like this: 144 | 145 | ``` 146 | val validatedNelValue: ValidatedNel[String, Int] = Invalid(NonEmptyList("error1", "error2")) 147 | 148 | //The following works fine: 149 | validatedNelValue should haveInvalid("error1") 150 | 151 | //But you can also combine them with the and word to match multiple values: 152 | validateNelValue should (haveInvalid("error1") and haveInvalid("error2")) 153 | ``` 154 | 155 | The second matcher is `haveAnInvalid` and can be used like this: 156 | 157 | ``` 158 | val validatedNelValue: ValidatedNel[Exception, Int] = Invalid(NonEmptyList(new ArrayIndexOutOfBoundsException, new NoSuchElementException)) 159 | 160 | //The following works fine: 161 | validatedNelValue should haveAnInvalid[NoSuchElementException] 162 | validatedNelValue shouldNot haveAnInvalid[NumberFormatException] 163 | 164 | //But you can also combine them with the and word to match multiple values: 165 | validateNelValue should (haveAnInvalid[ArrayIndexOutOfBoundsException] and haveAnInvalid[NoSuchElementException]) 166 | ``` 167 | 168 | ## Values Helpers 169 | 170 | A very common test idiom is to want to assert the Either is a Left or a Right and then extract the value. For this 171 | we supply `EitherValues`. This can be mixed into your test or imported as an object just like the matchers above, but 172 | instead of providing Matchers it instead adds `value` and `leftValue` as syntax to the `Either` type. 173 | 174 | ``` 175 | val x = Right("hello") 176 | //Passes! 177 | x.value shouldBe "hello" 178 | 179 | //Fails with the following message: 180 | // 'Hello' is Right, expected Left. 181 | x.leftValue shouldBe "hello" 182 | ``` 183 | 184 | The same is true for the `Validated`. If you import or mixin `ValidatedValues` you'll be able to call `.value` to extract 185 | `Valid` and `.invalidValue` to extract the `Invalid` side. 186 | 187 | ## Documentation and Support 188 | 189 | - See the [scaladoc](https://javadoc-badge.appspot.com/com.ironcorelabs/cats-scalatest_2.11). 190 | - The [tests](https://github.com/IronCoreLabs/cats-scalatest/tree/main/src/test/scala/cats/scalatest) show usage. 191 | - Yell at [@IronCoreLabs](https://twitter.com/ironcorelabs) or [@coltfred](https://twitter.com/coltfred) on twitter. 192 | - Drop by the cats [gitter](https://gitter.im/non/cats). 193 | 194 | ## Contributors 195 | 196 | - [Colt Frederickson](http://github.com/coltfred) [coltfred] 197 | 198 | Idea ported from [scalaz-scalatest](https://github.com/typelevel/scalaz-scalatest), which is 199 | primarily written by [Brendan McAdams](https://github.com/bwmcadams). 200 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 IronCoreLabs, & Contributors 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright (c) 2015 IronCoreLabs 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | --------------------------------------------------------------------------------