io.udash.rest.annotations.scala Maven / Gradle / Ivy
The newest version!
package io.udash
package rest
import com.avsystem.commons.annotation.{AnnotationAggregate, defaultsToName}
import com.avsystem.commons.meta.RealSymAnnotation
import com.avsystem.commons.rpc._
import com.avsystem.commons.serialization.optionalParam
import io.udash.rest.raw._
import scala.annotation.StaticAnnotation
/**
* Base trait for tag annotations that determine how a REST method is translated into actual HTTP request.
* A REST method may be annotated with one of HTTP method tags ([[io.udash.rest.GET GET]], [[io.udash.rest.PUT PUT]],
* [[io.udash.rest.POST POST]], [[io.udash.rest.PATCH PATCH]], [[io.udash.rest.DELETE DELETE]])
* which means that this method represents actual HTTP call and is expected to return a `AsyncWrapper[Result]` where
* `Result` is encodable as [[io.udash.rest.raw.RestResponse RestResponse]] and `AsyncWrapper` represents some
* abstraction over asynchronous computations (`Future` by default - see
* [[io.udash.rest.DefaultRestApiCompanion DefaultRestApiCompanion]]).
*
* If a REST method is not annotated with any of HTTP method tags, then either [[io.udash.rest.POST POST]] is
* assumed (if result type is a valid result type for HTTP method) or [[io.udash.rest.Prefix Prefix]] is assumed
* (if result type is another REST trait). [[io.udash.rest.Prefix Prefix]] means that this method only contributes
* to URL path, HTTP headers and query parameters but does not yet represent an actual HTTP request.
* Instead, it is expected to return instance of some other REST API trait which will ultimately determine the
* actual HTTP call.
*/
sealed trait RestMethodTag extends RpcTag {
/**
* HTTP URL path segment associated with REST method annotated with this tag. This path may be multipart
* (i.e. contain slashes). It may also be empty which means that this particular REST method does not contribute
* anything to URL path. Any special characters must already be URL-encoded (spaces should be encoded as `%20`,
* not as `+`). If path is not specified explicitly, method name is used (the actual method name, not `rpcName`).
*
* @example
* {{{
* trait SomeRestApi {
* @GET("users/find")
* def findUser(userId: String): Future[User]
* }
* object SomeRestApi extends RestApiCompanion[SomeRestApi]
* }}}
*/
@defaultsToName def path: String
}
object RestMethodTag {
/**
* Used as fake default value for `path` parameter. Replaced with actual method name by annotation processing
* in RPC macro engine.
*/
def methodName: String = throw new NotImplementedError("stub")
}
/**
* Base class for [[io.udash.rest.RestMethodTag RestMethodTag]]s representing actual HTTP methods, as opposed to
* [[io.udash.rest.Prefix Prefix]] methods.
*/
sealed abstract class HttpMethodTag(val method: HttpMethod) extends RestMethodTag with AnnotationAggregate
/**
* Base trait for annotations representing HTTP methods which may define an HTTP body. This includes
* [[io.udash.rest.PUT PUT]], [[io.udash.rest.POST POST]], [[io.udash.rest.PATCH PATCH]] and
* [[io.udash.rest.DELETE DELETE]]. Parameters of REST methods annotated with one of these tags are by default
* serialized into JSON (through encoding to [[io.udash.rest.raw.JsonValue JsonValue]]) and combined into JSON
* object that is sent as HTTP body.
*
* Parameters may also contribute to URL path, HTTP headers and query parameters if annotated with
* [[io.udash.rest.Path Path]], [[io.udash.rest.Header Header]] or [[io.udash.rest.Query Query]].
*
* REST method may also take a single parameter representing the entire HTTP body. Such parameter must be annotated
* as [[io.udash.rest.Body Body]] and must be the only body parameter of that method. Value of this parameter will
* be encoded as [[io.udash.rest.raw.HttpBody HttpBody]] which doesn't necessarily have to be JSON
* (it may define its own media type).
*
* @example
* {{{
* trait SomeRestApi {
* @POST("users/create") def createUser(@Body user: User): Future[Unit]
* @PATCH("users/update") def updateUser(id: String, name: String): Future[User]
* }
* object SomeRestApi extends RestApiCompanion[SomeRestApi]
* }}}
*/
sealed abstract class BodyMethodTag(method: HttpMethod) extends HttpMethodTag(method)
/**
* REST method annotated with `@GET` will translate to HTTP GET request. By default, parameters of such method
* are translated into URL query parameters (encoded as [[io.udash.rest.raw.PlainValue PlainValue]]).
* Alternatively, each parameter may be annotated with [[io.udash.rest.Path Path]] or [[io.udash.rest.Header Header]]
* which means that it will be translated into HTTP header value.
*
* @param path see [[RestMethodTag.path]]
*/
class GET(val path: String = RestMethodTag.methodName) extends HttpMethodTag(HttpMethod.GET) {
@rpcNamePrefix("get_", overloadedOnly = true)
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/**
* See [[io.udash.rest.BodyMethodTag BodyMethodTag]].
* This is the default tag for untagged methods which are not recognized as [[io.udash.rest.Prefix Prefix]] methods
* (i.e. their result type is not another REST trait).
*/
class POST(val path: String = RestMethodTag.methodName) extends BodyMethodTag(HttpMethod.POST) {
@rpcNamePrefix("post_", overloadedOnly = true)
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/** See [[io.udash.rest.BodyMethodTag BodyMethodTag]] */
class PATCH(val path: String = RestMethodTag.methodName) extends BodyMethodTag(HttpMethod.PATCH) {
@rpcNamePrefix("patch_", overloadedOnly = true)
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/** See [[io.udash.rest.BodyMethodTag BodyMethodTag]] */
class PUT(val path: String = RestMethodTag.methodName) extends BodyMethodTag(HttpMethod.PUT) {
@rpcNamePrefix("put_", overloadedOnly = true)
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/** See [[io.udash.rest.BodyMethodTag BodyMethodTag]] */
class DELETE(val path: String = RestMethodTag.methodName) extends BodyMethodTag(HttpMethod.DELETE) {
@rpcNamePrefix("delete_", overloadedOnly = true)
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/**
* Base trait for tag annotations which specify how an HTTP body is built for invocation of particular
* method. The default one is [[io.udash.rest.JsonBody JsonBody]].
*/
sealed trait BodyTypeTag extends RpcTag
/**
* Indicates that an HTTP REST method takes no body. This annotation is assumed by default
* for [[io.udash.rest.GET GET]] and [[io.udash.rest.Prefix Prefix]] methods. There should be no reason to use it
* explicitly.
*/
class NoBody extends BodyTypeTag
sealed trait SomeBodyTag extends BodyTypeTag
/**
* Causes the [[io.udash.rest.Body Body]] parameters of an HTTP REST method to be encoded as `application/json`.
* Each parameter value itself will be first serialized to [[io.udash.rest.raw.JsonValue JsonValue]].
* This annotation only applies to methods which may include HTTP body (i.e. not [[io.udash.rest.GET GET]])
* and is assumed by default, so there should be no reason to apply it explicitly.
*/
class JsonBody extends SomeBodyTag
/**
* Causes the [[io.udash.rest.Body Body]] parameters of an HTTP REST method to be encoded as
* `application/x-www-form-urlencoded`. Each parameter value itself will be first serialized to
* [[io.udash.rest.raw.PlainValue PlainValue]].
* This annotation only applies to methods which may include HTTP body (i.e. not [[io.udash.rest.GET GET]]).
*/
class FormBody extends SomeBodyTag
/**
* Requires that a method takes exactly one [[io.udash.rest.Body Body]] parameter which serializes directly into
* [[io.udash.rest.raw.HttpBody HttpBody]]. Serialization may then use arbitrary body format.
* This annotation only applies to methods which may include HTTP body (i.e. not [[io.udash.rest.GET GET]]).
*/
class CustomBody extends SomeBodyTag
/**
* REST methods annotated with [[io.udash.rest.Prefix Prefix]] are expected to return another REST API trait as their
* result. They do not yet represent an actual HTTP request but contribute to URL path, HTTP headers and query
* parameters.
*
* By default, parameters of a prefix method are interpreted as URL path fragments. Their values are encoded as
* [[io.udash.rest.raw.PlainValue PlainValue]] and appended to URL path. Alternatively, each parameter may also be
* explicitly annotated with [[io.udash.rest.Header Header]], [[io.udash.rest.Query Query]] or
* [[io.udash.rest.Cookie Cookie]].
*
* NOTE: REST method is interpreted as prefix method by default which means that there is no need to apply
* [[io.udash.rest.Prefix Prefix]] annotation explicitly unless you want to specify a custom path.
*
* @param path see [[RestMethodTag.path]]
*/
class Prefix(val path: String = RestMethodTag.methodName) extends RestMethodTag
sealed trait RestParamTag extends RpcTag
object RestParamTag {
/**
* Used as fake default value for `name` parameter. Replaced with actual param name by annotation processing
* in RPC macro engine.
*/
def paramName: String = throw new NotImplementedError("stub")
}
sealed trait NonBodyTag extends RestParamTag {
def isPath: Boolean = this match {
case _: Path => true
case _ => false
}
def isHeader: Boolean = this match {
case _: Header => true
case _ => false
}
def isQuery: Boolean = this match {
case _: Query => true
case _ => false
}
}
/**
* REST method parameters annotated with [[io.udash.rest.Path Path]] will be encoded as
* [[io.udash.rest.raw.PlainValue PlainValue]] and appended to URL path, in the declaration order.
* Parameters of [[io.udash.rest.Prefix Prefix]] REST methods are interpreted as [[io.udash.rest.Path Path]]
* parameters by default.
*/
class Path(val pathSuffix: String = "") extends NonBodyTag
/**
* REST method parameters annotated with [[io.udash.rest.Header Header]] will be encoded as
* [[io.udash.rest.raw.PlainValue PlainValue]] and added to HTTP headers.
* Header name must be explicitly given as argument of this annotation.
*/
class Header(override val name: String)
extends rpcName(name) with NonBodyTag
/**
* REST method parameters annotated with [[io.udash.rest.Query Query]] will be encoded as
* [[io.udash.rest.raw.PlainValue PlainValue]] and added to URL query parameters.
* Parameters of [[io.udash.rest.GET GET]] REST methods are interpreted as [[io.udash.rest.Query Query]]
* parameters by default.
*/
class Query(@defaultsToName override val name: String = RestParamTag.paramName)
extends rpcName(name) with NonBodyTag
/**
* REST method parameterrs annotated with [[io.udash.rest.Cookie Cookie]] will be encoded as
* [[io.udash.rest.raw.PlainValue PlainValue]] and sent as cookie values (using `Cookie` HTTP header).
* Cookie parameter values must not contain ';' character (semicolon).
*/
class Cookie(@defaultsToName override val name: String = RestParamTag.paramName)
extends rpcName(name) with NonBodyTag
/**
* REST method parameters annotated with [[io.udash.rest.Body Body]] will be used to build HTTP request body.
* How exactly that happens depends on [[io.udash.rest.BodyTypeTag BodyTypeTag]] applied on a method. By default,
* [[io.udash.rest.JsonBody JsonBody]] is assumed which means that body parameters will be combined into a single
* JSON object sent as body.
*
* Body parameters are allowed only in REST methods annotated with [[io.udash.rest.POST POST]],
* [[io.udash.rest.PATCH PATCH]], [[io.udash.rest.PUT PUT]] or [[io.udash.rest.DELETE DELETE]]. Actually, for these
* methods, an unannotated parameter is assumed to be a body parameter by default. This means that there's usually
* no reason to apply this annotation explicitly. It may only be useful when wanting to customize JSON/form field name
* which this annotation takes as its argument
*/
class Body(@defaultsToName override val name: String = RestParamTag.paramName)
extends rpcName(name) with RestParamTag
/**
* Like [[Query]] but indicates that the parameter is optional.
* This means that its type must be wrapped into an `Option`, `Opt`, `OptArg`, etc. and empty value
* corresponds to an absence of the parameter in the request.
*
* Also, the macro engine looks for serialization and other implicits directly for the type wrapped in an
* `Opt`, `Option`, e.g. `AsRaw/AsReal[PlainValue, Something]` is needed when an optional query parameter has
* type `Opt[Something]`.
*/
class OptQuery(@defaultsToName name: String = RestParamTag.paramName) extends AnnotationAggregate {
@Query(name) @optionalParam
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/**
* Like [[Header]] but indicates that the parameter is optional.
* This means that its type must be wrapped into an `Option`, `Opt`, `OptArg`, etc. and empty value
* corresponds to an absence of the header in the request.
*
* Also, the macro engine looks for serialization and other implicits directly for the type wrapped in an
* `Opt`, `Option`, e.g. `AsRaw/AsReal[PlainValue, Something]` is needed when an optional header parameter has
* type `Opt[Something]`.
*/
class OptHeader(name: String) extends AnnotationAggregate {
@Header(name) @optionalParam
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/**
* Like [[Cookie]] but indicates that the parameter is optional.
* This means that its type must be wrapped into an `Option`, `Opt`, `OptArg`, etc. and empty value
* corresponds to an absence of the parameter in the request.
*
* Also, the macro engine looks for serialization and other implicits directly for the type wrapped in an
* `Opt`, `Option`, e.g. `AsRaw/AsReal[PlainValue, Something]` is needed when an optional cookie parameter has
* type `Opt[Something]`.
*/
class OptCookie(@defaultsToName name: String = RestParamTag.paramName) extends AnnotationAggregate {
@Cookie(name) @optionalParam
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/**
* Like [[Body]] (for body field parameters) but indicates that the parameter is optional.
* This means that its type must be wrapped into an `Option`, `Opt`, `OptArg`, etc. and empty value
* corresponds to an absence of the field in the request body.
*
* Also, the macro engine looks for serialization and other implicits directly for the type wrapped in an
* `Opt`, `Option`, e.g. `AsRaw/AsReal[JsonValue, Something]` is needed when an optional field has
* type `Opt[Something]`.
*/
class OptBodyField(@defaultsToName name: String = RestParamTag.paramName) extends AnnotationAggregate {
@Body(name) @optionalParam
final def aggregated: List[StaticAnnotation] = reifyAggregated
}
/**
* Base trait for annotations which may be applied on REST API methods (including prefix methods)
* in order to customize outgoing request on the client side.
*/
trait RequestAdjuster extends RealSymAnnotation {
def adjustRequest(request: RestRequest): RestRequest
}
/**
* Base trait for annotations which may be applied on REST API methods (including prefix methods)
* in order to customize outgoing response on the server side.
*/
trait ResponseAdjuster extends RealSymAnnotation {
def adjustResponse(response: RestResponse): RestResponse
}
/**
* Convenience implementation of [[RequestAdjuster]].
*/
class adjustRequest(f: RestRequest => RestRequest) extends RequestAdjuster {
def adjustRequest(request: RestRequest): RestRequest = f(request)
}
/**
* Convenience implementation of [[ResponseAdjuster]].
*/
class adjustResponse(f: RestResponse => RestResponse) extends ResponseAdjuster {
def adjustResponse(response: RestResponse): RestResponse = f(response)
}
/**
* Annotation which may be applied on REST API methods (including prefix methods) in order to append additional
* HTTP header to all outgoing requests generated for invocations of that method on the client side.
*/
class addRequestHeader(name: String, value: String) extends RequestAdjuster {
def adjustRequest(request: RestRequest): RestRequest = request.header(name, value)
}
/**
* Annotation which may be applied on REST API methods (including prefix methods) in order to append additional
* HTTP header to all outgoing responses generated for invocations of that method on the server side.
*/
class addResponseHeader(name: String, value: String) extends ResponseAdjuster {
def adjustResponse(response: RestResponse): RestResponse = response.header(name, value)
}