summaryrefslogtreecommitdiff
path: root/cask/src/cask/main/Routes.scala
blob: 60992b5023eef1aa8bdae30e91a5bc61acf44e0a (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
package cask.main

import cask.internal.Router.{ArgReader, EntryPoint}
import cask.model.ParamContext

import scala.reflect.macros.blackbox.Context
import language.experimental.macros

object Routes{

  trait Endpoint[R] extends BaseDecorator{

    val path: String
    val methods: Seq[String]
    def subpath: Boolean = false
    def wrapMethodOutput(ctx: ParamContext,t: R): cask.internal.Router.Result[Any] = {
      cask.internal.Router.Result.Success(t)
    }

    def wrapPathSegment(s: String): Input

  }

  /**
    * The core interface of decorator annotations: the decorator provides "raw"
    * values to the annotated function via `getRawParams`, which then get
    * processed by `getParamParser` into the correct argument types before
    * being passed to the function.
    *
    * For a trivial "provide value" decorator, `getRawParams` would return the
    * final param value and `getParamParser` would return a no-op parser. For
    * a decorator that takes its input as query-params, JSON, or similar,
    * `getRawParams` would provide raw query/JSON/etc. values and
    * `getParamParser` would be responsible for processing those into the
    * correct parameter types.
    */
  trait BaseDecorator{
    type Input
    type InputParser[T] <: ArgReader[Input, T, ParamContext]
    def getRawParams(ctx: ParamContext): Either[cask.model.Response, Map[String, Input]]
    def getParamParser[T](implicit p: InputParser[T]) = p

  }

  trait Decorator extends BaseDecorator {
    type Input = Any
    type InputParser[T] = NoOpParser[Input, T]
  }

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

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

  case class EndpointMetadata[T](decorators: Seq[BaseDecorator],
                                 endpoint: Endpoint[_],
                                 entryPoint: EntryPoint[T, ParamContext])
  case class RoutesEndpointsMetadata[T](value: EndpointMetadata[T]*)
  object RoutesEndpointsMetadata{
    implicit def initialize[T] = macro initializeImpl[T]
    implicit def initializeImpl[T: c.WeakTypeTag](c: Context): c.Expr[RoutesEndpointsMetadata[T]] = {
      import c.universe._
      val router = new cask.internal.Router[c.type](c)

      val routeParts = for{
        m <- c.weakTypeOf[T].members
        val annotations = m.annotations.filter(_.tree.tpe <:< c.weakTypeOf[BaseDecorator]).reverse
        if annotations.nonEmpty
      } yield {
        if(!(annotations.head.tree.tpe <:< weakTypeOf[Endpoint[_]])) c.abort(
          annotations.head.tree.pos,
          s"Last annotation applied to a function must be an instance of Endpoint, " +
          s"not ${annotations.head.tree.tpe}"
        )
        val allEndpoints = annotations.filter(_.tree.tpe <:< weakTypeOf[Endpoint[_]])
        if(allEndpoints.length > 1) c.abort(
          annotations.head.tree.pos,
          s"You can only apply one Endpoint annotation to a function, not " +
          s"${allEndpoints.length} in ${allEndpoints.map(_.tree.tpe).mkString(", ")}"
        )
        val annotObjects =
          for(annot <- annotations)
          yield q"new ${annot.tree.tpe}(..${annot.tree.children.tail})"
        val annotObjectSyms =
          for(_ <- annotations.indices)
          yield c.universe.TermName(c.freshName("annotObject"))
        val route = router.extractMethod(
          m.asInstanceOf[MethodSymbol],
          weakTypeOf[T],
          (ctx: c.Tree, t: c.Tree) => q"${annotObjectSyms.head}.wrapMethodOutput($ctx, $t)",
          c.weakTypeOf[ParamContext],
          annotObjectSyms.map(annotObjectSym => q"$annotObjectSym.getParamParser"),
          annotObjectSyms.map(annotObjectSym => tq"$annotObjectSym.Input")

        )

        val declarations =
          for((sym, obj) <- annotObjectSyms.zip(annotObjects))
          yield q"val $sym = $obj"

        val res = q"""{
          ..$declarations
          cask.main.Routes.EndpointMetadata(
            Seq(..${annotObjectSyms.drop(1)}),
            ${annotObjectSyms.head},
            $route
          )
        }"""
        res
      }

      c.Expr[RoutesEndpointsMetadata[T]](q"""cask.main.Routes.RoutesEndpointsMetadata(..$routeParts)""")
    }
  }
}

trait Routes{
  private[this] var metadata0: Routes.RoutesEndpointsMetadata[this.type] = null
  def caskMetadata =
    if (metadata0 != null) metadata0
    else throw new Exception("Routes not yet initialize")

  protected[this] def initialize()(implicit routes: Routes.RoutesEndpointsMetadata[this.type]): Unit = {
    metadata0 = routes
  }
}