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

Add OpenAPI verifier to verify endpoints against OpenAPI spec #4174

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(", ")}")
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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[_, _]]
}
}
Loading