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

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

package endpoints4s.algebra

import java.util.UUID

import endpoints4s.{Codec, PartialInvariantFunctor, PartialInvariantFunctorSyntax, Tupler}

import scala.collection.compat.Factory

/** Algebra interface for describing URLs made of a path and a query string.
  *
  * A path is itself made of segments chained together.
  *
  * A query string is made of named parameters.
  *
  * {{{
  *   /**
  *     * Describes an URL starting with a segment containing “articles”, followed
  *     * by another `String` segment, and a query string containing
  *     * a mandatory `Lang` parameter named “lang”, and an
  *     * optional `Int` parameter named “page”.
  *     *
  *     * Examples of matching URLs:
  *     *
  *     * - /articles/kitchen?lang=fr
  *     * - /articles/garden?lang=en&page=2
  *     */
  *   val example = path / "articles" / segment[String]() /? (qs[Lang]("lang") & qs[Option[Int]]("page"))
  * }}}
  *
  * @group algebras
  * @groupname types Types
  * @groupdesc types Types introduced by the algebra
  * @groupprio types 1
  * @groupname operations Operations
  * @groupdesc operations Operations creating and transforming values
  * @groupprio operations 2
  */
trait Urls extends PartialInvariantFunctorSyntax {

  /** A query string carrying an `A` information
    *
    * QueryString values can be created with the [[qs]] operation,
    * and can be combined with the `&` operation:
    *
    * {{{
    *   val queryPageAndLang: QueryString[(Int, Option[String])] =
    *     qs[Int]("page") & qs[Option[String]]("lang")
    * }}}
    *
    *   - Server interpreters raise an error if they can’t parse the incoming
    *     request query string parameters as a value of type `A`. By default,
    *     they produce a Bad Request (400) response with a list of error messages
    *     in a JSON array. Refer to the documentation of your server interpreter
    *     to customize this behavior.
    *
    * @note  This type has implicit methods provided by the [[QueryStringSyntax]],
    *        [[InvariantFunctorSyntax]], and the [[PartialInvariantFunctorSyntax]] classes.
    * @group types
    */
  type QueryString[A]

  /** Provides `xmap` and `xmapPartial` operations.
    * @see [[PartialInvariantFunctorSyntax]] and [[InvariantFunctorSyntax]]
    */
  implicit def queryStringPartialInvariantFunctor: PartialInvariantFunctor[QueryString]

  /** Extension methods on [[QueryString]].
    * @group operations
    */
  implicit class QueryStringSyntax[A](first: QueryString[A]) {

    /** Convenient method to concatenate two [[QueryString]]s.
      *
      * {{{
      *   qs[Int]("foo") & qs[String]("baz")
      * }}}
      *
      * @param second `QueryString` to concatenate with this one
      * @tparam B Information carried by the second `QueryString`
      * @return A `QueryString` that carries both `A` and `B` information
      */
    final def &[B](
        second: QueryString[B]
    )(implicit tupler: Tupler[A, B]): QueryString[tupler.Out] =
      combineQueryStrings(first, second)
  }

  /** Concatenates two `QueryString`s */
  def combineQueryStrings[A, B](first: QueryString[A], second: QueryString[B])(implicit
      tupler: Tupler[A, B]
  ): QueryString[tupler.Out]

  /** Builds a `QueryString` with one parameter.
    *
    * Examples:
    *
    * {{{
    *   qs[Int]("page")            // mandatory `page` parameter
    *   qs[Option[String]]("lang") // optional `lang` parameter
    *   qs[List[Long]]("id")       // repeated `id` parameter
    * }}}
    *
    * @param name Parameter’s name
    * @tparam A Type of the value carried by the parameter
    * @group operations
    */
  def qs[A](name: String, docs: Documentation = None)(implicit
      value: QueryStringParam[A]
  ): QueryString[A]

  /** This type is necessary to express different perspectives of servers and clients
    * on optional query string parameters with default value:
    *   - Client interpreters should define it as `Option[A]` and omit query string parameters with default value
    *     that are empty
    *   - Server interpreters should define it as `A` and accept incoming requests whose query string parameters
    *     with default value are missing, while providing the defined default value
    *   - Documentation interpreters should mark the parameter as optional and document the provided default value
    *
    * @group types
    */
  type WithDefault[A]

  /** Builds a `QueryString` with one optional parameter, which has a default value.
    *
    * Examples:
    *
    * {{{
    *   optQsWithDefault[Int]("page", 1) // optional `page` parameter, with default value 1
    * }}}
    *
    * @param name Parameter’s name
    * @tparam A Type of the value carried by the parameter
    * @group operations
    */
  def optQsWithDefault[A](name: String, default: A, docs: Documentation = None)(implicit
      value: QueryStringParam[A]
  ): QueryString[WithDefault[A]] = unsupportedInterpreter("1.6.0")

  /** Make a query string parameter optional:
    *
    * {{{
    *   path / "articles" /? qs[Option[Int]]("page")
    * }}}
    *
    *   - Client interpreters must omit optional query string parameters that are empty.
    *   - Server interpreters must accept incoming requests whose optional query string
    *     parameters are missing, and they must report a failure for incoming requests
    *     whose optional query string parameters are present, but malformed,
    *   - Documentation interpreters should mark the parameter as optional.
    *
    * @group operations
    */
  implicit def optionalQueryStringParam[A: QueryStringParam]: QueryStringParam[Option[A]]

  /** Support query string parameters with multiple values:
    *
    * {{{
    *   path / "articles" /? qs[List[Long]]("id")
    * }}}
    *
    *   - Server interpreters must accept incoming requests where such parameters are
    *     missing (in such a case, its value is an empty collection), and report a
    *     failure if at least one value is malformed.
    *
    * @group operations
    */
  implicit def repeatedQueryStringParam[A: QueryStringParam, CC[X] <: Iterable[
    X
  ]](implicit factory: Factory[A, CC[A]]): QueryStringParam[CC[A]]

  /** A query string parameter codec for type `A`.
    *
    * The trait `Urls` provides implicit instances of type `QueryStringParam[A]`
    * for basic types (e.g., `Int`, `String`, etc.). You can create additional
    * instances by transforming or refining the existing instances with `xmap`
    * and `xmapPartial`.
    *
    * @note  This type has implicit methods provided by the [[PartialInvariantFunctorSyntax]]
    *        and the [[InvariantFunctorSyntax]] classes.
    * @group types
    */
  type QueryStringParam[A]

  /** Provides `xmap` and `xmapPartial` operations.
    * @see [[PartialInvariantFunctorSyntax]] and [[InvariantFunctorSyntax]]
    */
  implicit def queryStringParamPartialInvariantFunctor: PartialInvariantFunctor[QueryStringParam]

  /** Ability to define `String` query string parameters
    * @group operations
    */
  implicit def stringQueryString: QueryStringParam[String]

  /** Ability to define `UUID` query string parameters
    * @group operations
    */
  implicit def uuidQueryString: QueryStringParam[UUID] =
    stringQueryString.xmapWithCodec(Codec.uuidCodec)

  /** Ability to define `Int` query string parameters
    * @group operations
    */
  implicit def intQueryString: QueryStringParam[Int] =
    stringQueryString.xmapWithCodec(Codec.intCodec)

  /** Query string parameter containing a `Long` value
    * @group operations
    */
  implicit def longQueryString: QueryStringParam[Long] =
    stringQueryString.xmapWithCodec(Codec.longCodec)

  /** Query string parameter containing a `Boolean` value
    * @group operations
    */
  implicit def booleanQueryString: QueryStringParam[Boolean] =
    stringQueryString.xmapWithCodec(Codec.booleanCodec)

  /** Codec for query string parameters of type `Double`
    * @group operations
    */
  implicit def doubleQueryString: QueryStringParam[Double] =
    stringQueryString.xmapWithCodec(Codec.doubleCodec)

  /** An URL path segment codec for type `A`.
    *
    * The trait `Urls` provides implicit instances of `Segment[A]` for basic types
    * (e.g., `Int`, `String`, etc.). You can create additional instances by transforming
    * or refining the existing instances with `xmap` and `xmapPartial`.
    *
    * @note  This type has implicit methods provided by the [[PartialInvariantFunctorSyntax]]
    *        and the [[InvariantFunctorSyntax]] classes.
    * @group types
    */
  type Segment[A]

  /** Provides `xmap` and `xmapPartial` operations.
    * @see [[PartialInvariantFunctorSyntax]] and [[InvariantFunctorSyntax]]
    */
  implicit def segmentPartialInvariantFunctor: PartialInvariantFunctor[Segment]

  /** Path segment codec for type `String`
    *
    *   - Server interpreters should return an URL-decoded string value,
    *   - Client interpreters should take an URL-decoded string value.
    * @group operations
    */
  implicit def stringSegment: Segment[String]

  /** Path segment codec for type `UUID`
    * @group operations
    */
  implicit def uuidSegment: Segment[UUID] =
    stringSegment.xmapWithCodec(Codec.uuidCodec)

  /** Path segment codec for type `Int`
    * @group operations
    */
  implicit def intSegment: Segment[Int] =
    stringSegment.xmapWithCodec(Codec.intCodec)

  /** Path segment codec for type `Long`
    * @group operations
    */
  implicit def longSegment: Segment[Long] =
    stringSegment.xmapWithCodec(Codec.longCodec)

  /** Path segment codec for type `Double`
    * @group operations
    */
  implicit def doubleSegment: Segment[Double] =
    stringSegment.xmapWithCodec(Codec.doubleCodec)

  /** An URL path carrying an `A` information
    *
    * Values of type `Path[A]` can be constructed by the operations [[path]],
    * [[segment]], and [[remainingSegments]].
    *
    * {{{
    *   path / "user" / segment[UUID]("id")
    * }}}
    *
    *   - Server interpreters raise an error if they can’t parse the incoming
    *     request path as a value of type `A`. By default,
    *     they produce a Bad Request (400) response with a list of error messages
    *     in a JSON array. Refer to the documentation of your server interpreter
    *     to customize this behavior.
    *
    * @note  This type has implicit methods provided by the [[PathOps]],
    *        [[InvariantFunctorSyntax]], and the [[PartialInvariantFunctorSyntax]] classes.
    * @group types
    */
  type Path[A] <: Url[A]

  /** Provides `xmap` and `xmapPartial` operations.
    * @see [[PartialInvariantFunctorSyntax]] and [[InvariantFunctorSyntax]]
    */
  implicit def pathPartialInvariantFunctor: PartialInvariantFunctor[Path]

  /** Implicit conversion to get rid of intellij errors when defining paths. Effectively should not be called.
    * @see [[https://youtrack.jetbrains.com/issue/SCL-16284]]
    */
  private[endpoints4s] implicit def dummyPathToUrl[A](p: Path[A]): Url[A] = p

  /** Convenient methods for [[Path]]s.
    * @group operations
    */
  implicit class PathOps[A](first: Path[A]) {

    /** Chains this path with the `second` constant path segment */
    final def /(second: String): Path[A] =
      chainPaths(first, staticPathSegment(second))

    /** Chains this path with the `second` path segment */
    final def /[B](second: Path[B])(implicit
        tupler: Tupler[A, B]
    ): Path[tupler.Out] = chainPaths(first, second)

    /** Chains this path with the given [[QueryString]] */
    final def /?[B](qs: QueryString[B])(implicit
        tupler: Tupler[A, B]
    ): Url[tupler.Out] = urlWithQueryString(first, qs)
  }

  /** A path segment whose value is the given `segment`
    * @group operations
    */
  def staticPathSegment(segment: String): Path[Unit]

  /** A path segment carrying an `A` information
    * @group operations
    */
  def segment[A](name: String = "", docs: Documentation = None)(implicit
      s: Segment[A]
  ): Path[A]

  /** The remaining segments of the path. The `String` value carried by this `Path` is still URL-encoded.
    * @group operations
    */
  def remainingSegments(
      name: String = "",
      docs: Documentation = None
  ): Path[String] // TODO Make it impossible to chain it with another path (ie, `path / remainingSegments() / "foo"` should not compile)

  /** Chains the two paths */
  def chainPaths[A, B](first: Path[A], second: Path[B])(implicit
      tupler: Tupler[A, B]
  ): Path[tupler.Out]

  /** An empty path.
    *
    * Useful to begin a path definition:
    *
    * {{{
    *   path / "foo" / segment[Int] /? qs[String]("bar")
    * }}}
    *
    * @group operations
    */
  val path: Path[Unit] = staticPathSegment("")

  /** An URL carrying an `A` information
    *
    * Values of type `URL[A]` are typically constructed by first using the [[path]]
    * constructor and then chaining it with segments and query parameters.
    *
    * {{{
    *   path / "users" / segment[UUID]("id") /? qs[String]("apiKey")
    * }}}
    *
    *   - Server interpreters raise an error if they can’t parse the incoming
    *     request URL as a value of type `A`. By default,
    *     they produce a Bad Request (400) response with a list of error messages
    *     in a JSON array. Refer to the documentation of your server interpreter
    *     to customize this behavior.
    *
    * @note  This type has implicit methods provided by the [[PartialInvariantFunctorSyntax]]
    *        and [[InvariantFunctorSyntax]] classes.
    * @group types
    */
  type Url[A]

  /** Provides `xmap` and `xmapPartial` operations
    * @see [[PartialInvariantFunctorSyntax]] and [[InvariantFunctorSyntax]]
    */
  implicit def urlPartialInvariantFunctor: PartialInvariantFunctor[Url]

  /** Builds an URL from the given path and query string */
  def urlWithQueryString[A, B](path: Path[A], qs: QueryString[B])(implicit
      tupler: Tupler[A, B]
  ): Url[tupler.Out]

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy