summaryrefslogtreecommitdiff
path: root/cask/src/cask/router/Decorators.scala
blob: 14b4ab4e2b52f33eb8599abc8a1fb8766fb336d7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package cask.router

import cask.internal.Conversion
import cask.model.{Request, Response}

/**
  * A [[Decorator]] allows you to annotate a function to wrap it, via
  * `wrapFunction`. You can use this to perform additional validation before or
  * after the function runs, provide an additional parameter list of params,
  * open/commit/rollback database transactions before/after the function runs,
  * or even retrying the wrapped function if it fails.
  *
  * Calls to the wrapped function are done on the `delegate` parameter passed
  * to `wrapFunction`, which takes a `Map` representing any additional argument
  * lists (if any).
  */
trait Decorator[InnerReturned, Input]{
  final type InputTypeAlias = Input
  type InputParser[T] <: ArgReader[Input, T, Request]
  final type Delegate = Map[String, Input] => Result[InnerReturned]
  type OuterReturned <: Result[Any]
  def wrapFunction(ctx: Request, delegate: Delegate): OuterReturned
  def getParamParser[T](implicit p: InputParser[T]) = p
}
object Decorator{
  /**
   * A stack of [[Decorator]]s is invoked recursively: each decorator's `wrapFunction`
   * is invoked around the invocation of all inner decorators, with the inner-most
   * decorator finally invoking the route's [[EntryPoint.invoke]] function.
   *
   * Each decorator (and the final `Endpoint`) contributes a dictionary of name-value
   * bindings, which are eventually all passed to [[EntryPoint.invoke]]. Each decorator's
   * dictionary corresponds to a different argument list on [[EntryPoint.invoke]]. The
   * bindings passed from the router are aggregated with those from the `EndPoint` and
   * used as the first argument list.
   */
  def invoke[T](ctx: Request,
                endpoint: Endpoint[_, _],
                entryPoint: EntryPoint[T, _],
                routes: T,
                routeBindings: Map[String, String],
                remainingDecorators: List[RawDecorator],
                bindings: List[Map[String, Any]]): Result[Any] = try {
    remainingDecorators match {
      case head :: rest =>
        head.wrapFunction(
          ctx,
          args => invoke(ctx, endpoint, entryPoint, routes, routeBindings, rest, args :: bindings)
            .asInstanceOf[Result[cask.model.Response.Raw]]
        )

      case Nil =>
        endpoint.wrapFunction(ctx, { (endpointBindings: Map[String, Any]) =>
          val mergedEndpointBindings = endpointBindings ++ routeBindings.mapValues(endpoint.wrapPathSegment)
          val finalBindings = mergedEndpointBindings :: bindings

          entryPoint
            .asInstanceOf[EntryPoint[T, cask.model.Request]]
            .invoke(routes, ctx, finalBindings)
            .asInstanceOf[Result[Nothing]]
        })
    }
    // Make sure we wrap any exceptions that bubble up from decorator
    // bodies, so outer decorators do not need to worry about their
    // delegate throwing on them
  }catch{case e: Throwable => Result.Error.Exception(e) }
}

/**
  * A [[RawDecorator]] is a decorator that operates on the raw request and
  * response stream, before and after the primary [[Endpoint]] does it's job.
  */
trait RawDecorator extends Decorator[Response.Raw, Any]{
  type OuterReturned = Result[Response.Raw]
  type InputParser[T] = NoOpParser[Any, T]
}


/**
  * An [[HttpEndpoint]] that may return something else than a HTTP response, e.g.
  * a websocket endpoint which may instead return a websocket event handler
  */
trait Endpoint[InnerReturned, Input] extends Decorator[InnerReturned, Input]{
  /**
    * What is the path that this particular endpoint matches?
    */
  val path: String
  /**
    * Which HTTP methods does this endpoint support? POST? GET? PUT? Or some
    * combination of those?
    */
  val methods: Seq[String]

  /**
    * Whether or not this endpoint allows matching on sub-paths: does
    * `@endpoint("/foo")` capture the path "/foo/bar/baz"? Useful to e.g. have
    * an endpoint match URLs with paths in a filesystem (real or virtual) to
    * serve files
    */
  def subpath: Boolean = false

  def convertToResultType[T](t: T)
                            (implicit f: Conversion[T, InnerReturned]): InnerReturned = {
    f.f(t)
  }

  /**
    * [[HttpEndpoint]]s are unique among decorators in that they alone can bind
    * path segments to parameters, e.g. binding `/hello/:world` to `(world: Int)`.
    * In order to do so, we need to box up the path segment strings into an
    * [[Input]] so they can later be parsed by [[getParamParser]] into an
    * instance of the appropriate type.
    */
  def wrapPathSegment(s: String): Input

}

/**
  * Annotates a Cask endpoint that returns a HTTP [[Response]]; similar to a
  * [[RawDecorator]] but with additional metadata and capabilities.
  */
trait HttpEndpoint[InnerReturned, Input] extends Endpoint[InnerReturned, Input] {
  type OuterReturned = Result[Response.Raw]
}


class NoOpParser[Input, T] extends ArgReader[Input, T, Request] {
  def arity = 1

  def read(ctx: Request, label: String, input: Input) = input.asInstanceOf[T]
}
object NoOpParser{
  implicit def instance[Input, T] = new NoOpParser[Input, T]
  implicit def instanceAny[T] = new NoOpParser[Any, T]
}