endpoints4s.algebra.Endpoints.scala Maven / Gradle / Ivy
The newest version!
package endpoints4s.algebra
import endpoints4s.Hashing
import scala.annotation.nowarn
/** Algebra interface for describing endpoints made of requests and responses.
*
* Requests and responses contain headers and entity.
*
* {{{
* /**
* * Describes an HTTP endpoint whose:
* * - request uses verb “GET”,
* * - URL is made of path “/foo”,
* * - response has no entity
* */
* val example = endpoint(get(path / "foo"), emptyResponse)
* }}}
*
* This trait uses [[BuiltInErrors]] to model client and server errors.
*
* @group algebras
*/
trait Endpoints extends EndpointsWithCustomErrors with BuiltInErrors
/** Algebra interface for describing endpoints made of requests and responses.
*
* @group algebras
*/
trait EndpointsWithCustomErrors extends Requests with Responses with Errors {
/** Information carried by an HTTP endpoint
*
* Values of type [[Endpoint]] can be constructed by using the operation
* [[endpoint]].
*
* @tparam A Information carried by the request
* @tparam B Information carried by the response
* @note This type has implicit methods provided by the [[EndpointSyntax]] class
* @group types
*/
type Endpoint[A, B]
/** Define an HTTP endpoint
*
* @param request Request
* @param response Response
* @param docs Documentation (used by documentation interpreters)
* @group operations
*/
def endpoint[A, B](
request: Request[A],
response: Response[B],
docs: EndpointDocs = EndpointDocs()
): Endpoint[A, B]
/** Map the inner request of the endpoint to a new request. This is for example useful to add Auth headers to an
* existing endpoint.
*
* @param endpoint The current endpoint that is being mapped.
* @param func The function that maps the request to some new request.
* @group operations
* @return The endpoint with the mapped request.
* @example
* {{{
* val myEndpoint: Endpoint[Input, Output] = ???
* val basicAuthHeaders: RequestHeaders[Credentials] = ???
*
* val endpointWithAuth: Endpoint[(Input, Credentials), Output] =
* myEndpoint.mapRequest(_.addHeaders(basicAuthHeader))
* }}}
*/
def mapEndpointRequest[A, B, C](
endpoint: Endpoint[A, B],
func: Request[A] => Request[C]
): Endpoint[C, B] =
unsupportedInterpreter("1.6.0")
/** Map the inner response of the endpoint to a new response. This is for example useful so you can add error handling
* to an existing endpoint.
*
* @param endpoint The current endpoint that is being mapped.
* @param func The function that maps the response to some new response.
* @group operations
* @return The endpoint with the mapped response.
* @example
* {{{
* val myEndpoint: Endpoint[Input, Output] = ???
* val errorResponse: Response[Error] = ???
*
* val endpointWithErrorHandling: Endpoint[Input, Either[Error, Output]] =
* myEndpoint.mapResponse(resp => resp orElse errorResponse)
* }}}
*/
def mapEndpointResponse[A, B, C](
endpoint: Endpoint[A, B],
func: Response[B] => Response[C]
): Endpoint[A, C] =
unsupportedInterpreter("1.6.0")
/** Map the inner documentation of the endpoint to new documentation.
*
* @param endpoint The current endpoint that is being mapped.
* @param func The function that maps the documentation to some new documentation.
* @group operations
* @return The endpoint with the mapped documentation.
*/
def mapEndpointDocs[A, B](
endpoint: Endpoint[A, B],
func: EndpointDocs => EndpointDocs
): Endpoint[A, B] =
unsupportedInterpreter("1.6.0")
/** Extension methods for [[Endpoint]].
*
* @group operations
*/
implicit final class EndpointSyntax[A, B](endpoint: Endpoint[A, B]) {
/** Map the inner request of this endpoint, see [[mapEndpointRequest]]. */
//#map-request-signature
def mapRequest[C](func: Request[A] => Request[C]): Endpoint[C, B]
//#map-request-signature
= mapEndpointRequest(endpoint, func)
/** Map the inner response of this endpoint, see [[mapEndpointResponse]]. */
def mapResponse[C](func: Response[B] => Response[C]): Endpoint[A, C] =
mapEndpointResponse(endpoint, func)
/** Map the documentation of this endpoint, see [[mapEndpointDocs]].
*/
def mapDocs(func: EndpointDocs => EndpointDocs): Endpoint[A, B] =
mapEndpointDocs(endpoint, func)
}
/** @param operationId A unique identifier which identifies this operation
* @param summary Short description
* @param description Detailed description
* @param tags OpenAPI tags
* @param callbacks Callbacks indexed by event name
* @param deprecated Indicates whether this endpoint is deprecated or not
*/
final class EndpointDocs private (
val operationId: Option[String],
val summary: Documentation,
val description: Documentation,
val tags: List[Tag],
val callbacks: Map[String, CallbacksDocs],
val deprecated: Boolean
) extends Serializable {
override def toString =
s"EndpointDocs($operationId, $summary, $description, $tags, $callbacks, $deprecated)"
@nowarn("cat=unchecked")
override def equals(other: Any): Boolean =
other match {
case that: EndpointDocs =>
operationId == that.operationId &&
summary == that.summary &&
description == that.description &&
tags == that.tags &&
callbacks == that.callbacks &&
deprecated == that.deprecated
case _ => false
}
override def hashCode(): Int =
Hashing.hash(
operationId,
summary,
description,
tags,
callbacks,
deprecated
)
private[this] def copy(
operationId: Option[String] = operationId,
summary: Documentation = summary,
description: Documentation = description,
tags: List[Tag] = tags,
callbacks: Map[String, CallbacksDocs] = callbacks,
deprecated: Boolean = deprecated
): EndpointDocs =
new EndpointDocs(
operationId,
summary,
description,
tags,
callbacks,
deprecated
)
def withOperationId(operationId: Option[String]): EndpointDocs =
copy(operationId = operationId)
def withSummary(summary: Documentation): EndpointDocs =
copy(summary = summary)
def withDescription(description: Documentation): EndpointDocs =
copy(description = description)
def withTags(tags: List[Tag]): EndpointDocs =
copy(tags = tags)
def withCallbacks(callbacks: Map[String, CallbacksDocs]): EndpointDocs =
copy(callbacks = callbacks)
def withDeprecated(deprecated: Boolean): EndpointDocs =
copy(deprecated = deprecated)
}
object EndpointDocs {
/** @return An empty documentation value, with no summary, no description,
* no tags, no callbacks, and the `deprecated` flag set to `false`.
*
* You can transform the returned [[EndpointDocs]] value by using the `withXxx`
* operations:
*
* {{{
* EndpointDocs().withSummary(Some("endpoint summary"))
* }}}
*/
def apply(): EndpointDocs =
new EndpointDocs(None, None, None, Nil, Map.empty, false)
@deprecated(
"Use `EndpointDocs().withXxx(...)` instead of `EndpointDocs(xxx = ...)`",
"1.0.0"
)
def apply(
summary: Documentation = None,
description: Documentation = None,
tags: List[String] = Nil,
callbacks: Map[String, CallbacksDocs] = Map.empty,
deprecated: Boolean = false
): EndpointDocs =
new EndpointDocs(
None,
summary,
description,
tags.map(Tag(_)),
callbacks,
deprecated
)
}
/** Callbacks indexed by URL pattern
* @see Swagger Documentation at [[https://swagger.io/docs/specification/callbacks/]]
*/
type CallbacksDocs = Map[String, CallbackDocs]
/** @param method HTTP method used for the callback
* @param entity Contents of the callback message
* @param response Expected response
*/
final class CallbackDocs private (
val method: Method,
val entity: CallbackDocs.SomeRequestEntity,
val response: CallbackDocs.SomeResponse,
val requestDocs: Documentation
) extends Serializable {
override def toString =
s"CallbackDocs($method, $entity, $response, $requestDocs)"
@nowarn("cat=unchecked")
override def equals(other: Any): Boolean =
other match {
case that: CallbackDocs =>
method == that.method &&
entity == that.entity &&
response == that.response &&
requestDocs == that.requestDocs
case _ => false
}
override def hashCode(): Int =
Hashing.hash(method, entity, response, requestDocs)
private[this] def copy(
method: Method = method,
entity: CallbackDocs.SomeRequestEntity = entity,
response: CallbackDocs.SomeResponse = response,
requestDocs: Documentation = requestDocs
): CallbackDocs =
new CallbackDocs(method, entity, response, requestDocs)
def withMethod(method: Method): CallbackDocs =
copy(method = method)
def withRequestEntity[A](entity: RequestEntity[A]): CallbackDocs =
copy(entity = CallbackDocs.SomeRequestEntity(entity))
def withRequestDocs(requestDocs: Documentation): CallbackDocs =
copy(requestDocs = requestDocs)
def withResponse[A](response: Response[A]): CallbackDocs =
copy(response = CallbackDocs.SomeResponse(response))
}
object CallbackDocs {
/** A wrapper type for a [[RequestEntity]] whose carried information is unknown.
*
* This wrapper type is necessary because Scala 3 does not support writing `RequestEntity[_]`.
*/
trait SomeRequestEntity {
type T
def value: RequestEntity[T]
}
object SomeRequestEntity {
def apply[A](requestEntity: RequestEntity[A]): SomeRequestEntity =
new SomeRequestEntity {
type T = A
def value: RequestEntity[T] = requestEntity
}
}
/** A wrapper type for a [[Response]] whose carried information is unknown.
*
* This wrapper type is necessary because Scala 3 does not support writing `Response[_]`
*/
trait SomeResponse {
type T
def value: Response[T]
}
object SomeResponse {
def apply[A](response: Response[A]): SomeResponse =
new SomeResponse {
type T = A
def value: Response[T] = response
}
}
/** Convenience constructor that wraps the `entity` and `response` parameters.
*/
def apply[A, B](
method: Method,
entity: RequestEntity[A],
response: Response[B]
): CallbackDocs =
new CallbackDocs(
method,
SomeRequestEntity(entity),
SomeResponse(response),
None
)
@deprecated(
"Use `CallbackDocs(...).withRequestDocs(docs)` instead of `CallbackDocs(..., requestDocs = docs)`",
"1.0.0"
)
def apply[A, B](
method: Method,
entity: RequestEntity[A],
response: Response[B],
requestDocs: Documentation = None
): CallbackDocs =
new CallbackDocs(
method,
SomeRequestEntity(entity),
SomeResponse(response),
requestDocs
)
}
}