Skip to content

Instantly share code, notes, and snippets.

@mitallast
Created February 13, 2020 14:38
Show Gist options
  • Save mitallast/83e4a8a311603741e0b939bdefb69453 to your computer and use it in GitHub Desktop.
Save mitallast/83e4a8a311603741e0b939bdefb69453 to your computer and use it in GitHub Desktop.
package mall.swagger
import java.lang.IllegalArgumentException
import java.text.SimpleDateFormat
import java.util.{Date, UUID}
import cats._, cats.effect._, cats.implicits._
import io.circe._
import io.circe.generic.auto._
import io.circe.parser._
import io.circe.syntax._
import io.swagger.v3.oas.models.{OpenAPI, PathItem}
import io.swagger.v3.oas.models.media._
import io.swagger.v3.oas.models.responses.ApiResponse
import io.swagger.v3.parser.OpenAPIV3Parser
import org.apache.http.client.methods.{HttpPost, HttpUriRequest}
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.HttpClients
import org.apache.logging.log4j.scala.Logging
import org.scalatest._
import org.http4s.client.blaze._
import org.http4s.client._
import org.http4s.Uri
import scala.concurrent.ExecutionContext.global
import cats.effect.Blocker
import java.util.concurrent._
import collection.JavaConverters._
import scala.io.Source
object encoders {
val dateFormat = new SimpleDateFormat("yyyy-MM-dd")
implicit val dateEncoder: Encoder[Date] = Encoder.encodeString.contramap { date =>
dateFormat.format(date)
}
implicit val numberEncoder: Encoder[Number] = Encoder.instance {
case value: java.lang.Integer => Json.fromInt(value)
case value: java.lang.Long => Json.fromLong(value)
case value: java.lang.Float => Json.fromFloatOrNull(value)
case value: java.lang.Double => Json.fromDoubleOrNull(value)
case value: java.math.BigDecimal => Json.fromBigDecimal(value)
case value: java.math.BigInteger => Json.fromBigInt(value)
}
implicit val objectEncoder: Encoder[Object] = Encoder.instance {
case value: String => value.asJson
}
}
object extractors {
object Examples {
def unapply[T](schema: Schema[T]): Option[Vector[T]] = Option(schema.getExample).collect {
case map: java.util.Map[String, T] => map.asScala.values.toVector
}
}
object XExamples {
def unapply(schema: Schema[_]): Option[Vector[Object]] =
(for {
extensions <- Option(schema.getExtensions)
examples <- Option(extensions.get("x-examples"))
_ = println(examples)
} yield examples)
.collect {
case list: java.util.List[Object] => list.asScala.toVector
case map: java.util.Map[String, Object] => map.asScala.values.toVector
}
}
object Example {
def unapply[T](schema: Schema[T]): Option[T] = Option(schema.getExample).collect {
case t: T => t
}
}
object Default {
def unapply[T](schema: Schema[T]): Option[T] = Option(schema.getDefault).collect {
case t: T => t
}
}
object ComponentRef {
private val schemaRef = "^#/components/schemas/([a-zA-Z0-9_]+)$".r
def unapply[T](schema: Schema[T]): Option[String] = Option(schema.get$ref()).collect {
case schemaRef(name) => name
}
}
}
object generators {
import encoders._
import extractors._
sealed trait Generator[S] {
def apply(schema: S): Vector[Json]
}
def generate(schema: Schema[_])(implicit gen: Generator[Schema[_]]): Vector[Json] = gen(schema)
def instance[S](f: S => Vector[Json]): Generator[S] = new Generator[S] {
override def apply(schema: S): Vector[Json] = f(schema)
}
implicit val integerGen: Generator[IntegerSchema] = instance {
case Examples(examples) => examples.map(_.asJson)
case XExamples(examples) => examples.map(_.asJson)
case Example(example) => Vector(example.asJson)
case Default(example) => Vector(example.asJson)
case _ => Vector(Json.fromInt(0), Json.fromInt(1), Json.fromInt(42))
}
implicit val stringGen: Generator[StringSchema] = instance {
case Examples(examples) => examples.map(_.asJson)
case XExamples(examples) => examples.map(_.asJson)
case Example(example) => Vector(example.asJson)
case Default(example) => Vector(example.asJson)
case _ => Vector(Json.fromString("hello world"))
}
implicit val dateGen: Generator[DateSchema] = instance {
case Examples(examples) => examples.map(_.asJson)
case XExamples(examples) => examples.map(_.asJson)
case Example(example) => Vector(example.asJson)
case Default(example) => Vector(example.asJson)
case _ => Vector(Json.fromString("hello world"))
}
implicit val uuidGen: Generator[UUIDSchema] = instance {
case Examples(examples) => examples.map(_.asJson)
case XExamples(examples) => examples.map(_.asJson)
case Example(example) => Vector(example.asJson)
case Default(example) => Vector(example.asJson)
case _ => Vector(UUID.randomUUID().asJson)
}
implicit def arrayGen(implicit api: OpenAPI): Generator[ArraySchema] = instance { schema =>
generate(schema.getItems).map(Json.arr(_))
}
implicit def objectGen(implicit api: OpenAPI): Generator[ObjectSchema] = instance {
case Examples(examples) => examples.map(_.asJson)
case Example(example) => Vector(example.asJson)
case schema =>
val fields = schema.getProperties.asScala.mapValues(generate(_)).toList
val builder = Vector.newBuilder[Json]
def flatten(gen: List[(String, Json)], fields: List[(String, Vector[Json])]): Unit = fields match {
case Nil => builder += gen.toMap.asJson
case (field, examples) :: tail =>
for (example <- examples) {
val stack = (field, example) :: gen
flatten(stack, tail)
}
}
flatten(Nil, fields)
builder.result()
}
implicit def refGen(implicit api: OpenAPI): Generator[String] = instance { name =>
val schema: Schema[_] = api.getComponents.getSchemas.get(name)
generate(schema)
}
implicit def schemaGen(
implicit arr: Generator[ArraySchema],
obj: Generator[ObjectSchema],
ref: Generator[String]
): Generator[Schema[_]] = instance {
case schema: IntegerSchema => integerGen(schema)
case schema: StringSchema => stringGen(schema)
case schema: DateSchema => dateGen(schema)
case schema: UUIDSchema => uuidGen(schema)
case schema: ArraySchema => arr(schema)
case schema: ObjectSchema => obj(schema)
case ComponentRef(reference) => ref(reference)
case schema => throw new IllegalArgumentException(s"unsupported schema: $schema")
}
}
object validators extends Matchers {
import extractors._
sealed trait Validator[S] {
def apply(value: Json, schema: S): Unit
}
def validate(value: Json, schema: Schema[_])(implicit v: Validator[Schema[_]]): Unit = v(value, schema)
def instance[S](f: (Json, S) => Unit): Validator[S] = new Validator[S] {
override def apply(value: Json, schema: S): Unit = f(value, schema)
}
implicit val stringValidator: Validator[StringSchema] = instance { (json, schema) =>
json.fold[Unit](
() => (), // ignore null
_ => fail("should be string, actual boolean"),
_ => fail("should be string, actual number"),
value => {
if (schema.getPattern != null) value should (fullyMatch regex schema.getPattern)
}, // test string,
_ => fail("should be string, actual array"),
_ => fail("should be string, actual object")
)
}
implicit val dateValidator: Validator[DateSchema] = instance { (json, schema) =>
json.fold[Unit](
() => (), // ignore null
_ => fail("should be date, actual boolean"),
_ => fail("should be date, actual number"),
value => {
value should (fullyMatch regex "[1-9]\\d\\d\\d-[0-1]\\d-[0123]\\d")
}, // test string,
_ => fail("should be date, actual array"),
_ => fail("should be date, actual object")
)
}
implicit val uuidValidator: Validator[UUIDSchema] = instance { (json, schema) =>
json.fold[Unit](
() => (), // ignore null
_ => fail("should be uuid, actual boolean"),
_ => fail("should be uuid, actual number"),
value => {
value should (fullyMatch regex "\\b[0-9a-f]{8}\\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\\b[0-9a-f]{12}\\b")
}, // test string,
_ => fail("should be uuid, actual array"),
_ => fail("should be uuid, actual object")
)
}
implicit val integerValidator: Validator[IntegerSchema] = instance { (json, schema) =>
json.fold[Unit](
() => (), // ignore null
_ => fail("should be integer, actual boolean"),
num => {
val valueOpt = Option(schema.getFormat) match {
case Some("int32") => num.toInt.map(_.longValue())
case Some("int64") => num.toLong
case None => num.toLong
}
valueOpt should not be (empty)
val value = valueOpt.get
if (schema.getMinimum != null) value shouldBe >=(schema.getMinimum)
if (schema.getExclusiveMinimum != null) value shouldBe >(schema.getMinimum)
if (schema.getMaximum != null) value shouldBe <=(schema.getMaximum)
if (schema.getExclusiveMaximum != null) value shouldBe <(schema.getMaximum)
},
_ => fail("should be integer, actual string"),
_ => fail("should be integer, actual array"),
_ => fail("should be integer, actual object")
)
}
implicit val numberValidator: Validator[NumberSchema] = instance { (json, schema) =>
json.fold[Unit](
() => (), // ignore null
_ => fail("should be number, actual boolean"),
num => {
val value = Option(schema.getFormat) match {
case Some("float") => num.toFloat.toDouble
case Some("double") => num.toDouble
case None => num.toDouble
}
if (schema.getMinimum != null) value shouldBe >=(schema.getMinimum)
if (schema.getExclusiveMinimum != null) value shouldBe >(schema.getMinimum)
if (schema.getMaximum != null) value shouldBe <=(schema.getMaximum)
if (schema.getExclusiveMaximum != null) value shouldBe <(schema.getMaximum)
},
_ => fail("should be number, actual string"),
_ => fail("should be number, actual array"),
_ => fail("should be number, actual object")
)
}
implicit def arrayValidator(implicit api: OpenAPI): Validator[ArraySchema] = instance { (json, schema) =>
json.fold[Unit](
() => (), // ignore null
_ => fail("should be array, actual boolean"),
_ => fail("should be array, actual number"),
_ => fail("should be array, actual string"),
array => {
val itemSchema: Schema[_] = schema.getItems
for (item <- array) {
validate(item, itemSchema)
}
},
_ => fail("should be array, actual object")
)
}
implicit def objectValidator(implicit api: OpenAPI): Validator[ObjectSchema] = instance { (json, schema) =>
json.fold[Unit](
() => (), // ignore null
_ => fail("should be array, actual boolean"),
_ => fail("should be array, actual number"),
_ => fail("should be array, actual string"),
_ => fail("should be array, actual array"),
obj => {
val requiredSet = Option(schema.getRequired).map(_.asScala.toSet).getOrElse(Set.empty)
for ((property, schema) <- schema.getProperties.asScala) {
obj.contains(property) || !requiredSet.contains(property) shouldBe true
obj(property) match {
case Some(value) => validate(value, schema)
case None => requiredSet.contains(property) shouldBe false
}
}
}
)
}
implicit def refValidator(implicit api: OpenAPI): Validator[String] = instance { (json, name) =>
val schema: Schema[_] = api.getComponents.getSchemas.get(name)
validate(json, schema)
}
implicit def schemaValidator(
implicit arr: Validator[ArraySchema],
obj: Validator[ObjectSchema],
ref: Validator[String]
): Validator[Schema[_]] = instance {
case (json, schema: StringSchema) => stringValidator(json, schema)
case (json, schema: DateSchema) => dateValidator(json, schema)
case (json, schema: UUIDSchema) => uuidValidator(json, schema)
case (json, schema: IntegerSchema) => integerValidator(json, schema)
case (json, schema: NumberSchema) => numberValidator(json, schema)
case (json, schema: ArraySchema) => arr(json, schema)
case (json, schema: ObjectSchema) => obj(json, schema)
case (json, ComponentRef(reference)) => ref(json, reference)
case (json, schema) => throw new IllegalArgumentException(s"unsupported schema: $schema")
}
}
class SwaggerApiSpec extends WordSpec with Matchers with Logging {
import io.swagger.v3.parser.core.models.ParseOptions
import generators._
import validators._
private val options = new ParseOptions
options.setResolveFully(true)
private implicit val api: OpenAPI = new OpenAPIV3Parser().read("./petstore.yaml", null, options)
private val blockingPool = Executors.newFixedThreadPool(5)
private val blocker = Blocker.liftExecutorService(blockingPool)
private implicit val cs = IO.contextShift(ExecutionException.global)
private val httpClient: Client[IO] = JavaNetClientBuilder[IO](blocker).create
private val server = api.getServers.asScala.head
api.getTags.asScala.foreach { tag =>
tag.getName should {
api.getPaths.asScala.foreach {
case (path, pathItem) =>
pathItem.readOperationsMap().asScala.filter(_._2.getTags.contains(tag.getName)).foreach {
case (method, operation) =>
// only request body parameters allowed
operation.getParameters shouldBe null
val requestBody = operation.getRequestBody
requestBody should not be (null)
val requestContent = requestBody.getContent
requestContent should not be (null)
val requestMedia = requestContent.get("application/json")
requestMedia should not be (null)
val requestSchema: Schema[_] = requestMedia.getSchema
requestSchema should not be (null)
val responses = operation.getResponses
responses should not be (null)
val examples = generators.generate(requestSchema)
examples.zipWithIndex.foreach {
case (example, index) =>
s"$method $path #$index" in {
try {
val body = example.noSpaces
val uri = server.getUrl.replaceAll("/$", "") + path
logger.info(s"$method $uri $body")
val request: HttpUriRequest = method match {
case PathItem.HttpMethod.POST =>
val request = new HttpPost(uri)
request.setHeader("Content-Type", "application/json")
request.setHeader("Accept-Encoding", "")
request.setEntity(new StringEntity(example.noSpaces))
request
case _ => fail("unexpected http method")
}
logger.info(s"send request")
val httpResponse = http.execute(request)
logger.info(s"check status code: ${httpResponse.getStatusLine.getStatusCode}")
Option(responses.get(s"${httpResponse.getStatusLine.getStatusCode}"))
.orElse(Option(responses.getDefault))
.foreach { response =>
val responseContent = response.getContent
responseContent should not be (null)
val responseMedia = responseContent.get("application/json")
responseMedia should not be (null)
val responseSchema: Schema[_] = responseMedia.getSchema
responseSchema should not be (null)
logger.info(s"read contents")
val responseBody = Source.fromInputStream(httpResponse.getEntity.getContent).mkString
logger.info(s"response: $responseBody")
parse(responseBody) match {
case Left(e) => fail(e)
case Right(json) => validate(json, responseSchema)
}
}
} finally {
logger.info(s"complete")
}
}
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment