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

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

The newest version!
package endpoints4s.openapi

import java.util.UUID

import endpoints4s.{ujson => _, _}
import endpoints4s.algebra.Documentation
import endpoints4s.openapi.model.Schema
import scala.collection.compat.Factory

/** Interpreter for [[algebra.Urls]]
  *
  * @group interpreters
  */
trait Urls extends algebra.Urls {

  type QueryString[A] = DocumentedQueryString

  /** @param parameters List of query string parameters
    */
  case class DocumentedQueryString(parameters: List[DocumentedParameter])

  /** @param name Name of the parameter
    * @param required Whether this parameter is required or not (MUST be true for path parameters)
    */
  case class DocumentedParameter(
      name: String,
      required: Boolean,
      description: Option[String],
      schema: Schema
  )

  def combineQueryStrings[A, B](first: QueryString[A], second: QueryString[B])(implicit
      tupler: Tupler[A, B]
  ): QueryString[tupler.Out] =
    DocumentedQueryString(first.parameters ++ second.parameters)

  def qs[A](name: String, docs: Documentation)(implicit
      value: QueryStringParam[A]
  ): QueryString[A] =
    DocumentedQueryString(
      List(
        DocumentedParameter(
          name,
          required = value.isRequired,
          docs,
          value.schema
        )
      )
    )

  type WithDefault[A] = A

  override def optQsWithDefault[A](name: String, default: A, docs: Documentation = None)(implicit
      value: QueryStringParam[A]
  ): QueryString[WithDefault[A]] =
    DocumentedQueryString(
      List(
        DocumentedParameter(
          name,
          required = false,
          docs,
          schema = {
            val defaultJson = value.encoder.encode(default)
            value.schema.withDefinedDefault(defaultJson)
          }
        )
      )
    )

  /** A query string parameter documentation description for type `A`
    *
    * @param schema Schema that corresponds to type `A`
    * @param isRequired Whether the query string parameter is required
    * @param encoder JSON encoder for query string parameter value, primarily
    * used for encoding of the parameter's default value.
    * Result of the encoding is optional since the empty value of optional
    * query string parameter should result in the empty encoded value.
    */
  case class DocumentedQueryStringParam[A](
      schema: Schema,
      isRequired: Boolean,
      encoder: Encoder[A, Option[ujson.Value]]
  )
  type QueryStringParam[A] = DocumentedQueryStringParam[A]

  implicit def optionalQueryStringParam[A](implicit
      param: QueryStringParam[A]
  ): QueryStringParam[Option[A]] =
    param.copy(
      isRequired = false,
      encoder = _.flatMap(param.encoder.encode)
    )

  implicit def repeatedQueryStringParam[A, CC[X] <: Iterable[X]](implicit
      param: QueryStringParam[A],
      factory: Factory[A, CC[A]]
  ): QueryStringParam[CC[A]] =
    DocumentedQueryStringParam(
      Schema.Array(
        Left(param.schema),
        description = None,
        example = None,
        title = None
      ),
      isRequired = false,
      encoder = cc => Some(ujson.Arr(cc.flatMap(param.encoder.encode)))
    )

  implicit lazy val queryStringPartialInvariantFunctor: PartialInvariantFunctor[QueryString] =
    new PartialInvariantFunctor[QueryString] {
      def xmapPartial[A, B](
          fa: QueryString[A],
          f: A => Validated[B],
          g: B => A
      ): QueryString[B] = fa
    }

  implicit lazy val queryStringParamPartialInvariantFunctor
      : PartialInvariantFunctor[QueryStringParam] =
    new PartialInvariantFunctor[QueryStringParam] {
      def xmapPartial[A, B](
          fa: QueryStringParam[A],
          f: A => Validated[B],
          g: B => A
      ): QueryStringParam[B] = new DocumentedQueryStringParam[B](
        schema = fa.schema,
        isRequired = fa.isRequired,
        encoder = b => fa.encoder.encode(g(b))
      )
    }

  def stringQueryString: QueryStringParam[String] =
    DocumentedQueryStringParam(Schema.simpleString, isRequired = true, a => Some(ujson.Str(a)))

