Cask macros code analysis covers the macros implementation and related data types. To debug Macro AST, use def scalacOptions = Seq("-Ymacro-debug-lite") to inpsect generated code.

1 The Routes

An instance of Routes has a set of defined Decorator, an implicit actor Context, an implicit log, and an instance of RoutesEndpointsMetatadata. A subtype of Routes is required to call the Routes#initialize() method that has an implicit parameter of RoutesEndpointsMetatadata. The call triggers the call to the RoutesEndpointsMetadata.iniitalize macro to create an instance of RoutesEndpointsMetatadata[T] for this subtype T of the Routes.

The RoutesEndpointsMetadata[T] case class only has a value member that is a sequence of EndpointMetadata[T]. An EndpointMetadata[T] has

  • a sequnce of Decorator excluding the Endpoint (?)
  • one Endpoint
  • one EntryPoint[T, _].

The macro’s function it to return a list of EndpointMetadta for the delcared subtype of Routes by performning the following steps:

  • Get members of T, for each member, get its non-empty annotations that are a subtype of Decorator. Generate an EndpointMetaData[T] for each annotated member.
  • Abort if the last annotation doesn’t have a type of Endpoint or there are more than one Endpoint annotations.
  • Genereate a list of EndpointMetadata for the list of endpoints.

For each annotated member (an endpoint):

  • Generate all annotation objects.
  • Generate a list of names and positions for the annotation objects
  • Call the Macros.extraceMethod to create an EntryPoint object for this annotated endpoint.
  • create a list of definitions for the annotation ojbects
  • verify that the inner annotation’s OuterReturned type is the enclosing annotation’s InnerREturned type.
  • generate the code block that has
    • the declaration of all generated annotation objects
    • an instance of EndpointMetadata
    • A list of annotations in the order of definition, from outmost to the inner most, removing the last endpoint annontation
    • the endpoint annontation
    • the EntrypPoint

2 Macros.extraceMethod

The parameters are

  • method: MethodSymbol: the method annotated by the endpoint as a MethodSymbol
  • curCls: c.universe.Type: current class, the type of T, a subtype of Routes
  • convertToResultType: c.Tree: the Endpoint annotation’s convertToResultType method
  • ctx: c.Tree: the cask.Request type
  • argReaders: Seq[c.Tree]: a list of each annotation object’s getParamParser methods, in reverse annotation order
  • annotDeserializeTypes: Seq[c.Tree]: a list of each annotation object’s InputTypeAlias types, in reverse annotation order

The steps are

  • generate a symbol for an instance of the current Routes type, like fresh$macro$4.
  • generate method doc.
  • generate a symbol for argument value, like argValues$macro$5.
  • generate a symbol for argument signature, like argSigs$macro$6.
  • generate a symbol for context, like ctx$macro$7. It is used to define the context variable like ctx$macro$7: cask.Request.
  • for each of the method’s parameter list, create argData that is a list of quadruples: (argNameCats, argSigs, argNames, readArgs). Each argument list is corresponding to
    • argNameCats: a list of argument names with a cast, like status.asInstanceOf[String].
    • argSigs: the signature of arguments. ArgSig is a case class having two parameter lists: the first lis tincludes the name, type, doc, and default value of type Option[T => V]. The 2nd list is an implicit val of the decorator’s getParamParser.
    • argNames: a list of argument names
    • readArgs: a list of calls to cask.router.Runtime.makeReadCall. The call uses getParamParser to parse the argument and returns an Either result for Result.ParamError or valid result.

With the list of quadruples for each parameter list, create an EntryPoint[T, cask.Request]. It has four fields:

  • name: String: the method (entry point) name defined right below an end point notation.
  • argSignatures: Seq[Seq[ArgSig[_, T, _, C]]]: a sequence of a seuqnce of ArgSig. The element of the first sequence is each parameter list. One parameter list for one decorator. the 2nd sequence is a list of ArgSig in an parameter list. Cask only allows empty or one parameter in a paramter list.
  • doc: Option[String]: method doc string
  • invoke0: it is a call to cask.router.Runtime.validate(Seq(makeReadCall(...), makeReadCall(...))).map(endpoint.convertToResultType(entryPoint(a3)(a2)(a1)).

3 The invoke method

For a matched route, the invoke method performs the following steps:

  • For each decorator, the Runtime.makeReadCall uses its InputParser[T] to parse the parameter.
  • The Runtime.validate validates all decorator results. The result is an Either that has either a list of ParamError or a list of valid arguments for the entry point method.
  • Call the entry point mehtod with all arguments.
  • Call the end point’s convertToResultType to convert the entry point result to the type of declared result of the end point.

Each parameter list corresponds to one decorator. The endpoint decorator’s input is the output of the entry point. Its output is the input of the next decorator. For HTTPEndpoint, the output is Response.Raw, and all outer decoratos have a type of Response.Raw for their input and output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// I is the input type
// T is the define type - a subtype of Routes
// V is the result value type for both the default converter and `ArgReader#read(...)`
// C is the type of context, it is fixed as `cask.Request`
case class ArgSig[I, -T, +V, -C](name: String,
                                 typeString: String,
                                 doc: Option[String],
                                 default: Option[T => V])
                                (implicit val reads: ArgReader[I, V, C])

// T is the type of return value
// I is the inpu ttype
// C is the `cask.Request`
// it is called by Runtime.makeReadCall
// When arity is 0, use `null` as input. Use by `ParamReader[T]`
// such as `HttpExchangeParam`, `FormDataParam`, `RequestParam` and
// `CookieParam` to read from Request.
// When arity is 1, get value from decorator's Map[String, I]
trait ArgReader[I, +T, -C]{
  def arity: Int
  def read(ctx: C, label: String, input: I): T
}

4 Endpoint

An endpoint decorator is the last decorator enclosing the entry point. For an HttpEndpoint, the OuterReturned is fixed to Response.Raw, the InnerReturned is the type returned from entry point call. The Input is passed as the value type of Map[String, Input]. For example, in WebEndpoint, the wrapFunction is defined as: delegate(WebEndpoint.buildMapFromQueryParams(ctx)): it gets query parameters and passed into the entry point as an instance of Map[String, Seq[String]]. The warpPathSegment(s: String) = Seq(s) matches the Input type. The warpPathESgment result is combined with the result of buildMapFromQueryParams in Decorator#invoke method.

The HttpEndpoint uses QueryParamReader to convert a String to a set of predefined types. For custom data type T, define an implicit instance of QueryParamReader[T].

5 RawDecorator

Non Endpoint decorator should use RawDecorator to wrap perform action before or after the enclosing decorator. Its OutReturned and InnerReturned are Response.Raw. Its Input is Any and doesn’t use InputParser.

Therefore, the functions of a RawDecorator can be one or more of the following:

  • perform an action before the enclosing decorator
  • pass some arguments to the enclosing decorator
  • perform an action after the enclosing decorator

6 Response.Raw

7 Example

For the following source:

 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
class MyEndoint(val path: String, val methods: Seq[String])
    extends cask.HttpEndpoint[String, Seq[String]] {
  def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
    delegate(Map()).map { resulut =>
      cask.Response("Echo " + resulut, statusCode = 200)
    }
  }

  def wrapPathSegment(s: String) = Seq(s)

  type InputParser[T] = cask.endpoints.QueryParamReader[T]
}

object MyRoutes extends cask.MainRoutes {

  class User {
    override def toString = "[haoyi]"
  }

  class loggedIn extends cask.RawDecorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
      delegate(Map("user" -> new User()))
    }
  }

  class withExtra extends cask.RawDecorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
      delegate(Map("extra" -> 31337))
    }
  }

  @withExtra()
  @loggedIn()
  @MyEndoint("/echo/:status", methods = Seq("get"))
  def echoStatus(status: String)(user: User)(extra: Int): String = {
    s"Status: ${status}, User: ${user}, Extra: ${extra}"
  }

  initialize()
}

