From 7f867c681b2d7a6f2cc1008168551db9af18abde Mon Sep 17 00:00:00 2001 From: Vishwanath Martur <64204611+vishwamartur@users.noreply.github.com> Date: Sat, 16 Nov 2024 21:36:07 +0530 Subject: [PATCH] Add OpenAPI verifier to verify endpoints against OpenAPI spec Related to #3645 Implement `OpenAPIVerifier` in the `openapi-docs` module to verify endpoints against a given OpenAPI spec. * Add `OpenAPIVerifier` class in `docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIVerifier.scala` - Implement three modes: exact, at-least, and at-most - Use `OpenAPIComparator` to compare `OpenAPI` instances * Add tests for `OpenAPIVerifier` in `docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/OpenAPIVerifierTest.scala` - Test all three modes: exact, at-least, and at-most - Use sample endpoints and OpenAPI specs for testing --- .../tapir/docs/openapi/OpenAPIVerifier.scala | 49 +++++++ .../docs/openapi/OpenAPIVerifierTest.scala | 123 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIVerifier.scala create mode 100644 docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/OpenAPIVerifierTest.scala diff --git a/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIVerifier.scala b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIVerifier.scala new file mode 100644 index 0000000000..b73bad4598 --- /dev/null +++ b/docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIVerifier.scala @@ -0,0 +1,49 @@ +package sttp.tapir.docs.openapi + +import sttp.apispec.openapi.OpenAPI +import sttp.apispec.openapi.circe._ +import sttp.tapir.AnyEndpoint +import sttp.tapir.docs.openapi.OpenAPIVerifier.Mode +import sttp.tapir.docs.openapi.OpenAPIVerifier.Mode.{AtLeast, AtMost, Exact} +import sttp.tapir.openapi.OpenAPIComparator +import io.circe.parser._ + +object OpenAPIVerifier { + sealed trait Mode + object Mode { + case object Exact extends Mode + case object AtLeast extends Mode + case object AtMost extends Mode + } + + def verify(endpoints: List[AnyEndpoint], openApiSpec: String, mode: Mode): Either[String, Unit] = { + parse(openApiSpec) match { + case Left(error) => Left(s"Failed to parse OpenAPI spec: ${error.getMessage}") + case Right(json) => + json.as[OpenAPI] match { + case Left(error) => Left(s"Failed to decode OpenAPI spec: ${error.getMessage}") + case Right(openApi) => + val generatedOpenApi = OpenAPIDocsInterpreter().toOpenAPI(endpoints, openApi.info) + val comparator = new OpenAPIComparator + + mode match { + case Exact => + comparator.compare(generatedOpenApi, openApi) match { + case Nil => Right(()) + case issues => Left(s"Incompatibilities found: ${issues.mkString(", ")}") + } + case AtLeast => + comparator.compareAtLeast(generatedOpenApi, openApi) match { + case Nil => Right(()) + case issues => Left(s"Incompatibilities found: ${issues.mkString(", ")}") + } + case AtMost => + comparator.compareAtMost(generatedOpenApi, openApi) match { + case Nil => Right(()) + case issues => Left(s"Incompatibilities found: ${issues.mkString(", ")}") + } + } + } + } + } +} diff --git a/docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/OpenAPIVerifierTest.scala b/docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/OpenAPIVerifierTest.scala new file mode 100644 index 0000000000..311ffe495f --- /dev/null +++ b/docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/OpenAPIVerifierTest.scala @@ -0,0 +1,123 @@ +package sttp.tapir.docs.openapi + +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import sttp.apispec.openapi.Info +import sttp.tapir._ +import sttp.tapir.docs.openapi.OpenAPIVerifier.Mode.{AtLeast, AtMost, Exact} + +class OpenAPIVerifierTest extends AnyFunSuite with Matchers { + + val sampleEndpoints: List[AnyEndpoint] = List( + endpoint.get.in("path1").out(stringBody), + endpoint.post.in("path2").in(jsonBody[Int]).out(jsonBody[String]) + ) + + val sampleOpenApiSpec: String = + """ + |openapi: 3.0.0 + |info: + | title: Sample API + | version: 1.0.0 + |paths: + | /path1: + | get: + | responses: + | '200': + | description: OK + | content: + | text/plain: + | schema: + | type: string + | /path2: + | post: + | requestBody: + | content: + | application/json: + | schema: + | type: integer + | responses: + | '200': + | description: OK + | content: + | application/json: + | schema: + | type: string + |""".stripMargin + + test("OpenAPIVerifier should verify exact match") { + val result = OpenAPIVerifier.verify(sampleEndpoints, sampleOpenApiSpec, Exact) + result shouldBe Right(()) + } + + test("OpenAPIVerifier should verify at-least match") { + val result = OpenAPIVerifier.verify(sampleEndpoints, sampleOpenApiSpec, AtLeast) + result shouldBe Right(()) + } + + test("OpenAPIVerifier should verify at-most match") { + val result = OpenAPIVerifier.verify(sampleEndpoints, sampleOpenApiSpec, AtMost) + result shouldBe Right(()) + } + + val additionalOpenApiSpec: String = + """ + |openapi: 3.0.0 + |info: + | title: Sample API + | version: 1.0.0 + |paths: + | /path1: + | get: + | responses: + | '200': + | description: OK + | content: + | text/plain: + | schema: + | type: string + | /path2: + | post: + | requestBody: + | content: + | application/json: + | schema: + | type: integer + | responses: + | '200': + | description: OK + | content: + | application/json: + | schema: + | type: string + | /path3: + | put: + | requestBody: + | content: + | application/json: + | schema: + | type: string + | responses: + | '200': + | description: OK + | content: + | application/json: + | schema: + | type: string + |""".stripMargin + + test("OpenAPIVerifier should fail exact match with additional endpoints") { + val result = OpenAPIVerifier.verify(sampleEndpoints, additionalOpenApiSpec, Exact) + result shouldBe a[Left[_, _]] + } + + test("OpenAPIVerifier should pass at-least match with additional endpoints") { + val result = OpenAPIVerifier.verify(sampleEndpoints, additionalOpenApiSpec, AtLeast) + result shouldBe Right(()) + } + + test("OpenAPIVerifier should fail at-most match with additional endpoints") { + val result = OpenAPIVerifier.verify(sampleEndpoints, additionalOpenApiSpec, AtMost) + result shouldBe a[Left[_, _]] + } +}