Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1744 added implicit class for EitherValues #1746

Open
wants to merge 1 commit into
base: 3.1.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions project/scalatest.scala
Expand Up @@ -414,6 +414,15 @@ object ScalatestBuild extends BuildCommons with DottyBuild with NativeBuild with
|import matchers.should.Matchers._""".stripMargin,
libraryDependencies ++= scalaXmlDependency(scalaVersion.value),
libraryDependencies ++= scalatestLibraryDependencies,
Compile / unmanagedSourceDirectories ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, scalaMajor)) if scalaMajor < 13 =>
Seq(sourceDirectory.value / "main" / "scala-pre-2.13")

case _ =>
Seq.empty
}
},
genMustMatchersTask,
genGenTask,
genTablesTask,
Expand Down
44 changes: 44 additions & 0 deletions scalatest-test/src/test/scala/org/scalatest/EitherValuesSpec.scala
Expand Up @@ -30,6 +30,12 @@ class EitherValuesSpec extends FunSpec {
e.left.value should startWith ("hi")
}

it("should return the left value inside an either if leftValue is defined") {
val e: Either[String, String] = Left("hi there")
e.leftValue should === ("hi there")
e.leftValue should startWith ("hi")
}

it("should throw TestFailedException if left.value is empty") {
val e: Either[String, String] = Right("hi there")
val caught =
Expand All @@ -40,12 +46,29 @@ class EitherValuesSpec extends FunSpec {
caught.failedCodeFileName.value should be ("EitherValuesSpec.scala")
caught.message.value should be (Resources.eitherLeftValueNotDefined)
}

it("should throw TestFailedException if leftValue is empty") {
val e: Either[String, String] = Right("hi there")
val caught =
the [TestFailedException] thrownBy {
e.leftValue should startWith ("hi")
}
caught.failedCodeLineNumber.value should equal (thisLineNumber - 2)
caught.failedCodeFileName.value should be ("EitherValuesSpec.scala")
caught.message.value should be (Resources.eitherLeftValueNotDefined)
}

it("should return the right value inside an either if right.value is defined") {
val e: Either[String, String] = Right("hi there")
e.right.value should === ("hi there")
e.right.value should startWith ("hi")
}

it("should return the right value inside an either if rightValue is defined") {
val e: Either[String, String] = Right("hi there")
e.rightValue should === ("hi there")
e.rightValue should startWith ("hi")
}

it("should throw TestFailedException if right.value is empty") {
val e: Either[String, String] = Left("hi there")
Expand All @@ -58,14 +81,35 @@ class EitherValuesSpec extends FunSpec {
caught.message.value should be (Resources.eitherRightValueNotDefined)
}

it("should throw TestFailedException if rightValue is empty") {
val e: Either[String, String] = Left("hi there")
val caught =
the [TestFailedException] thrownBy {
e.rightValue should startWith ("hi")
}
caught.failedCodeLineNumber.value should equal (thisLineNumber - 2)
caught.failedCodeFileName.value should be ("EitherValuesSpec.scala")
caught.message.value should be (Resources.eitherRightValueNotDefined)
}

it("should allow an immediate application of parens to invoke apply on the type contained in the Left") {
val lefty: Either[Map[String, Int], String] = Left(Map("I" -> 1, "II" -> 2))
lefty.left.value("II") shouldBe 2
}

it("should allow an immediate application of parens to invoke apply on leftValue on the type contained in the Left") {
val lefty: Either[Map[String, Int], String] = Left(Map("I" -> 1, "II" -> 2))
lefty.leftValue("II") shouldBe 2
}

it("should allow an immediate application of parens to invoke apply on the type contained in the Right") {
val righty: Either[String, Map[String, Int]] = Right(Map("I" -> 1, "II" -> 2))
righty.right.value("II") shouldBe 2
}

it("should allow an immediate application of parens to invoke apply on rightValue on the type contained in the Right") {
val righty: Either[String, Map[String, Int]] = Right(Map("I" -> 1, "II" -> 2))
righty.rightValue("II") shouldBe 2
}
}
}
210 changes: 210 additions & 0 deletions scalatest/src/main/scala-2.13/org/scalatest/EitherValues.scala
@@ -0,0 +1,210 @@
/*
* Copyright 2001-2013 Artima, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.scalatest

import org.scalactic.source
import org.scalatest.exceptions.StackDepthException
import org.scalatest.exceptions.TestFailedException

/**
* Trait that provides an implicit conversion that adds <code>left.value</code> and <code>right.value</code> methods
* to <code>Either</code>, which will return the selected value of the <code>Either</code> if defined,
* or throw <code>TestFailedException</code> if not.
*
* <p>
* This construct allows you to express in one statement that an <code>Either</code> should be <em>left</em> or <em>right</em>
* and that its value should meet some expectation. Here's are some examples:
* </p>
*
* <pre class="stHighlight">
* either1.right.value should be &gt; 9
* either2.left.value should be ("Muchas problemas")
* </pre>
*
* <p>
* Or, using assertions instead of matcher expressions:
* </p>
*
* <pre class="stHighlight">
* assert(either1.right.value &gt; 9)
* assert(either2.left.value === "Muchas problemas")
* </pre>
*
* <p>
* Were you to simply invoke <code>right.get</code> or <code>left.get</code> on the <code>Either</code>,
* if the <code>Either</code> wasn't defined as expected (<em>e.g.</em>, it was a <code>Left</code> when you expected a <code>Right</code>), it
* would throw a <code>NoSuchElementException</code>:
* </p>
*
* <pre class="stHighlight">
* val either: Either[String, Int] = Left("Muchas problemas")
*
* either.right.get should be &gt; 9 // either.right.get throws NoSuchElementException
* </pre>
*
* <p>
* The <code>NoSuchElementException</code> would cause the test to fail, but without providing a <a href="exceptions/StackDepth.html">stack depth</a> pointing
* to the failing line of test code. This stack depth, provided by <a href="exceptions/TestFailedException.html"><code>TestFailedException</code></a> (and a
* few other ScalaTest exceptions), makes it quicker for
* users to navigate to the cause of the failure. Without <code>EitherValues</code>, to get
* a stack depth exception you would need to make two statements, like this:
* </p>
*
* <pre class="stHighlight">
* val either: Either[String, Int] = Left("Muchas problemas")
*
* either should be ('right) // throws TestFailedException
* either.right.get should be &gt; 9
* </pre>
*
* <p>
* The <code>EitherValues</code> trait allows you to state that more concisely:
* </p>
*
* <pre class="stHighlight">
* val either: Either[String, Int] = Left("Muchas problemas")
*
* either.right.value should be &gt; 9 // either.right.value throws TestFailedException
* </pre>
*/
trait EitherValues {

import scala.language.implicitConversions

/**
* Implicit conversion that adds a <code>value</code> method to <code>LeftProjection</code>.
*
* @param either the <code>LeftProjection</code> on which to add the <code>value</code> method
*/
implicit def convertLeftProjectionToValuable[L, R](leftProj: Either.LeftProjection[L, R])(implicit pos: source.Position): LeftValuable[L, R] = new LeftValuable(leftProj, pos)

/**
* Implicit conversion that adds a <code>value</code> method to <code>RightProjection</code>.
*
* @param either the <code>RightProjection</code> on which to add the <code>value</code> method
*/
implicit def convertRightProjectionToValuable[L, R](rightProj: Either.RightProjection[L, R])(implicit pos: source.Position): RightValuable[L, R] = new RightValuable(rightProj, pos)

/**
* Implicit class that adds the <code>rightValue</code> and <code>leftValue</code> methods to <code>Either</code>.
* @param either the <code>Either</code> on which to add the methods
*/
implicit class EitherValuable[L, R](either: Either[L, R])(implicit pos: source.Position) {
/**
* Returns the <code>Left</code> value contained in the wrapped <code>LeftProjection</code>, if defined as a <code>Left</code>, else throws <code>TestFailedException</code> with
* a detail message indicating the <code>Either</code> was defined as a <code>Right</code>, not a <code>Left</code>.
*/
def leftValue: L =
either.swap.getOrElse(throw new TestFailedException((_: StackDepthException) => Some(Resources.eitherLeftValueNotDefined), None, pos))

/**
* Returns the <code>Right</code> value contained in the wrapped <code>Either</code>, if defined as a <code>Right</code>, else throws <code>TestFailedException</code> with
* a detail message indicating the <code>Either</code> was defined as a <code>Left</code>, not a <code>Right</code>.
*/
def rightValue: R =
either.getOrElse(throw new TestFailedException((_: StackDepthException) => Some(Resources.eitherRightValueNotDefined), None, pos))
}

/**
* Wrapper class that adds a <code>value</code> method to <code>LeftProjection</code>, allowing
* you to make statements like:
*
* <pre class="stHighlight">
* either.left.value should be &gt; 9
* </pre>
*
* @param leftProj A <code>LeftProjection</code> to convert to <code>LeftValuable</code>, which provides the
* <code>value</code> method.
*/
class LeftValuable[L, R](leftProj: Either.LeftProjection[L, R], pos: source.Position) {

/**
* Returns the <code>Left</code> value contained in the wrapped <code>LeftProjection</code>, if defined as a <code>Left</code>, else throws <code>TestFailedException</code> with
* a detail message indicating the <code>Either</code> was defined as a <code>Right</code>, not a <code>Left</code>.
*/
def value: L = {
try {
leftProj.get
}
catch {
case cause: NoSuchElementException =>
throw new TestFailedException((_: StackDepthException) => Some(Resources.eitherLeftValueNotDefined), Some(cause), pos)
}
}
}

/**
* Wrapper class that adds a <code>value</code> method to <code>RightProjection</code>, allowing
* you to make statements like:
*
* <pre class="stHighlight">
* either.right.value should be &gt; 9
* </pre>
*
* @param rightProj A <code>RightProjection</code> to convert to <code>RightValuable</code>, which provides the
* <code>value</code> method.
*/
class RightValuable[L, R](rightProj: Either.RightProjection[L, R], pos: source.Position) {

/**
* Returns the <code>Right</code> value contained in the wrapped <code>RightProjection</code>, if defined as a <code>Right</code>, else throws <code>TestFailedException</code> with
* a detail message indicating the <code>Either</code> was defined as a <code>Right</code>, not a <code>Left</code>.
*/
def value: R = {
try {
rightProj.get
}
catch {
case cause: NoSuchElementException =>
throw new TestFailedException((_: StackDepthException) => Some(Resources.eitherRightValueNotDefined), Some(cause), pos)
}
}
}
}

/**
* Companion object that facilitates the importing of <code>ValueEither</code> members as
* an alternative to mixing it in. One use case is to import <code>EitherValues</code>'s members so you can use
* <code>left.value</code> and <code>right.value</code> on <code>Either</code> in the Scala interpreter:
*
* <pre class="stREPL">
* $ scala -cp scalatest-1.7.jar
* Welcome to Scala version 2.9.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_29).
* Type in expressions to have them evaluated.
* Type :help for more information.
*
* scala&gt; import org.scalatest._
* import org.scalatest._
*
* scala&gt; import matchers.Matchers._
* import matchers.Matchers._
*
* scala&gt; import EitherValues._
* import EitherValues._
*
* scala&gt; val e: Either[String, Int] = Left("Muchas problemas")
* e: Either[String,Int] = Left(Muchas problemas)
*
* scala&gt; e.left.value should be ("Muchas problemas")
*
* scala&gt; e.right.value should be &lt; 9
* org.scalatest.TestFailedException: The Either on which rightValue was invoked was not defined.
* at org.scalatest.EitherValues$RightValuable.value(EitherValues.scala:148)
* at .&lt;init&gt;(&lt;console&gt;:18)
* ...
* </pre>
*/
object EitherValues extends EitherValues
Expand Up @@ -98,6 +98,26 @@ trait EitherValues {
*/
implicit def convertRightProjectionToValuable[L, R](rightProj: Either.RightProjection[L, R])(implicit pos: source.Position): RightValuable[L, R] = new RightValuable(rightProj, pos)

/**
* Implicit class that adds the <code>rightValue</code> and <code>leftValue</code> methods to <code>Either</code>.
* @param either the <code>Either</code> on which to add the methods
*/
implicit class EitherValuable[L, R](either: Either[L, R])(implicit pos: source.Position) {
/**
* Returns the <code>Left</code> value contained in the wrapped <code>LeftProjection</code>, if defined as a <code>Left</code>, else throws <code>TestFailedException</code> with
* a detail message indicating the <code>Either</code> was defined as a <code>Right</code>, not a <code>Left</code>.
*/
def leftValue: L =
either.left.getOrElse(throw new TestFailedException((_: StackDepthException) => Some(Resources.eitherLeftValueNotDefined), None, pos))

/**
* Returns the <code>Right</code> value contained in the wrapped <code>Either</code>, if defined as a <code>Right</code>, else throws <code>TestFailedException</code> with
* a detail message indicating the <code>Either</code> was defined as a <code>Left</code>, not a <code>Right</code>.
*/
def rightValue: R =
either.right.getOrElse(throw new TestFailedException((_: StackDepthException) => Some(Resources.eitherRightValueNotDefined), None, pos))
}

/**
* Wrapper class that adds a <code>value</code> method to <code>LeftProjection</code>, allowing
* you to make statements like:
Expand Down