Cask code analysis for data marshalling. It covers the general data mechanism and the JSON data marshalling. Use def scalacOptions = Seq("-Xprint:typer") to see the implicit converters being applied.

1 The General Response.Data

The Data is defined inside the object Response, it is a path-dependent type. An entry point’s result is converted into an HTTP endpoint’s Response.Raw using the convertToResultType method. The method takes an implicit parameter: implicit f: Conversion[T, InnerReturned]).

1
2
3
4
class Conversion[T, V](val f: T => V)
object Conversion{
  implicit def create[T, V](implicit f: T => V) = new Conversion(f)
}

Therefore, it looks for an implicit f: T => V. Usually the target type V should provide one implicit converter. For an HTTP endpint that has an InnerReturned of String, the expanded call becomes:

1
2
3
4
5
6
7
internal.this.Conversion.create[String, cask.model.Response.Raw]((
  (t: String) => Response.this.Data.dataResponse[String](t)((
    (s: String) => Response.this.Data.WritableData[String](s)(
      ((s: String) => geny.this.Writable.StringWritable(s))
      )
    ))
  ))

Four levels of implicit parameters involvled in this call, the create itself is an implict method.

If you define a convertToResultType method in a custom endpoint and calls Response(data), it uses one of the three implicit classes defined in Response.Data object: WritableData[T], NumericData[T: Numeric] and BooleanData(s: Boolean). There are two levels of implicit parameters.

1
2
3
Response.this.Data.WritableData[String](t)((
  (s: String) => geny.this.Writable.StringWritable(s)
))

The WritableData[T] has an implicit parameter implicit f: T => geny.Writable. The geny.Readable defines three implict classes: StringReadable(s: String), ByteArrayReadable(a: Array[Byte]) and class InputStreamReadable(i: InputStream) that convert String, Array[Byte] and InputStream into a Readable, which is a subtype of Writable.

2 postJson

The postJson extends HttpEndpoint[Response[JsonData], ujson.Value]. From input to output, it has three steps:

  • read the input to Map[String, ujson.Value]
  • convert ujson.Value to parameter types of its entry point method
  • convert entry point’s result into Response[JsonData]

For a custom type, just declare a serializer and cask and upickle will handle the marshalling :

1
2
3
4
5
6
import upickle.default.{ReadWriter => RW, macroRW}

case class Thing(myFieldA: Int, myFieldB: String)
object Thing{
  implicit val rw: RW[Thing] = macroRW
}

2.1 The Input Type

Its Input type is ujson.Value. During its wrapFunction, it

  • coverts the request Request#exchange.getInputStream into ByteArrayOutputtream
  • then into ujson.Value
  • then Map[String, uJson.Value] - it is the paramst to entry point in the wrapFunction method: delegate(params).

You don’t need to do anything here. If it failed, it terminates the request and resonse with a 400->BadRequest error.

2.2 ArgSig

The next step is to convert an ujson.Value to a parameter type of the entry point method. For each parameter of the entry point method, the cask macros creates an ArgSig instance. For example, the following code:

1
2
3
4
@cask.postJson("/json")
def jsonEndpoint(value1: ujson.Value, value2: Seq[Int]) = {
  "OK " + value1 + " " + value2
}

has the following ArgSig:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
val annotObject$macro$1 = new cask.endpoints.postJson(
  "/json",
  endpoints.this.postJson.init$default$2
);
cask.router.ArgSig[
    annotObject$macro$1.InputTypeAlias,
    app.FormJsonPost.type,
    ujson.Value,
    cask.Request
  ]("value1", "ujson.Value", scala.None, scala.None)(
    annotObject$macro$1.getParamParser[ujson.Value]
  ),
  cask.router.ArgSig[
    annotObject$macro$1.InputTypeAlias,
    app.FormJsonPost.type,
    Seq[Int],
    cask.Request
  ]("value2", "Seq[Int]", scala.None, scala.None)(
    annotObject$macro$1.getParamParser[Seq[Int]]
  )
)

The getParamParser uses an implicit parmeter of type InputParser[T] that is defined as JsReader[T] in the postJson end point class. The JsReader object derfines two implicit instances:

  • defaultJsReader[T]: convert ujson.Value to T. Requires an implicit upickle.defualt.Reader instance in scope. ujson.Value is a subtype of Reader.
  • paramReader: convert values from the request url or body to T, for all non-ujson.Value types.

The implicit parameter for value1 is endpoints.this.JsReader.defaultJsReader[ujson.Value](upickle.this.default.JsValueR)). For value2, it is endpoints.this.JsReader.defaultJsReader[Seq[Int]](upickle.this.default.SeqLikeReader[Seq, Int](upickle.this.default.IntReader, immutable.this.Seq.iterableFactory[Int])).

The implicitly[upickle.default.Reader[T]] matches default.JsValueR and default.SeqLikeReader[Seq, Int].

If cask fails to convert parameter type, it returns Result.Error.InvalidArguments(Seq[ParamError]). The ParamError could be DefaultFailed or Invalid – both result in a 400 error.

2.3 Response[JsonData]

For return data, the call is

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
annotObject$macro$1.convertToResultType[String](
  fresh$macro$2.jsonEndpoint(
  value1.asInstanceOf[ujson.Value],
  value2.asInstanceOf[Seq[Int]]))

  (internal.this.Conversion.create[String, cask.model.Response[cask.endpoints.JsonData]]((
    (t: String) => endpoints.this.JsonData.dataResponse[String](t)((
      (t: String) => endpoints.this.JsonData.JsonDataImpl[String](t)(
        upickle.this.default.StringWriter
        )
      ))
    ))
  )

Therefore for JSON data, It uses