The macros generates the following code:

  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
/*
performing macro expansion scala.StringContext.apply("Status: ", ", User: ", ", Extra: ", "").s(status, user, extra)
at source-/app/src/MyRoutes.scala,line-38,offset=974

"Status: ".+(status).+(", User: ").+(user).+(", Extra: ").+(extra)

performing macro expansion router.this.RoutesEndpointsMetadata.initialize[app.MyRoutes.type] at
source-/app/src/MyRoutes.scala,line-41,offset=1044
 */

object Demo {
  cask.router.RoutesEndpointsMetadata({
    val annotObject$macro$1 = new app.MyRoutes.withExtra();
    val annotObject$macro$2 = new app.MyRoutes.loggedIn();
    val annotObject$macro$3 = new app.MyEndoint(
      "/echo/:status",
      scala.collection.immutable.Seq.apply[String]("get")
    );
    cask.router.EndpointMetadata(
      cask.router.EndpointMetadata
        .seqify3(annotObject$macro$3)(annotObject$macro$2)(annotObject$macro$1)
        .reverse
        .dropRight(1),
      annotObject$macro$3,
      cask.router.EntryPoint[app.MyRoutes.type, cask.Request](
        "echoStatus",
        scala.collection.immutable.List(
          scala.collection.immutable.List(
            cask.router.ArgSig[
              annotObject$macro$3.InputTypeAlias,
              app.MyRoutes.type,
              String,
              cask.Request
            ]("status", "String", scala.None, scala.None)(
              annotObject$macro$3.getParamParser[String]
            )
          ),
          scala.collection.immutable.List(
            cask.router.ArgSig[
              annotObject$macro$2.InputTypeAlias,
              app.MyRoutes.type,
              app.MyRoutes.User,
              cask.Request
            ]("user", "app.MyRoutes.User", scala.None, scala.None)(
              annotObject$macro$2.getParamParser[app.MyRoutes.User]
            )
          ),
          scala.collection.immutable.List(
            cask.router.ArgSig[
              annotObject$macro$1.InputTypeAlias,
              app.MyRoutes.type,
              Int,
              cask.Request
            ]("extra", "Int", scala.None, scala.None)(
              annotObject$macro$1.getParamParser[Int]
            )
          )
        ),
        scala.None,
        (
            (
                fresh$macro$4: app.MyRoutes.type,
                ctx$macro$7: cask.Request,
                argValues$macro$5: Seq[Map[String, Any]],
                argSigs$macro$6: scala.Seq[scala.Seq[
                  cask.router.ArgSig[Any, _$1, _$2, cask.Request] forSome {
                    type _$1;  // original has prefix of <<synthetic>>
                    type _$2 // original has prefix of <<synthetic>>
                  }
                ]]
            ) =>
              cask.router.Runtime
                .validate(
                  Seq(
                    cask.router.Runtime.makeReadCall(
                      argValues$macro$5(0),
                      ctx$macro$7,
                      scala.None,
                      argSigs$macro$6(0)(0)
                    ),
                    cask.router.Runtime.makeReadCall(
                      argValues$macro$5(1),
                      ctx$macro$7,
                      scala.None,
                      argSigs$macro$6(1)(0)
                    ),
                    cask.router.Runtime.makeReadCall(
                      argValues$macro$5(2),
                      ctx$macro$7,
                      scala.None,
                      argSigs$macro$6(2)(0)
                    )
                  )
                )
                .map(arguments match {  // originally the arguments was <empty>
                  case Seq((status @ _), (user @ _), (extra @ _)) =>
                    annotObject$macro$3.convertToResultType(
                      fresh$macro$4.echoStatus(status.asInstanceOf[String])(
                        user.asInstanceOf[app.MyRoutes.User]
                      )(extra.asInstanceOf[Int])
                    )
                })
        )
      )
    )
  })
}