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

org.mashupbots.socko.rest.SwaggerApiDocs.scala Maven / Gradle / Ivy

The newest version!
//
// Copyright 2013 Vibul Imtarnasan, David Bolton and Socko contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package org.mashupbots.socko.rest

import scala.collection.mutable.HashMap
import scala.reflect.runtime.{universe => ru}
import org.json4s.NoTypeHints
import org.json4s.native.{Serialization => json}
import org.mashupbots.socko.infrastructure.Logger
import org.mashupbots.socko.infrastructure.CharsetUtil
import org.mashupbots.socko.events.EndPoint

/**
 * Generated [[https://developers.helloreverb.com/swagger/ Swagger]] API documentation
 * 
 * @param lookup Map of path and swagger JSON associated with the path
 */
case class SwaggerApiDocs(lookup: Map[String, Array[Byte]]) {
  
  /**
   * Gets the JSON doc for the specified end point
   * 
   * @param path Full path to the requested documentation. For example: `/api/api-docs.json/pet`.
   * @return JSON UTF-8, `None` if data for path not found
   */
  def get(path: String): Option[Array[Byte]] = {
    lookup.get(path)
  }
}

/**
 * Companion object
 */
object SwaggerApiDocs extends Logger {

  /**
   * URL path relative to the config `rootUrl` that will trigger the return of the API documentation
   */
  val urlPath = "/api-docs.json"
    
  /**
   * Generates a Map of URL paths and the associated API documentation to be returned for these paths
   *
   * @param operations Rest operations
   * @param config Rest configuration
   * @param rm Runtime Mirror used to reflect property meta data
   * @return Map with the `key` being the exact path to match, the value is the JSON string
   */
  def apply(operations: Seq[RestOperation], config: RestConfig, rm: ru.Mirror): SwaggerApiDocs = {
    val result: HashMap[String, Array[Byte]] = new HashMap[String, Array[Byte]]()

    // Group operations into resources based on path segments
    val apisMap: Map[String, Seq[RestOperation]] = operations.groupBy(o => {
      // Get number of path segments specified in config for grouping
      val pathSegements = if (o.endPoint.relativePathSegments.size <= config.swaggerApiGroupingPathSegment) {
        o.endPoint.relativePathSegments
      } else {
        o.endPoint.relativePathSegments.take(config.swaggerApiGroupingPathSegment)
      }

      // Only use static, non-variable, segments as the group by key
      val staticPathSegements: List[PathSegment] = pathSegements.takeWhile(ps => !ps.isVariable)
      staticPathSegements.map(ps => ps.name).mkString("/", "/", "")
    })

    // Resource Listing
    val resourceListing = SwaggerResourceListing(apisMap, config)
    result.put(config.rootPath + urlPath, jsonify(resourceListing))

    // API registrations
    val apiregistrations: Map[String, SwaggerApiDeclaration] = apisMap.map(f => {
      val (path, ops) = f
      val apiDec = SwaggerApiDeclaration(path, ops, config, rm)
      result.put(config.rootPath + urlPath + path, jsonify(apiDec))
      (path, apiDec)
    })

    // Finish
    SwaggerApiDocs(result.toMap)
  }

  private def jsonify(data: AnyRef): Array[Byte] = {
    if (data == null) Array.empty else {
      implicit val formats = json.formats(NoTypeHints)
      val s = json.write(data)
      s.getBytes(CharsetUtil.UTF_8)
    }

  }
}

trait SwaggerDoc

/**
 * Swagger [[https://github.com/wordnik/swagger-core/wiki/Resource-Listing resource listing]]
 */
case class SwaggerResourceListing(
  apiVersion: String,
  swaggerVersion: String,
  basePath: String,
  apis: Seq[SwaggerResourceListingApi]) extends SwaggerDoc

/**
 * Companion object
 */
object SwaggerResourceListing {

  /**
   * Creates a new [[org.mashupbots.socko.rest.SwaggerResourceListing]] for a group of APIs
   *
   * For example, the following operations are assumed to be in 2 resource groups: `/users` and `/pets`.
   * {{{
   * GET /users
   *
   * GET /pets
   * POST /pets/{id}
   * PUT /pets/{id}
   * }}}
   *
   * @param resources Operations grouped by their path. The grouping is specified in the key. For example,
   *   `/users` and `/pets`.
   * @param config REST configuration
   */
  def apply(resources: Map[String, Seq[RestOperation]], config: RestConfig): SwaggerResourceListing = {
    val resourceListingSwaggerApiPaths: Seq[String] = resources.keys.toSeq.sortBy(s => s)
    val resourceListingApis: Seq[SwaggerResourceListingApi] =
      resourceListingSwaggerApiPaths.map(s => SwaggerResourceListingApi(SwaggerApiDocs.urlPath + s, ""))
    val resourceListing = SwaggerResourceListing(
      config.apiVersion,
      config.swaggerVersion,
      config.rootApiUrl,
      resourceListingApis)

    resourceListing
  }
}

/**
 * Describes a specific resource in the resource listing
 */
case class SwaggerResourceListingApi(
  path: String,
  description: String)

/**
 * Swagger [[https://github.com/wordnik/swagger-core/wiki/API-Declaration API declaration]]
 */
case class SwaggerApiDeclaration(
  apiVersion: String,
  swaggerVersion: String,
  basePath: String,
  resourcePath: String,
  apis: Seq[SwaggerApiPath],
  models: Map[String, SwaggerModel]) extends SwaggerDoc

/**
 * Companion object
 */
object SwaggerApiDeclaration {

  /**
   * Creates a new [[org.mashupbots.socko.rest.SwaggerApiDeclaration]] for a resource path as listed in the
   * resource listing.
   *
   * For example, the following operations
   * {{{
   * POST /pets/{id}
   * PUT /pets/{id}
   * }}}
   *
   * maps to 1 SwaggerApiPath `/pets` with 2 operations: `POST` and `PUT`
   *
   * @param resourcePath Unique path. In the above example, it is `/pets/{id}`
   * @param ops HTTP method operations for that unique path
   * @param config REST configuration
   * @param rm Runtime mirror for reflecting meta data
   */
  def apply(resourcePath: String, ops: Seq[RestOperation], config: RestConfig, rm: ru.Mirror): SwaggerApiDeclaration = {
    // Context for this resource path
    val ctx = SwaggerContext(config, SwaggerModelRegistry(rm))

    // Group by path so we can list the operations
    val pathGrouping: Map[String, Seq[RestOperation]] = ops.groupBy(op => op.registration.path)

    // Map group to SwaggerApiPaths
    val apiPathsMap: Map[String, SwaggerApiPath] = pathGrouping.map(f => {
      val (path, ops) = f
      (path, SwaggerApiPath(path, ops, ctx))
    })

    // Convert to list and sort
    val apiPaths: Seq[SwaggerApiPath] = apiPathsMap.values.toSeq.sortBy(p => p.path)

    // Build registration
    SwaggerApiDeclaration(
      config.apiVersion,
      config.swaggerVersion,
      config.rootApiUrl,
      resourcePath,
      apiPaths,
      ctx.modelRegistry.models.toMap)
  }
}

/**
 * API path refers to a specific path and all the operations for that path
 */
case class SwaggerApiPath(
  path: String,
  operations: Seq[SwaggerApiOperation])

/**
 * Companion object
 */
object SwaggerApiPath {

  /**
   * Creates a new [[org.mashupbots.socko.rest.SwaggerApiPath]] for a given path
   *
   * For example, the following operations:
   * {{{
   * GET /pets/{id}
   * POST /pets/{id}
   * PUT /pets/{id}
   * }}}
   *
   * maps to 1 SwaggerApiPath `/pets` with 3 operations: `GET`, `POST` and `PUT`
   *
   * @param path Unique path
   * @param ops HTTP method operations for that unique path
   * @param ctx Processing context
   */
  def apply(path: String, ops: Seq[RestOperation], ctx: SwaggerContext): SwaggerApiPath = {
    val apiOps: Seq[SwaggerApiOperation] = ops.map(op => SwaggerApiOperation(op, ctx))
    SwaggerApiPath(
      path,
      apiOps.sortBy(f => f.httpMethod))
  }
}

/**
 * API operation refers to a specific HTTP operation that can be performed
 * for a path
 */
case class SwaggerApiOperation(
  httpMethod: String,
  summary: Option[String],
  notes: Option[String],
  deprecated: Option[Boolean],
  responseClass: String,
  nickname: String,
  parameters: Option[Seq[SwaggerApiParameter]],
  errorResponses: Option[Seq[SwaggerApiError]])

/**
 * Companion object
 */
object SwaggerApiOperation {

  /**
   * Creates a new [[org.mashupbots.socko.rest.SwaggerApiOperation]] from a [[org.mashupbots.socko.rest.RestOperation]]
   *
   * @param op Rest operation to document
   * @param ctx Processing context
   */
  def apply(op: RestOperation, ctx: SwaggerContext): SwaggerApiOperation = {
    val params: Seq[SwaggerApiParameter] = op.deserializer.requestParamBindings.map(b => SwaggerApiParameter(b, ctx))
    val errors: Seq[SwaggerApiError] = op.registration.errors.map(e => SwaggerApiError(e.code, e.reason)).toSeq

    val swaggerType: String = op.serializer.dataSerializer match {
      case s: VoidDataSerializer =>
        "void"
      case s: ObjectDataSerializer =>
        ctx.modelRegistry.register(s.tpe)
        SwaggerReflector.dataType(s.tpe)
      case s: PrimitiveDataSerializer =>
        SwaggerReflector.dataType(s.tpe)
      case s: ByteArrayDataSerializer =>
        SwaggerReflector.dataType(ru.typeOf[Array[Byte]])
      case s: ByteSeqDataSerializer =>
        SwaggerReflector.dataType(ru.typeOf[Seq[Byte]])
      case _ =>
        throw new IllegalStateException("Unsupported DataSerializer: " + op.serializer.dataSerializer.toString)
    }

    SwaggerApiOperation(
      op.registration.method.toString,
      if (op.registration.description.isEmpty) None else Some(op.registration.description),
      if (op.registration.notes.isEmpty) None else Some(op.registration.notes),
      if (op.registration.deprecated) Some(true) else None,
      swaggerType,
      op.registration.name,
      if (params.isEmpty) None else Some(params),
      if (errors.isEmpty) None else Some(errors.sortBy(e => e.code)))
  }
}

/**
 * API [[https://github.com/wordnik/swagger-core/wiki/Parameters parameter]] refers to a path, body, query string or
 * header parameter in a [[org.mashupbots.socko.rest.SwaggerApiOperation]]
 */
case class SwaggerApiParameter(
  name: String,
  description: Option[String],
  paramType: String,
  dataType: String,
  required: Option[Boolean],
  allowableValues: Option[AllowableValues],
  allowMultiple: Option[Boolean])

/**
 * Companion object
 */
object SwaggerApiParameter {

  /**
   * Creates a new [[org.mashupbots.socko.rest.SwaggerApiParameter]] for a [[org.mashupbots.socko.rest.SwaggerApiParameter]]
   *
   * @param binding parameter binding
   * @param ctx Processing context
   */
  def apply(binding: RequestParamBinding, ctx: SwaggerContext): SwaggerApiParameter = {
    val swaggerParamType: String = binding match {
      case _: PathBinding => "path"
      case _: QueryStringBinding => "query"
      case _: HeaderBinding => "header"
      case _: BodyBinding =>
        ctx.modelRegistry.register(binding.tpe)
        "body"
      case _ => throw new IllegalStateException("Unsupported RequestParamBinding: " + binding.toString)
    }

    val swaggerDataType: String = SwaggerReflector.dataType(binding.tpe)

    SwaggerApiParameter(
      binding.registration.name,
      if (binding.registration.description.isEmpty()) None else Some(binding.registration.description),
      swaggerParamType,
      swaggerDataType,
      if (binding.required) Some(true) else None,
      binding.registration.allowableValues,
      if (binding.registration.allowMultiple) Some(true) else None)
  }
}

/**
 * API error refers to the HTTP response status code and its description
 */
case class SwaggerApiError(code: Int, reason: String)

/**
 * A swagger model complex data type's properties
 *
 * @param type Swagger data type
 * @param description Description of the property
 * @param required Boolean to indicate if the property is required. If `None`, `false` is assumed.
 * @param allowableValues Optional allowable list or range of values
 * @param items Only applicable for containers. Defines the data type of items in a container.
 *   For primitives, it is `"type":"string"`.  For complex types, it is `"ref":"Category"`.
 */
case class SwaggerModelProperty(
  `type`: String,
  description: Option[String],
  required: Option[Boolean],
  allowableValues: Option[AllowableValues],
  items: Option[Map[String, String]])

/**
 * A swagger model complex data type
 *
 * @param id Unique id
 * @param description description
 * @param properties List of properties
 */
case class SwaggerModel(
  id: String,
  description: Option[String],
  properties: Map[String, SwaggerModelProperty])

/**
 * Registry of swagger models. Makes sure that we don't output a model more than once.
 *
 * @param rm Runtime Mirror
 */
case class SwaggerModelRegistry(rm: ru.Mirror) {
  val models: HashMap[String, SwaggerModel] = new HashMap[String, SwaggerModel]()

  val typeRestModelMetaData = ru.typeOf[RestModelMetaData]

  /**
   * Registers a complex type in the swagger model
   */
  private def registerComplexType(tpe: ru.Type) = {
    // If this is an option, get the base type
    val thisType = SwaggerReflector.optionContentType(tpe)

    // Add to model if not already added
    val name = SwaggerReflector.dataType(thisType)
    if (!models.contains(name)) {
      // Sub complex types to that may also need reflecting
      val subModels = collection.mutable.Set[ru.Type]()

      // Get properties meta data
      val propertiesMetaData = locatePropertiesMetaData(thisType)

      // Get properties of this model
      val properties: Map[String, SwaggerModelProperty] =
        thisType.members
          .filter(s => s.isTerm && !s.isMethod && !s.isMacro)
          .map(s => {
            val dot = s.fullName.lastIndexOf('.')
            val termName = if (dot > 0) s.fullName.substring(dot + 1) else s.fullName
            val required = !(s.typeSignature <:< SwaggerReflector.optionAnyRefType)
            val termRequired = if (required) Some(true) else None
            val (termType: String, items: Map[String, String]) = parsePropertyType(s.typeSignature, subModels)
            val termItems = if (items.isEmpty) None else Some(items)

            val metaData: Option[RestPropertyMetaData] = propertiesMetaData.find(p => p.name == termName)
            val description: Option[String] =
              if (metaData.isEmpty) None
              else if (metaData.get.description.isEmpty) None
              else Some(metaData.get.description)
            val allowableValues: Option[AllowableValues] = if (metaData.isEmpty) None else metaData.get.allowableValues

            // Return name-value for map
            (termName, SwaggerModelProperty(termType, description, termRequired, allowableValues, termItems))
          }).toMap

      // Add model to registry
      val model = SwaggerModel(name, None, properties)
      models.put(name, model)

      // Add sub-models - we do this after we add to model to cyclical entries
      subModels.foreach(sm => register(sm))
    }
  }

  /**
   * Parse the type of a property and output the Swagger API details
   *
   * @param tpe Type to parse
   * @param subModels Complex Types and Complex Types within a container are added to this set so that
   *   they can be registered as a model object for swagger output.
   * @return Tuple of swagger data type name and map of swagger container content type. Non container classes
   *   returns an empty map of container content type.
   */
  private def parsePropertyType(tpe: ru.Type, subModels: collection.mutable.Set[ru.Type]): (String, Map[String, String]) = {
    if (SwaggerReflector.isPrimitive(tpe)) {
      // Primitive
      (SwaggerReflector.dataType(tpe), Map.empty[String, String])
    } else {
      val containerType = SwaggerReflector.containerType(tpe)
      if (containerType == "") {
        // Complex
        subModels.add(tpe)
        (SwaggerReflector.dataType(tpe), Map.empty[String, String])
      } else {
        // Container
        val contentType = SwaggerReflector.containerContentType(tpe)
        if (SwaggerReflector.isPrimitive(contentType)) {
          // Container of primitives
          (containerType, Map[String, String]("type" -> SwaggerReflector.dataType(contentType)))
        } else {
          // Container of complex types
          subModels.add(contentType)
          (containerType, Map[String, String]("$ref" -> SwaggerReflector.dataType(contentType)))
        }
      }
    }
  }

  /**
   * Finds the companion object of `tpe` and if it extends [[org.mashupbots.socko.rest.RestModelMetaData]],
   * `modelProperties` is returned.
   *
   * @param tpe Type to investigate
   * @return Sequence of [[org.mashupbots.socko.rest.RestModelMetaData]]. Empty if no extra meta data found.
   */
  private def locatePropertiesMetaData(tpe: ru.Type): Seq[RestPropertyMetaData] = {
    val cs = tpe.typeSymbol.asClass
    val companionModuleSymbol = cs.companionSymbol.asModule
    val moduleType = companionModuleSymbol.typeSignature
    if (moduleType <:< typeRestModelMetaData) {
      val moduleMirror = rm.reflectModule(companionModuleSymbol)
      val companionObj = moduleMirror.instance.asInstanceOf[RestModelMetaData]
      companionObj.modelProperties
    } else {
      Seq.empty
    }
  }

  /**
   * Determines if we need to register a type as a model. Primitives are ignored.
   *
   * @param tpe Type to register
   */
  def register(tpe: ru.Type): Unit = {
    if (SwaggerReflector.isPrimitive(tpe))
      // Ignore primitive
      Unit
    else {
      val containerType = SwaggerReflector.containerType(tpe)
      if (containerType == "") {
        // Register complex type
        registerComplexType(tpe)
      } else {
        // Container type
        val contentType = SwaggerReflector.containerContentType(tpe)
        if (SwaggerReflector.isPrimitive(contentType))
          // Ignore primitive containers
          Unit
        else
          // Register Container of complex types
          registerComplexType(contentType)
      }
    }
  }
}

case class SwaggerContext(config: RestConfig, modelRegistry: SwaggerModelRegistry)

    
                      




© 2015 - 2025 Weber Informatics LLC | Privacy Policy