From bfe345c509f2d7ab62025446e21784ad5de11de0 Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sat, 4 Nov 2023 18:12:49 +0100 Subject: [PATCH 1/3] Add Codec.emap --- core/src/main/scala/sttp/tapir/Codec.scala | 16 ++++++++++++++++ core/src/test/scala/sttp/tapir/CodecTest.scala | 14 ++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/sttp/tapir/Codec.scala b/core/src/main/scala/sttp/tapir/Codec.scala index 4926a163a3..c3f8e896b8 100644 --- a/core/src/main/scala/sttp/tapir/Codec.scala +++ b/core/src/main/scala/sttp/tapir/Codec.scala @@ -87,6 +87,22 @@ trait Codec[L, H, +CF <: CodecFormat] { outer => def mapDecode[HH](f: H => DecodeResult[HH])(g: HH => H): Codec[L, HH, CF] = map(Mapping.fromDecode(f)(g)) def map[HH](f: H => HH)(g: HH => H): Codec[L, HH, CF] = mapDecode(f.andThen(Value(_)))(g) + /** Maps this codec to the given higher-level type `HH`. + * + * @param f + * decoding function + * @param g + * encoding function + * @tparam HH + * target type + * @see + * [[map]] + * @see + * [[mapDecode]] + */ + def emap[HH](f: H => Either[String, HH])(g: HH => H): Codec[L, HH, CF] = + mapDecode(s => DecodeResult.fromEitherString(s.toString, f(s)))(g) + /** Adds the given validator to the codec's schema, and maps this codec to the given higher-level type `HH`. * * Unlike a `.validate(v).map(f)(g)` invocation, during decoding the validator is run before applying the `f` function. If there are diff --git a/core/src/test/scala/sttp/tapir/CodecTest.scala b/core/src/test/scala/sttp/tapir/CodecTest.scala index cbe8edbbae..9413606ce5 100644 --- a/core/src/test/scala/sttp/tapir/CodecTest.scala +++ b/core/src/test/scala/sttp/tapir/CodecTest.scala @@ -1,7 +1,7 @@ package sttp.tapir import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.Assertion +import org.scalatest.{Assertion, Inside} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.Checkers @@ -17,7 +17,7 @@ import java.util.{Date, UUID} import scala.reflect.ClassTag // see also CodecTestDateTime and CodecDelimitedTest -class CodecTest extends AnyFlatSpec with Matchers with Checkers { +class CodecTest extends AnyFlatSpec with Matchers with Checkers with Inside { private implicit val arbitraryUri: Arbitrary[Uri] = Arbitrary(for { scheme <- Gen.alphaLowerStr if scheme.nonEmpty @@ -88,6 +88,16 @@ class CodecTest extends AnyFlatSpec with Matchers with Checkers { codec.schema.validator should not be (Validator.pass) } + it should "provide an emap function" in { + val codec: Codec[String, Int, TextPlain] = Codec.string.emap(_.toIntOption.toRight("Not an int"))(_.toString) + codec.encode(10) should be("10") + codec.decode("10") should be(DecodeResult.Value(10)) + inside(codec.decode("foo")) { case e: DecodeResult.Error => + e.original should be("foo") + e.error.getMessage should be("Not an int") + } + } + case class Member(age: Int) { require(age >= 18) } From 762d10adc36ab471ad68f351a2339664e203c5f2 Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sun, 5 Nov 2023 10:21:30 +0100 Subject: [PATCH 2/3] Adapt test for scala 2.12 --- core/src/test/scala/sttp/tapir/CodecTest.scala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/src/test/scala/sttp/tapir/CodecTest.scala b/core/src/test/scala/sttp/tapir/CodecTest.scala index 9413606ce5..17f0731186 100644 --- a/core/src/test/scala/sttp/tapir/CodecTest.scala +++ b/core/src/test/scala/sttp/tapir/CodecTest.scala @@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets import java.time._ import java.util.{Date, UUID} import scala.reflect.ClassTag +import scala.util.Try // see also CodecTestDateTime and CodecDelimitedTest class CodecTest extends AnyFlatSpec with Matchers with Checkers with Inside { @@ -89,12 +90,12 @@ class CodecTest extends AnyFlatSpec with Matchers with Checkers with Inside { } it should "provide an emap function" in { - val codec: Codec[String, Int, TextPlain] = Codec.string.emap(_.toIntOption.toRight("Not an int"))(_.toString) + val codec: Codec[String, Int, TextPlain] = Codec.string.emap(s => Try(s.toInt).toEither.left.map(_.getMessage))(_.toString) codec.encode(10) should be("10") codec.decode("10") should be(DecodeResult.Value(10)) - inside(codec.decode("foo")) { case e: DecodeResult.Error => - e.original should be("foo") - e.error.getMessage should be("Not an int") + inside(codec.decode("foo")) { case DecodeResult.Error(original, error) => + original should be("foo") + error.getMessage should be("""For input string: "foo"""") } } From f4b4d6bd37e8811d8285fdf0fa7dfdd9921289b1 Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Mon, 6 Nov 2023 08:17:32 +0100 Subject: [PATCH 3/3] emap => mapEither --- core/src/main/scala/sttp/tapir/Codec.scala | 4 +++- core/src/test/scala/sttp/tapir/CodecTest.scala | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/main/scala/sttp/tapir/Codec.scala b/core/src/main/scala/sttp/tapir/Codec.scala index c3f8e896b8..929d33c940 100644 --- a/core/src/main/scala/sttp/tapir/Codec.scala +++ b/core/src/main/scala/sttp/tapir/Codec.scala @@ -99,8 +99,10 @@ trait Codec[L, H, +CF <: CodecFormat] { outer => * [[map]] * @see * [[mapDecode]] + * @see + * [[mapValidate]] */ - def emap[HH](f: H => Either[String, HH])(g: HH => H): Codec[L, HH, CF] = + def mapEither[HH](f: H => Either[String, HH])(g: HH => H): Codec[L, HH, CF] = mapDecode(s => DecodeResult.fromEitherString(s.toString, f(s)))(g) /** Adds the given validator to the codec's schema, and maps this codec to the given higher-level type `HH`. diff --git a/core/src/test/scala/sttp/tapir/CodecTest.scala b/core/src/test/scala/sttp/tapir/CodecTest.scala index 17f0731186..b5f10ef049 100644 --- a/core/src/test/scala/sttp/tapir/CodecTest.scala +++ b/core/src/test/scala/sttp/tapir/CodecTest.scala @@ -89,8 +89,8 @@ class CodecTest extends AnyFlatSpec with Matchers with Checkers with Inside { codec.schema.validator should not be (Validator.pass) } - it should "provide an emap function" in { - val codec: Codec[String, Int, TextPlain] = Codec.string.emap(s => Try(s.toInt).toEither.left.map(_.getMessage))(_.toString) + it should "provide a mapEither function" in { + val codec: Codec[String, Int, TextPlain] = Codec.string.mapEither(s => Try(s.toInt).toEither.left.map(_.getMessage))(_.toString) codec.encode(10) should be("10") codec.decode("10") should be(DecodeResult.Value(10)) inside(codec.decode("foo")) { case DecodeResult.Error(original, error) =>