  override def uuidQueryString: QueryStringParam[UUID] =
    DocumentedQueryStringParam(
      Schema.simpleUUID,
      isRequired = true,
      u => Some(ujson.Str(u.toString))
    )

  override def intQueryString: QueryStringParam[Int] =
    DocumentedQueryStringParam(
      Schema.simpleInteger,
      isRequired = true,
      n => Some(ujson.Num(n.toDouble))
    )

  override def longQueryString: QueryStringParam[Long] =
    DocumentedQueryStringParam(
      Schema.simpleLong,
      isRequired = true,
      n => Some(ujson.Num(n.toDouble))
    )

  override def booleanQueryString: QueryStringParam[Boolean] =
    DocumentedQueryStringParam(Schema.simpleBoolean, isRequired = true, a => Some(ujson.Bool(a)))

  override def doubleQueryString: QueryStringParam[Double] =
    DocumentedQueryStringParam(Schema.simpleNumber, isRequired = true, a => Some(ujson.Num(a)))

  type Segment[A] = Schema

  implicit lazy val segmentPartialInvariantFunctor: PartialInvariantFunctor[Segment] =
    new PartialInvariantFunctor[Segment] {
      def xmapPartial[A, B](
          fa: Segment[A],
          f: A => Validated[B],
          g: B => A
      ): Segment[B] = fa
    }

  def stringSegment: Segment[String] = Schema.simpleString

  override def uuidSegment: Segment[UUID] = Schema.simpleUUID

  override def intSegment: Segment[Int] = Schema.simpleInteger

  override def longSegment: Segment[Long] = Schema.simpleLong

  override def doubleSegment: Segment[Double] = Schema.simpleNumber

  type Path[A] = DocumentedUrl

  implicit lazy val pathPartialInvariantFunctor: PartialInvariantFunctor[Path] =
    new PartialInvariantFunctor[Path] {
      def xmapPartial[A, B](
          fa: Path[A],
          f: A => Validated[B],
          g: B => A
      ): Path[B] = fa
    }

  def staticPathSegment(segment: String): Path[Unit] =
    DocumentedUrl(Left(segment) :: Nil, Nil)

  def chainPaths[A, B](first: Path[A], second: Path[B])(implicit
      tupler: Tupler[A, B]
  ): Path[tupler.Out] =
    DocumentedUrl(
      first.path ++ second.path,
      first.queryParameters ++ second.queryParameters // (In practice this should be empty…)
    )

  def segment[A](name: String, docs: Documentation)(implicit
      A: Segment[A]
  ): Path[A] = {
    DocumentedUrl(
      Right(DocumentedParameter(name, required = true, docs, A)) :: Nil,
      Nil
    )
  }

  def remainingSegments(name: String, docs: Documentation): Path[String] = {
    // The OpenAPI specification does not support path parameters containing slashes
    // See https://github.com/OAI/OpenAPI-Specification/issues/1459
    // Consequently, we *incorrectly* document it as a *single* string segment
    // TODO We should at least show a warning, but the error can only be detected
    // at runtime (when one uses the `remainingSegments` method *and* the openapi
    // interpreter). So, the best we could do is to log a warning.
    segment(name, docs)(stringSegment)
  }

  type Url[A] = DocumentedUrl

  implicit lazy val urlPartialInvariantFunctor: PartialInvariantFunctor[Url] =
    new PartialInvariantFunctor[Url] {
      def xmapPartial[A, B](
          fa: Url[A],
          f: A => Validated[B],
          g: B => A
      ): Url[B] = fa
    }

  /** @param path List of path segments. Left is a static segment, right is a path parameter
    * @param queryParameters Query string parameters
    */
  case class DocumentedUrl(
      path: List[Either[String, DocumentedParameter]],
      queryParameters: List[DocumentedParameter]
  )

  def urlWithQueryString[A, B](path: Path[A], qs: QueryString[B])(implicit
      tupler: Tupler[A, B]
  ): Url[tupler.Out] =
    path.copy(queryParameters = path.queryParameters ++ qs.parameters)

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy