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

endpoints4s.openapi.Endpoints.scala Maven / Gradle / Ivy

There is a newer version: 5.0.1
Show newest version
package endpoints4s
package openapi

import endpoints4s.openapi.model._
import scala.collection.mutable

/** Interpreter for [[algebra.Endpoints]] that produces an [[endpoints4s.openapi.model.OpenApi]] instance for endpoints,
  * and uses [[algebra.BuiltInErrors]] to model client and server errors.
  *
  * @group interpreters
  */
trait Endpoints extends algebra.Endpoints with EndpointsWithCustomErrors with BuiltInErrors

/** Interpreter for [[algebra.Endpoints]] that produces an [[endpoints4s.openapi.model.OpenApi]] instance for endpoints.
  *
  * @group interpreters
  */
trait EndpointsWithCustomErrors
    extends algebra.EndpointsWithCustomErrors
    with Requests
    with Responses {

  /** @return An `OpenApi` instance for the given endpoint descriptions
    * @param info      General information about the documentation to generate
    * @param endpoints The endpoints to generate the documentation for
    */
  def openApi(info: Info)(endpoints: DocumentedEndpoint*): OpenApi = {
    val pathItems = mutable.LinkedHashMap.empty[String, PathItem]
    for (e <- endpoints) {
      val key = e.path
      val newValue = pathItems.get(key) match {
        case Some(current) =>
          PathItem(collection.immutable.ListMap(current.operations.toSeq: _*) ++ e.item.operations)
        case None => PathItem(collection.immutable.ListMap(e.item.operations.toSeq: _*))
      }
      pathItems.update(key, newValue)
    }

    val components = Components(
      schemas = captureSchemas(endpoints),
      securitySchemes = captureSecuritySchemes(endpoints)
    )
    OpenApi(info, pathItems, components)
  }

  type Endpoint[A, B] = DocumentedEndpoint

  case class DocumentedEndpoint(
      request: DocumentedRequest,
      response: List[DocumentedResponse],
      securityRequirements: Seq[SecurityRequirement],
      docs: EndpointDocs
  ) {

    def withSecurityRequirements(securityRequirements: SecurityRequirement*): DocumentedEndpoint =
      copy(securityRequirements = securityRequirements)

    private lazy val correctPathSegments: List[Either[String, DocumentedParameter]] = {
      val prefix = "_arg"
      request.url.path
        .foldLeft((0, List[Either[String, DocumentedParameter]]())) { // (num of empty-named args, new args list)
          case ((nextEmptyArgNum, args), Right(arg)) if arg.name.isEmpty =>
            val renamed = arg.copy(name = s"$prefix$nextEmptyArgNum")
            (nextEmptyArgNum + 1, Right(renamed) :: args)
          case ((x, args), elem) => (x, elem :: args)
        }
        ._2
        .reverse
    }

    lazy val item: PathItem = {
      val method =
        request.method match {
          case Get     => "get"
          case Put     => "put"
          case Post    => "post"
          case Delete  => "delete"
          case Options => "options"
          case Patch   => "patch"
        }

      val pathParams = correctPathSegments.collect { case Right(param) => param }
      val parameters =
        pathParams.map(p => Parameter(p.name, In.Path, p.required, p.description, p.schema)) ++
          request.url.queryParameters.map(p =>
            Parameter(p.name, In.Query, p.required, p.description, p.schema)
          ) ++
          request.headers.value.map(h =>
            Parameter(
              h.name,
              In.Header,
              required = h.required,
              h.description,
              h.schema
            )
          )
      def responseHeaders(
          documentedHeaders: DocumentedHeaders
      ): Map[String, ResponseHeader] =
        documentedHeaders.value.map { header =>
          header.name -> ResponseHeader(
            header.required,
            header.description,
            header.schema
          )
        }.toMap
      lazy val responses =
        (clientErrorsResponse ++ serverErrorResponse ++ response)
          .map(r =>
            r.status.toString() -> Response(
              r.documentation,
              responseHeaders(r.headers),
              r.content
            )
          )
          .toMap

      val operation =
        Operation(
          docs.operationId,
          docs.summary,
          docs.description,
          parameters,
          if (request.entity.isEmpty) None
          else Some(RequestBody(request.documentation, request.entity)),
          responses,
          docs.tags,
          security = securityRequirements.toList,
          docs.callbacks.map { case (event, callbacks) =>
            val items = callbacks.map { case (urlPattern, callback) =>
              val method = callback.method.toString.toLowerCase
              val requestBody =
                RequestBody(callback.requestDocs, callback.entity.value)
              val responses =
                callback.response.value
                  .map(r =>
                    r.status.toString() -> Response(
                      r.documentation,
                      responseHeaders(r.headers),
                      r.content
                    )
                  )
                  .toMap
              val callbackOperation =
                Operation(
                  None,
                  None,
                  None,
                  Nil,
                  Some(requestBody),
                  responses,
                  Nil,
                  Nil,
                  Map.empty,
                  deprecated = false
                )
              (urlPattern, PathItem(Map(method -> callbackOperation)))
            }
            (event, items)
          },
          docs.deprecated
        )

      PathItem(Map(method -> operation))
    }
    lazy val path: String = correctPathSegments
      .map {
        case Left(str)    => str
        case Right(param) => s"{${param.name}}"
      }
      .mkString("/")
  }

  def endpoint[A, B](
      request: Request[A],
      response: Response[B],
      docs: EndpointDocs = EndpointDocs()
  ): Endpoint[A, B] =
    DocumentedEndpoint(request, response, Nil, docs)

  override def mapEndpointRequest[A, B, C](
      currentEndpoint: Endpoint[A, B],
      func: Request[A] => Request[C]
  ): Endpoint[C, B] = currentEndpoint.copy(request = func(currentEndpoint.request))

  override def mapEndpointResponse[A, B, C](
      currentEndpoint: Endpoint[A, B],
      func: Response[B] => Response[C]
  ): Endpoint[A, C] = currentEndpoint.copy(response = func(currentEndpoint.response))

  override def mapEndpointDocs[A, B](
      currentEndpoint: Endpoint[A, B],
      func: EndpointDocs => EndpointDocs
  ): Endpoint[A, B] =
    currentEndpoint.copy(docs = func(currentEndpoint.docs))

  private def captureSchemas(
      endpoints: Iterable[DocumentedEndpoint]
  ): Map[String, Schema] = {

    val allReferencedSchemas = for {
      documentedEndpoint <- endpoints
      operations = documentedEndpoint.item.operations.values
      operation <- operations ++ operations.flatMap(
        _.callbacks.values.flatMap(_.values.flatMap(_.operations.values))
      )
      operationParametersSchemas = operation.parameters.map(_.schema)
      requestBodySchema = for {
        body <- operation.requestBody.toList
        mediaType <- body.content.values
        schema <- mediaType.schema.toList
      } yield schema
      responseSchemas = for {
        (_, response) <- operation.responses.toSeq
        (_, mediaType) <- response.content.toSeq
        schema <-
          mediaType.schema.toList ++ response.headers.values
            .map(_.schema)
      } yield schema
      schema <- requestBodySchema ++ responseSchemas ++ operationParametersSchemas
      recSchema <- captureReferencedSchemasRec(schema)
    } yield recSchema

    allReferencedSchemas.collect {
      case ref: Schema.Reference if ref.original.isDefined =>
        ref.name -> ref.original.get
    }.toMap
  }

  private def captureReferencedSchemasRec(
      schema: Schema
  ): Seq[Schema.Reference] =
    schema match {
      case obj: Schema.Object =>
        obj.properties.map(_.schema).flatMap(captureReferencedSchemasRec) ++
          obj.additionalProperties.toList.flatMap(captureReferencedSchemasRec)
      case array: Schema.Array =>
        array.elementType match {
          case Left(elementType) =>
            captureReferencedSchemasRec(elementType)
          case Right(elementTypes) =>
            elementTypes.flatMap(captureReferencedSchemasRec)
        }
      case enm: Schema.Enum =>
        captureReferencedSchemasRec(enm.elementType)
      case _: Schema.Primitive =>
        Nil
      case oneOf: Schema.OneOf =>
        val alternativeSchemas =
          oneOf.alternatives match {
            case discAlternatives: Schema.DiscriminatedAlternatives =>
              discAlternatives.alternatives.map(_._2)
            case enumAlternatives: Schema.EnumeratedAlternatives =>
              enumAlternatives.alternatives
          }
        alternativeSchemas.flatMap(captureReferencedSchemasRec)
      case allOf: Schema.AllOf =>
        allOf.schemas.flatMap {
          case _: Schema.Reference => Nil
          case s                   => captureReferencedSchemasRec(s)
        }
      case referenced: Schema.Reference =>
        referenced +: referenced.original
          .map(captureReferencedSchemasRec)
          .getOrElse(Nil)
    }

  private def captureSecuritySchemes(
      endpoints: Iterable[DocumentedEndpoint]
  ): Map[String, SecurityScheme] = {
    endpoints
      .flatMap(_.item.operations.values)
      .flatMap(_.security)
      .map(s => s.name -> s.scheme)
      .toMap
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy