All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalatra.swagger.SwaggerBase.scala Maven / Gradle / Ivy

The newest version!
package org.scalatra
package swagger

import org.json4s.JsonDSL._
import org.json4s._
import org.scalatra.json.JsonSupport
import org.scalatra.swagger.DataType.{ ContainerDataType, ValueDataType }
import org.slf4j.LoggerFactory

import scala.util.Try
import scala.collection.immutable.ListMap

/**
 * Trait that serves the resource and operation listings, as specified by the Swagger specification.
 */
trait SwaggerBase extends Initializable { self: ScalatraBase with JsonSupport[?] with CorsSupport =>

  private lazy val logger = LoggerFactory.getLogger(getClass)

  implicit protected def jsonFormats: Formats = DefaultFormats
  implicit protected def docToJson(doc: Api): JValue = Extraction.decompose(doc)

  implicit override def string2RouteMatcher(path: String): RailsRouteMatcher = new RailsRouteMatcher(path)

  implicit class JsonAssocNonEmpty(left: JObject) {
    def ~!(right: JObject): JObject = {
      right.obj.headOption match {
        case Some((_, JArray(arr))) if arr.isEmpty => left.obj
        case Some((_, JObject(fs))) if fs.isEmpty => left.obj
        case _ => JObject(left.obj ::: right.obj)
      }
    }
  }

  /**
   * The name of the route to use when getting the index listing for swagger
   * defaults to optional resources.:format or /
   * @return The name of the route
   */
  protected def indexRoute: String = "resources"

  /**
   * Whether to include the format parameter in the index listing for swagger
   * defaults to false, the format parameter will not be present but is still optional.
   * @return true if the format parameter should be included in the returned json
   */
  protected def includeFormatParameter: Boolean = false

  abstract override def initialize(config: ConfigT): Unit = {
    super.initialize(config)
    get("/swagger.json") {
      val renderedSwagger = renderSwagger2(swagger.docs.toList)

      swagger.extraSwaggerDefinition.map {
        renderedSwagger merge _
      }.getOrElse(renderedSwagger)
    }
  }

  /**
   * Returns the Swagger instance responsible for generating the resource and operation listings.
   */
  protected implicit def swagger: SwaggerEngine

  //  private[this] def dontAddOnEmpty(key: String, value: List[String])(json: JValue) = {
  //    val v: JValue = if (value.nonEmpty) key -> value else JNothing
  //    json merge v
  //  }

  private[this] def generateDataType(dataType: DataType): List[JField] = {
    dataType match {
      case t: ValueDataType if t.qualifiedName.isDefined =>
        List(("$ref" -> s"#/definitions/${t.name}"))
      case t: ValueDataType =>
        List(("type" -> t.name), ("format" -> t.format))
      case t: ContainerDataType if t.name == "Map" =>
        List(("type" -> "object"), ("additionalProperties" -> generateDataType(t.typeArg.get)))
      case t: ContainerDataType =>
        List(("type" -> "array"), ("items" -> generateDataType(t.typeArg.get)))
    }
  }

  protected def bathPath: Option[String] = {
    val path = url("/", includeContextPath = swagger.baseUrlIncludeContextPath, includeServletPath = swagger.baseUrlIncludeServletPath)
    if (path.isEmpty) None else Some(path)
  }

  protected def renderSwagger2(docs: List[Api]): JValue = {
    ("swagger" -> "2.0") ~
      ("basePath" -> bathPath) ~
      ("host" -> { if (swagger.host.isEmpty) JNothing else JString(swagger.host) }) ~
      ("info" ->
        ("title" -> swagger.apiInfo.title) ~
        ("version" -> swagger.apiVersion) ~
        ("description" -> swagger.apiInfo.description) ~
        ("termsOfService" -> swagger.apiInfo.termsOfServiceUrl) ~
        ("contact" -> (
          ("name" -> swagger.apiInfo.contact.name) ~
          ("url" -> swagger.apiInfo.contact.url) ~
          ("email" -> swagger.apiInfo.contact.email))) ~
          ("license" -> (
            ("name" -> swagger.apiInfo.license.name) ~
            ("url" -> swagger.apiInfo.license.url)))) ~
            ("paths" ->
              ListMap(docs.filter(_.apis.nonEmpty).flatMap { doc =>
                doc.apis.collect {
                  case api: Endpoint =>
                    (api.path -> api.operations.sortBy(_.position).map { operation =>
                      val responses = operation.responseMessages.map { response =>
                        (response.code.toString ->
                          ("description", response.message) ~~
                          response.responseModel.map { model =>
                            List(JField("schema", JObject(JField("$ref", s"#/definitions/${model}"))))
                          }.getOrElse(Nil))
                      }.toMap
                      (operation.method.toString.toLowerCase -> (
                        ("operationId" -> operation.operationId) ~
                        ("summary" -> operation.summary) ~!
                        ("description" -> operation.description) ~!
                        ("schemes" -> operation.schemes) ~!
                        ("consumes" -> operation.consumes) ~!
                        ("produces" -> operation.produces) ~!
                        ("tags" -> operation.tags) ~
                        ("deprecated" -> operation.deprecated) ~
                        ("parameters" -> operation.getVisibleParameters.sortBy(_.position).map { parameter =>
                          ("name" -> parameter.name) ~
                            ("description" -> parameter.description) ~
                            ("default" -> parameter.defaultValue) ~
                            ("example" -> toTypedAst(parameter.example, parameter.`type`)) ~~
                            generateAllowableValues(parameter.allowableValues, parameter.maximumValue, parameter.maximumValue) ~
                            ("required" -> parameter.required) ~
                            ("in" -> swagger2ParamTypeMapping(parameter.paramType.toString.toLowerCase)) ~~
                            (if (parameter.paramType.toString.toLowerCase == "body") {
                              List(JField("schema", generateDataType(parameter.`type`)))
                            } else {
                              generateDataType(parameter.`type`)
                            })
                        }) ~
                        ("responses" ->
                          (if (responses.nonEmpty && responses.keySet.exists(_.startsWith("2"))) responses else {
                            responses + ("200" ->
                              (if (operation.responseClass.name == "void") {
                                JObject(JField("description", "No response"))
                              } else {
                                JObject(JField("description", "OK"), JField("schema", generateDataType(operation.responseClass)))
                              }))
                          })) ~!
                          ("security" -> (operation.authorizations.flatMap { requirement =>
                            swagger.authorizations.find(_.keyName == requirement).map {
                              case a: OAuth => (requirement -> a.scopes)
                              case b: ApiKey => (requirement -> List.empty)
                              case c: BasicAuth => (requirement -> List.empty)
                              case _ => (requirement -> List.empty)
                            }
                          }))))
                    })
                }
              }: _*)) ~
              ("definitions" -> docs.flatMap { doc =>
                doc.models.map {
                  case (name, model) =>
                    (name ->
                      ("type" -> "object") ~
                      ("description" -> model.description) ~
                      ("discriminator" -> model.discriminator) ~
                      ("properties" -> ListMap(model.getVisibleProperties.sortBy(_._2.position).map {
                        case (name, property) =>
                          (name ->
                            ("example" -> toTypedAst(property.example, property.`type`)) ~
                            ("description" -> property.description) ~~
                            generateAllowableValues(property.allowableValues, property.minimumValue, property.maximumValue) ~
                            ("default" -> toTypedAst(property.default, property.`type`)) ~~
                            generateDataType(property.`type`))
                      } *)) ~!
                      ("required" -> model.getVisibleProperties.collect {
                        case (name, property) if property.required => name
                      }))
                }
              }.toMap) ~
              ("securityDefinitions" -> (swagger.authorizations.flatMap { auth =>
                (auth match {
                  case a: OAuth => a.grantTypes.headOption.map {
                    case g: ImplicitGrant => (a.keyName -> JObject(
                      JField("type", "oauth2"),
                      JField("description", a.description),
                      JField("flow", "implicit"),
                      JField("authorizationUrl", g.loginEndpoint.url),
                      JField("scopes", a.scopes.map(scope => JField(scope, scope)))))
                    case g: AuthorizationCodeGrant => (a.keyName -> JObject(
                      JField("type", "oauth2"),
                      JField("description", a.description),
                      JField("flow", "accessCode"),
                      JField("authorizationUrl", g.tokenRequestEndpoint.url),
                      JField("tokenUrl", g.tokenEndpoint.url),
                      JField("scopes", a.scopes.map(scope => JField(scope, scope)))))
                    case g: ApplicationGrant => ("oauth2" -> JObject(
                      JField("type", "oauth2"),
                      JField("description", a.description),
                      JField("flow", "application"),
                      JField("tokenUrl", g.tokenEndpoint.url),
                      JField("scopes", a.scopes.map(scope => JField(scope, scope)))))
                  }
                  case a: ApiKey => Some((a.keyName -> JObject(
                    JField("type", "apiKey"),
                    JField("description", a.description),
                    JField("name", a.keyName),
                    JField("in", a.passAs))))
                  case a: BasicAuth => Some(((a.keyName -> JObject(
                    JField("type", "basic"),
                    JField("description", a.description),
                    JField("name", a.keyName)))))
                })
              }).toMap)
  }

  private def toTypedAst(dataVal: Option[String], dataType: DataType): JValue = {
    dataVal.map { x =>
      val parsingResult: Option[JValue] = dataType.name.toLowerCase match {
        case "string" => Some(JString(x))
        case "integer" => Try(x.toLong).map(JLong(_)).toOption
        case "number" => Try(x.toDouble).map(JDouble(_)).toOption
        case "boolean" => Try(x.toBoolean).map(JBool(_)).toOption
        case _ => Try(parse(x)).toOption
      }
      parsingResult.getOrElse(JString(x))
    }.getOrElse(JNothing)
  }

  private def generateAllowableValues(allowableValues: AllowableValues, minimum: Option[Double], maximum: Option[Double]): List[JField] = {
    allowableValues match {
      case x: AllowableValues.AllowableRangeValues =>
        List(
          "minimum" -> minimum.map(JDouble(_)).getOrElse(JLong(x.values.start)),
          "maximum" -> maximum.map(JDouble(_)).getOrElse(JLong(x.values.end)))
      case x: AllowableValues.AllowableValuesList[?] =>
        List("enum" -> Extraction.decompose(x.values))
      case _ =>
        List("minimum" -> minimum, "maximum" -> maximum)
    }
  }

  private def swagger2ParamTypeMapping(paramTypeName: String): String = {
    if (paramTypeName == "form") "formData" else paramTypeName
  }

  error {
    case t: Throwable =>
      logger.error("Error during rendering swagger.json", t)
      throw t
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy