diff --git a/integrations/cats/src/main/scala/sttp/tapir/integ/cats/TapirCodecCats.scala b/integrations/cats/src/main/scala/sttp/tapir/integ/cats/TapirCodecCats.scala index 4d93ddc992..4e81843605 100644 --- a/integrations/cats/src/main/scala/sttp/tapir/integ/cats/TapirCodecCats.scala +++ b/integrations/cats/src/main/scala/sttp/tapir/integ/cats/TapirCodecCats.scala @@ -1,6 +1,6 @@ package sttp.tapir.integ.cats -import cats.data.{Chain, NonEmptyChain, NonEmptyList, NonEmptySet} +import cats.data.{Chain, NonEmptyChain, NonEmptyList, NonEmptySet, NonEmptyVector} import sttp.tapir._ import sttp.tapir.integ.cats.ValidatorCats.nonEmptyFoldable import sttp.tapir.Validator.nonEmpty @@ -13,6 +13,10 @@ trait TapirCodecCats { Schema[NonEmptyList[T]](SchemaType.SArray(implicitly[Schema[T]])(_.toList)) .validate(nonEmptyFoldable) + implicit def schemaForNev[T: Schema]: Schema[NonEmptyVector[T]] = + Schema[NonEmptyVector[T]](SchemaType.SArray(implicitly[Schema[T]])(_.toVector)) + .validate(ValidatorCats.nonEmptyFoldable) + implicit def schemaForChain[T: Schema]: Schema[Chain[T]] = implicitly[Schema[List[T]]].map(l => Option(Chain.fromSeq(l)))(_.toList) @@ -29,6 +33,11 @@ trait TapirCodecCats { .validate(nonEmpty) .mapDecode { l => DecodeResult.fromOption(NonEmptyList.fromList(l)) }(_.toList) + implicit def codecForNonEmptyVector[L, H, CF <: CodecFormat](implicit c: Codec[L, Vector[H], CF]): Codec[L, NonEmptyVector[H], CF] = + c.schema(_.copy(isOptional = false)) + .validate(Validator.nonEmpty) + .mapDecode { v => DecodeResult.fromOption(NonEmptyVector.fromVector(v)) }(_.toVector) + implicit def codecForChain[L, H, CF <: CodecFormat](implicit c: Codec[L, List[H], CF]): Codec[L, Chain[H], CF] = c.map(Chain.fromSeq(_))(_.toList) diff --git a/integrations/cats/src/test/scala/sttp/tapir/integ/cats/TapirCodecCatsTest.scala b/integrations/cats/src/test/scala/sttp/tapir/integ/cats/TapirCodecCatsTest.scala index 4387aed653..3b60e9780c 100644 --- a/integrations/cats/src/test/scala/sttp/tapir/integ/cats/TapirCodecCatsTest.scala +++ b/integrations/cats/src/test/scala/sttp/tapir/integ/cats/TapirCodecCatsTest.scala @@ -1,6 +1,6 @@ package sttp.tapir.integ.cats -import cats.data.{NonEmptyChain, NonEmptyList, NonEmptySet} +import cats.data.{NonEmptyChain, NonEmptyList, NonEmptySet, NonEmptyVector} import org.scalacheck.{Arbitrary, Gen} import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers @@ -19,6 +19,9 @@ class TapirCodecCatsTest extends AnyFlatSpec with Matchers with Checkers { implicitly[Schema[NonEmptyList[String]]].schemaType shouldBe SArray[NonEmptyList[String], String](Schema(SString()))(_.toList) implicitly[Schema[NonEmptyList[String]]].isOptional shouldBe false + implicitly[Schema[NonEmptyVector[String]]].schemaType shouldBe SArray[NonEmptyVector[String], String](Schema(SString()))(_.toVector) + implicitly[Schema[NonEmptyVector[String]]].isOptional shouldBe false + implicitly[Schema[NonEmptySet[String]]].schemaType shouldBe SArray[NonEmptySet[String], String](Schema(SString()))(_.toSortedSet) implicitly[Schema[NonEmptySet[String]]].isOptional shouldBe false @@ -32,13 +35,19 @@ class TapirCodecCatsTest extends AnyFlatSpec with Matchers with Checkers { def expectedValidator[C[X] <: Iterable[X]] = schemaForTest.asIterable[C].validate(Validator.minSize(1)) implicitly[Schema[NonEmptyList[Test]]].showValidators shouldBe expectedValidator[List].showValidators + implicitly[Schema[NonEmptyVector[Test]]].showValidators shouldBe expectedValidator[List].showValidators implicitly[Schema[NonEmptySet[Test]]].showValidators shouldBe expectedValidator[List].showValidators implicitly[Schema[NonEmptyChain[Test]]].showValidators shouldBe expectedValidator[Set].showValidators } implicit def arbitraryNonEmptyList[T: Arbitrary]: Arbitrary[NonEmptyList[T]] = Arbitrary( - Gen.nonEmptyListOf(implicitly[Arbitrary[T]].arbitrary).map(NonEmptyList.fromListUnsafe(_)) + Gen.nonEmptyListOf(implicitly[Arbitrary[T]].arbitrary).map(NonEmptyList.fromListUnsafe) + ) + + implicit def arbitraryNonEmptyVector[T: Arbitrary]: Arbitrary[NonEmptyVector[T]] = + Arbitrary( + Gen.nonEmptyListOf(implicitly[Arbitrary[T]].arbitrary).map(_.toVector).map(NonEmptyVector.fromVectorUnsafe) ) implicit def arbitraryNonEmptyChain[T: Arbitrary]: Arbitrary[NonEmptyChain[T]] = @@ -64,43 +73,70 @@ class TapirCodecCatsTest extends AnyFlatSpec with Matchers with Checkers { it should "fail on empty list" in { val codecForNel = implicitly[Codec[List[String], NonEmptyList[String], CodecFormat.TextPlain]] - codecForNel.decode(List()) shouldBe DecodeResult.Missing + codecForNel.decode(Nil) shouldBe DecodeResult.Missing } - it should "have the proper schema" in { + it should "have the proper schema for list" in { val codecForNel = implicitly[Codec[List[String], NonEmptyList[String], CodecFormat.TextPlain]] codecForNel.schema.copy(validator = Validator.pass) shouldBe implicitly[Schema[NonEmptyList[String]]].copy(validator = Validator.pass) codecForNel.schema.validator.show shouldBe implicitly[Schema[NonEmptyList[String]]].validator.show } - it should "have the proper validator" in { + it should "have the proper validator for list" in { val codecForNel = implicitly[Codec[List[String], NonEmptyList[String], CodecFormat.TextPlain]] codecForNel.schema.showValidators shouldBe implicitly[Schema[NonEmptyList[String]]].showValidators } + "Provided PlainText coder for non empty vector" should "correctly serialize a non empty vector" in { + val codecForNev = implicitly[Codec[List[String], NonEmptyVector[String], CodecFormat.TextPlain]] + val rawCodec = implicitly[Codec[List[String], Vector[String], CodecFormat.TextPlain]] + check((a: NonEmptyVector[String]) => codecForNev.encode(a) == rawCodec.encode(a.toVector)) + } + + it should "correctly deserialize everything it serialize for vector" in { + val codecForNev = implicitly[Codec[List[String], NonEmptyVector[String], CodecFormat.TextPlain]] + check((a: NonEmptyVector[String]) => codecForNev.decode(codecForNev.encode(a)) == DecodeResult.Value(a)) + } + + it should "fail on empty vector" in { + val codecForNev = implicitly[Codec[List[String], NonEmptyVector[String], CodecFormat.TextPlain]] + codecForNev.decode(Nil) shouldBe DecodeResult.Missing + } + + it should "have the proper schema for vector" in { + val codecForNev = implicitly[Codec[List[String], NonEmptyVector[String], CodecFormat.TextPlain]] + codecForNev.schema.copy(validator = Validator.pass) shouldBe implicitly[Schema[NonEmptyVector[String]]].copy(validator = Validator.pass) + codecForNev.schema.validator.show shouldBe implicitly[Schema[NonEmptyVector[String]]].validator.show + } + + it should "have the proper validator for vector" in { + val codecForNev = implicitly[Codec[List[String], NonEmptyVector[String], CodecFormat.TextPlain]] + codecForNev.schema.showValidators shouldBe implicitly[Schema[NonEmptyVector[String]]].showValidators + } + "Provided PlainText codec for non empty chain" should "correctly serialize a non empty chain" in { val codecForNec = implicitly[Codec[List[String], NonEmptyChain[String], CodecFormat.TextPlain]] val rawCodec = implicitly[Codec[List[String], List[String], CodecFormat.TextPlain]] check((a: NonEmptyChain[String]) => codecForNec.encode(a) == rawCodec.encode(a.toNonEmptyList.toList)) } - it should "correctly deserialize everything it serialize" in { + it should "correctly deserialize everything it serialize for chain" in { val codecForNec = implicitly[Codec[List[String], NonEmptyChain[String], CodecFormat.TextPlain]] check((a: NonEmptyChain[String]) => codecForNec.decode(codecForNec.encode(a)) == DecodeResult.Value(a)) } - it should "fail on empty list" in { + it should "fail on empty chain" in { val codecForNec = implicitly[Codec[List[String], NonEmptyChain[String], CodecFormat.TextPlain]] - codecForNec.decode(List()) shouldBe DecodeResult.Missing + codecForNec.decode(Nil) shouldBe DecodeResult.Missing } - it should "have the proper schema" in { + it should "have the proper schema for chain" in { val codecForNec = implicitly[Codec[List[String], NonEmptyChain[String], CodecFormat.TextPlain]] codecForNec.schema.copy(validator = Validator.pass) shouldBe implicitly[Schema[NonEmptyChain[String]]].copy(validator = Validator.pass) codecForNec.schema.validator.show shouldBe implicitly[Schema[NonEmptyChain[String]]].validator.show } - it should "have the proper validator" in { + it should "have the proper validator for chain" in { val codecForNec = implicitly[Codec[List[String], NonEmptyChain[String], CodecFormat.TextPlain]] codecForNec.schema.showValidators shouldBe implicitly[Schema[NonEmptyChain[String]]].showValidators } @@ -111,7 +147,7 @@ class TapirCodecCatsTest extends AnyFlatSpec with Matchers with Checkers { check((a: NonEmptySet[String]) => codecForNes.encode(a) == rawCodec.encode(a.toSortedSet)) } - it should "correctly deserialize everything it serialize" in { + it should "correctly deserialize everything it serialize for set" in { val codecForNes = implicitly[Codec[List[String], NonEmptySet[String], CodecFormat.TextPlain]] check((a: NonEmptySet[String]) => codecForNes.decode(codecForNes.encode(a)) == DecodeResult.Value(a)) } @@ -121,13 +157,13 @@ class TapirCodecCatsTest extends AnyFlatSpec with Matchers with Checkers { codecForNes.decode(Nil) shouldBe DecodeResult.Missing } - it should "have the proper schema" in { + it should "have the proper schema for set" in { val codecForNes = implicitly[Codec[List[String], NonEmptySet[String], CodecFormat.TextPlain]] codecForNes.schema.copy(validator = Validator.pass) shouldBe implicitly[Schema[NonEmptySet[String]]].copy(validator = Validator.pass) codecForNes.schema.validator.show shouldBe implicitly[Schema[NonEmptySet[String]]].validator.show } - it should "have the proper validator" in { + it should "have the proper validator for set" in { val codecForNes = implicitly[Codec[List[String], NonEmptySet[String], CodecFormat.TextPlain]] codecForNes.schema.showValidators shouldBe implicitly[Schema[NonEmptySet[String]]].showValidators }