From 528be07be6dfa7e9dd9d628242b9dcaf8a7638eb Mon Sep 17 00:00:00 2001 From: Ionut IORTOMAN Date: Tue, 28 Jul 2020 15:52:59 +0200 Subject: [PATCH 1/3] Test to reproduce issue 657. I'm going to need help solving it, i'm not that well versed in scala yet. --- build.sbt | 7 ++-- .../src/test/resources/expected-enum.yml | 34 +++++++++++++++++++ .../tapir/docs/openapi/VerifyYamlTest.scala | 16 ++++++++- .../scala/sttp/tapir/tests/FruitAmount.scala | 16 +++++++++ 4 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 docs/openapi-docs/src/test/resources/expected-enum.yml diff --git a/build.sbt b/build.sbt index 8d7f07cf87..6bc0e7cba7 100644 --- a/build.sbt +++ b/build.sbt @@ -148,12 +148,13 @@ lazy val tests: ProjectMatrix = (projectMatrix in file("tests")) "io.circe" %% "circe-generic" % Versions.circe, "com.softwaremill.common" %% "tagging" % "2.2.1", scalaTest, - "com.softwaremill.macwire" %% "macros" % "2.3.7" % "provided" - ), + "com.softwaremill.macwire" %% "macros" % "2.3.7" % "provided", + "com.beachape" %% "enumeratum" % Versions.enumeratum, + "com.beachape" %% "enumeratum-circe" % Versions.enumeratum), libraryDependencies ++= loggerDependencies ) .jvmPlatform(scalaVersions = allScalaVersions) - .dependsOn(core, circeJson) + .dependsOn(core, circeJson, enumeratum) // integrations diff --git a/docs/openapi-docs/src/test/resources/expected-enum.yml b/docs/openapi-docs/src/test/resources/expected-enum.yml new file mode 100644 index 0000000000..cdddcb87a9 --- /dev/null +++ b/docs/openapi-docs/src/test/resources/expected-enum.yml @@ -0,0 +1,34 @@ +openapi: 3.0.1 +info: + title: Fruits + version: '1.0' +paths: + /enum-test: + get: + operationId: getEnum-test + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/FruitWithEnum' +components: + schemas: + FruitWithEnum: + required: + - fruit + - amount + type: object + properties: + fruit: + type: string + amount: + type: integer + fruitType: + type: array + items: + type: string + enum: + - APPLE + - PEAR diff --git a/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala b/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala index 187e619f6c..c730d1cb11 100644 --- a/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala +++ b/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala @@ -6,6 +6,7 @@ import io.circe.generic.auto._ import sttp.model.{Method, StatusCode} import sttp.tapir.EndpointIO.Example import sttp.tapir._ +import sttp.tapir.codec.enumeratum.TapirCodecEnumeratum import sttp.tapir.docs.openapi.dtos.Book import sttp.tapir.docs.openapi.dtos.a.{Pet => APet} import sttp.tapir.docs.openapi.dtos.b.{Pet => BPet} @@ -20,13 +21,14 @@ import scala.io.Source import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -class VerifyYamlTest extends AnyFunSuite with Matchers { +class VerifyYamlTest extends AnyFunSuite with Matchers with TapirCodecEnumeratum { val all_the_way: Endpoint[(FruitAmount, String), Unit, (FruitAmount, Int), Nothing] = endpoint .in(("fruit" / path[String] / "amount" / path[Int]).mapTo(FruitAmount)) .in(query[String]("color")) .out(jsonBody[FruitAmount]) .out(header[Int]("X-Role")) + test("should match the expected yaml") { val expectedYaml = loadYaml("expected.yml") @@ -36,6 +38,18 @@ class VerifyYamlTest extends AnyFunSuite with Matchers { actualYamlNoIndent shouldBe expectedYaml } + val enum_test = endpoint.in(("enum-test")).out(jsonBody[FruitWithEnum]) + + test("should match yaml with enum") { + val expectedYaml = loadYaml("expected-enum.yml") + + val actualYaml = List(enum_test).toOpenAPI(Info("Fruits", "1.0")).toYaml + val actualYamlNoIndent = noIndentation(actualYaml) + + actualYamlNoIndent shouldBe expectedYaml + } + + val endpoint_wit_recursive_structure: Endpoint[Unit, Unit, F1, Nothing] = endpoint .out(jsonBody[F1]) diff --git a/tests/src/main/scala/sttp/tapir/tests/FruitAmount.scala b/tests/src/main/scala/sttp/tapir/tests/FruitAmount.scala index cd5d9708b3..df0c3d7b4b 100644 --- a/tests/src/main/scala/sttp/tapir/tests/FruitAmount.scala +++ b/tests/src/main/scala/sttp/tapir/tests/FruitAmount.scala @@ -1,6 +1,11 @@ package sttp.tapir.tests +import enumeratum.EnumEntry +import enumeratum.Enum import sttp.tapir._ +import sttp.tapir.codec.enumeratum.TapirCodecEnumeratum + +import scala.collection.immutable case class FruitAmount(fruit: String, amount: Int) @@ -12,8 +17,19 @@ case class ValidFruitAmount(fruit: StringWrapper, amount: IntWrapper) case class ColorWrapper(color: Color) +case class FruitWithEnum(fruit: String, amount: Int, fruitType: List[FruitType]) + sealed trait Entity { def name: String } case class Person(name: String, age: Int) extends Entity case class Organization(name: String) extends Entity + +sealed trait FruitType extends EnumEntry + +object FruitType extends Enum[FruitType] { + case object APPLE extends FruitType + case object PEAR extends FruitType + + override def values: immutable.IndexedSeq[FruitType] = findValues +} \ No newline at end of file From 216e1e34679deee1a3af0a48b043a086a0e83932 Mon Sep 17 00:00:00 2001 From: Tomasz Pewinski Date: Wed, 12 Aug 2020 11:34:44 +0200 Subject: [PATCH 2/3] Avoid extracting validators from a collection of enums This prevents the enum: validator being attached on the array level in OpenAPI yaml. --- .../tapir/docs/openapi/schema/package.scala | 17 +++++++------- ...enum.yml => expected_valid_enum_array.yml} | 0 .../tapir/docs/openapi/VerifyYamlTest.scala | 22 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) rename docs/openapi-docs/src/test/resources/{expected-enum.yml => expected_valid_enum_array.yml} (100%) diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/package.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/package.scala index 22c4683d12..3d75b0ab93 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/package.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/package.scala @@ -24,14 +24,15 @@ package object schema { private[schema] def asPrimitiveValidators(v: Validator[_]): Seq[Validator.Primitive[_]] = { v match { - case Validator.Mapped(wrapped, _) => asPrimitiveValidators(wrapped) - case Validator.All(validators) => validators.flatMap(asPrimitiveValidators) - case Validator.Any(validators) => validators.flatMap(asPrimitiveValidators) - case Validator.CollectionElements(wrapped, _) => asPrimitiveValidators(wrapped) - case Validator.Product(_) => Nil - case Validator.Coproduct(_) => Nil - case Validator.OpenProduct(_) => Nil - case bv: Validator.Primitive[_] => List(bv) + case Validator.Mapped(wrapped, _) => asPrimitiveValidators(wrapped) + case Validator.All(validators) => validators.flatMap(asPrimitiveValidators) + case Validator.Any(validators) => validators.flatMap(asPrimitiveValidators) + case Validator.CollectionElements(Validator.Enum(_, _), _) => Nil + case Validator.CollectionElements(wrapped, _) => asPrimitiveValidators(wrapped) + case Validator.Product(_) => Nil + case Validator.Coproduct(_) => Nil + case Validator.OpenProduct(_) => Nil + case bv: Validator.Primitive[_] => List(bv) } } diff --git a/docs/openapi-docs/src/test/resources/expected-enum.yml b/docs/openapi-docs/src/test/resources/expected_valid_enum_array.yml similarity index 100% rename from docs/openapi-docs/src/test/resources/expected-enum.yml rename to docs/openapi-docs/src/test/resources/expected_valid_enum_array.yml diff --git a/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala b/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala index c730d1cb11..7ec8c258f9 100644 --- a/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala +++ b/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala @@ -38,18 +38,6 @@ class VerifyYamlTest extends AnyFunSuite with Matchers with TapirCodecEnumeratum actualYamlNoIndent shouldBe expectedYaml } - val enum_test = endpoint.in(("enum-test")).out(jsonBody[FruitWithEnum]) - - test("should match yaml with enum") { - val expectedYaml = loadYaml("expected-enum.yml") - - val actualYaml = List(enum_test).toOpenAPI(Info("Fruits", "1.0")).toYaml - val actualYamlNoIndent = noIndentation(actualYaml) - - actualYamlNoIndent shouldBe expectedYaml - } - - val endpoint_wit_recursive_structure: Endpoint[Unit, Unit, F1, Nothing] = endpoint .out(jsonBody[F1]) @@ -627,6 +615,16 @@ class VerifyYamlTest extends AnyFunSuite with Matchers with TapirCodecEnumeratum actualYamlNoIndent shouldBe expectedYaml } + test("use enum validator for array elements") { + val out_enum_array = endpoint.in(("enum-test")).out(jsonBody[FruitWithEnum]) + val expectedYaml = loadYaml("expected_valid_enum_array.yml") + + val actualYaml = List(out_enum_array).toOpenAPI(Info("Fruits", "1.0")).toYaml + val actualYamlNoIndent = noIndentation(actualYaml) + + actualYamlNoIndent shouldBe expectedYaml + } + test("support example of list and not-list types") { val expectedYaml = loadYaml("expected_examples_of_list_and_not_list_types.yml") val actualYaml = endpoint.post From 24ab3d7eeb8261bf58715944611848c27efef06d Mon Sep 17 00:00:00 2001 From: Tomasz Pewinski Date: Wed, 12 Aug 2020 16:32:20 +0200 Subject: [PATCH 3/3] Fix the more general case of bug #657 In the case of array schemas, the constraints of the array elements were being attached to the array itself when generating the OpenAPI descriptions. This is fixed by not extracting validators out of CollectionElements validator when the schema is equal to SArray, and extracting all validators otherwise. --- .../openapi/schema/TSchemaToOSchema.scala | 13 ++++++-- .../tapir/docs/openapi/schema/package.scala | 30 +++++++++++++------ .../resources/expected_valid_int_array.yml | 21 +++++++++++++ .../tapir/docs/openapi/VerifyYamlTest.scala | 11 ++++++- .../main/scala/sttp/tapir/tests/package.scala | 8 +++++ 5 files changed, 70 insertions(+), 13 deletions(-) create mode 100644 docs/openapi-docs/src/test/resources/expected_valid_int_array.yml diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/TSchemaToOSchema.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/TSchemaToOSchema.scala index d0a99ed5e5..858c2affbe 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/TSchemaToOSchema.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/TSchemaToOSchema.scala @@ -54,11 +54,18 @@ private[schema] class TSchemaToOSchema(schemaReferenceMapper: SchemaReferenceMap ) } + val primitiveValidators = typeData.schema.schemaType match { + case TSchemaType.SArray(_) => asPrimitiveValidators(typeData.validator) + case _ => asPrimitiveValidatorsDeep(typeData.validator) + } + val wholeNumbers = typeData.schema.schemaType match { + case TSchemaType.SInteger => true + case _ => false + } + result .map(addMetadata(_, typeData.schema)) - .map( - addConstraints(_, asPrimitiveValidators(typeData.validator), typeData.schema.schemaType.isInstanceOf[TSchemaType.SInteger.type]) - ) + .map(addConstraints(_, primitiveValidators, wholeNumbers)) } private def addMetadata(oschema: OSchema, tschema: TSchema[_]): OSchema = { diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/package.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/package.scala index 3d75b0ab93..fb0b45e986 100644 --- a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/package.scala +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/schema/package.scala @@ -24,15 +24,27 @@ package object schema { private[schema] def asPrimitiveValidators(v: Validator[_]): Seq[Validator.Primitive[_]] = { v match { - case Validator.Mapped(wrapped, _) => asPrimitiveValidators(wrapped) - case Validator.All(validators) => validators.flatMap(asPrimitiveValidators) - case Validator.Any(validators) => validators.flatMap(asPrimitiveValidators) - case Validator.CollectionElements(Validator.Enum(_, _), _) => Nil - case Validator.CollectionElements(wrapped, _) => asPrimitiveValidators(wrapped) - case Validator.Product(_) => Nil - case Validator.Coproduct(_) => Nil - case Validator.OpenProduct(_) => Nil - case bv: Validator.Primitive[_] => List(bv) + case Validator.Mapped(wrapped, _) => asPrimitiveValidators(wrapped) + case Validator.All(validators) => validators.flatMap(asPrimitiveValidators) + case Validator.Any(validators) => validators.flatMap(asPrimitiveValidators) + case Validator.CollectionElements(_, _) => Nil + case Validator.Product(_) => Nil + case Validator.Coproduct(_) => Nil + case Validator.OpenProduct(_) => Nil + case bv: Validator.Primitive[_] => List(bv) + } + } + + private[schema] def asPrimitiveValidatorsDeep(v: Validator[_]): Seq[Validator.Primitive[_]] = { + v match { + case Validator.Mapped(wrapped, _) => asPrimitiveValidatorsDeep(wrapped) + case Validator.All(validators) => validators.flatMap(asPrimitiveValidatorsDeep) + case Validator.Any(validators) => validators.flatMap(asPrimitiveValidatorsDeep) + case Validator.CollectionElements(mapped, _) => asPrimitiveValidatorsDeep(mapped) + case Validator.Product(_) => Nil + case Validator.Coproduct(_) => Nil + case Validator.OpenProduct(_) => Nil + case bv: Validator.Primitive[_] => List(bv) } } diff --git a/docs/openapi-docs/src/test/resources/expected_valid_int_array.yml b/docs/openapi-docs/src/test/resources/expected_valid_int_array.yml new file mode 100644 index 0000000000..2cf39cfdad --- /dev/null +++ b/docs/openapi-docs/src/test/resources/expected_valid_int_array.yml @@ -0,0 +1,21 @@ +openapi: 3.0.1 +info: + title: Entities + version: '1.0' +paths: + /: + get: + operationId: getRoot + requestBody: + content: + application/json: + schema: + type: array + items: + type: integer + minimum: 1 + maximum: 10 + required: false + responses: + '200': + description: '' \ No newline at end of file diff --git a/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala b/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala index 7ec8c258f9..11828ebc5c 100644 --- a/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala +++ b/docs/openapi-docs/src/test/scala/sttp/tapir/docs/openapi/VerifyYamlTest.scala @@ -28,7 +28,6 @@ class VerifyYamlTest extends AnyFunSuite with Matchers with TapirCodecEnumeratum .out(jsonBody[FruitAmount]) .out(header[Int]("X-Role")) - test("should match the expected yaml") { val expectedYaml = loadYaml("expected.yml") @@ -575,6 +574,16 @@ class VerifyYamlTest extends AnyFunSuite with Matchers with TapirCodecEnumeratum actualYamlNoIndent shouldBe expectedYaml } + test("render validator for additional properties of array elements") { + val expectedYaml = loadYaml("expected_valid_int_array.yml") + + val actualYaml = Validation.in_valid_int_array + .toOpenAPI(Info("Entities", "1.0")) + .toYaml + val actualYamlNoIndent = noIndentation(actualYaml) + actualYamlNoIndent shouldBe expectedYaml + } + test("render enum validator for classes") { val expectedYaml = loadYaml("expected_valid_enum_class.yml") diff --git a/tests/src/main/scala/sttp/tapir/tests/package.scala b/tests/src/main/scala/sttp/tapir/tests/package.scala index ed22118595..7bc2aeab50 100644 --- a/tests/src/main/scala/sttp/tapir/tests/package.scala +++ b/tests/src/main/scala/sttp/tapir/tests/package.scala @@ -414,6 +414,14 @@ package object tests { endpoint.in(jsonBody[ColorWrapper]) } + val in_valid_int_array: Endpoint[List[IntWrapper], Unit, Unit, Nothing] = { + implicit val schemaForIntWrapper: Schema[IntWrapper] = Schema(SchemaType.SInteger) + implicit val encoder: Encoder[IntWrapper] = Encoder.encodeInt.contramap(_.v) + implicit val decode: Decoder[IntWrapper] = Decoder.decodeInt.map(IntWrapper.apply) + implicit val v: Validator[IntWrapper] = Validator.all(Validator.min(1), Validator.max(10)).contramap(_.v) + endpoint.in(jsonBody[List[IntWrapper]]) + } + val allEndpoints: Set[Endpoint[_, _, _, _]] = wireSet[Endpoint[_, _, _, _]] }