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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
|
package forge.discover
import ammonite.main.Compat
import forge.define.Target
import sourcecode.Compat.Context
import scala.annotation.StaticAnnotation
import scala.collection.mutable
import scala.language.experimental.macros
/**
* More or less a minimal version of Autowire's Server that lets you generate
* a set of "routes" from the methods defined in an object, and call them
* using passing in name/args/kwargs via Java reflection, without having to
* generate/compile code or use Scala reflection. This saves us spinning up
* the Scala compiler and greatly reduces the startup time of cached scripts.
*/
object Router{
class doc(s: String) extends StaticAnnotation
class main extends StaticAnnotation
def generateRoutes[T]: Seq[Router.EntryPoint[T]] = macro generateRoutesImpl[T]
def generateRoutesImpl[T: c.WeakTypeTag](c: Context): c.Expr[Seq[EntryPoint[T]]] = {
import c.universe._
val r = new Router(c)
val allRoutes = r.getAllRoutesForClass(
weakTypeOf[T].asInstanceOf[r.c.Type],
).asInstanceOf[Iterable[c.Tree]]
c.Expr[Seq[EntryPoint[T]]](q"_root_.scala.Seq(..$allRoutes)")
}
/**
* Models what is known by the router about a single argument: that it has
* a [[name]], a human-readable [[typeString]] describing what the type is
* (just for logging and reading, not a replacement for a `TypeTag`) and
* possible a function that can compute its default value
*/
case class ArgSig[T](name: String,
typeString: String,
doc: Option[String],
default: Option[T => Any])
def stripDashes(s: String) = {
if (s.startsWith("--")) s.drop(2)
else if (s.startsWith("-")) s.drop(1)
else s
}
/**
* What is known about a single endpoint for our routes. It has a [[name]],
* [[argSignatures]] for each argument, and a macro-generated [[invoke0]]
* that performs all the necessary argument parsing and de-serialization.
*
* Realistically, you will probably spend most of your time calling [[invoke]]
* instead, which provides a nicer API to call it that mimmicks the API of
* calling a Scala method.
*/
case class EntryPoint[T](name: String,
argSignatures: Seq[ArgSig[T]],
doc: Option[String],
varargs: Boolean,
invoke0: (T, Map[String, String], Seq[String]) => Result[Target[Any]]){
def invoke(target: T, groupedArgs: Seq[(String, Option[String])]): Result[Target[Any]] = {
var remainingArgSignatures = argSignatures.toList
val accumulatedKeywords = mutable.Map.empty[ArgSig[T], mutable.Buffer[String]]
val keywordableArgs = if (varargs) argSignatures.dropRight(1) else argSignatures
for(arg <- keywordableArgs) accumulatedKeywords(arg) = mutable.Buffer.empty
val leftoverArgs = mutable.Buffer.empty[String]
val lookupArgSig = argSignatures.map(x => (x.name, x)).toMap
var incomplete: Option[ArgSig[T]] = None
for(group <- groupedArgs){
group match{
case (value, None) =>
if (value(0) == '-' && !varargs){
lookupArgSig.get(stripDashes(value)) match{
case None => leftoverArgs.append(value)
case Some(sig) => incomplete = Some(sig)
}
} else remainingArgSignatures match {
case Nil => leftoverArgs.append(value)
case last :: Nil if varargs => leftoverArgs.append(value)
case next :: rest =>
accumulatedKeywords(next).append(value)
remainingArgSignatures = rest
}
case (rawKey, Some(value)) =>
val key = stripDashes(rawKey)
lookupArgSig.get(key) match{
case Some(x) if accumulatedKeywords.contains(x) =>
if (accumulatedKeywords(x).nonEmpty && varargs){
leftoverArgs.append(rawKey, value)
}else{
accumulatedKeywords(x).append(value)
remainingArgSignatures = remainingArgSignatures.filter(_.name != key)
}
case _ =>
leftoverArgs.append(rawKey, value)
}
}
}
val missing0 = remainingArgSignatures.filter(_.default.isEmpty)
val missing = if(varargs) {
missing0.filter(_ != argSignatures.last)
} else {
missing0.filter(x => incomplete != Some(x))
}
val duplicates = accumulatedKeywords.toSeq.filter(_._2.length > 1)
if (
incomplete.nonEmpty ||
missing.nonEmpty ||
duplicates.nonEmpty ||
(leftoverArgs.nonEmpty && !varargs)
){
Result.Error.MismatchedArguments(
missing = missing,
unknown = leftoverArgs,
duplicate = duplicates,
incomplete = incomplete
)
} else {
val mapping = accumulatedKeywords
.iterator
.collect{case (k, Seq(single)) => (k.name, single)}
.toMap
try invoke0(target, mapping, leftoverArgs)
catch{case e: Throwable =>
Result.Error.Exception(e)
}
}
}
}
def tryEither[T](t: => T, error: Throwable => Result.ParamError) = {
try Right(t)
catch{ case e: Throwable => Left(error(e))}
}
def readVarargs[T](arg: ArgSig[_],
values: Seq[String],
thunk: String => T) = {
val attempts =
for(item <- values)
yield tryEither(thunk(item), Result.ParamError.Invalid(arg, item, _))
val bad = attempts.collect{ case Left(x) => x}
if (bad.nonEmpty) Left(bad)
else Right(attempts.collect{case Right(x) => x})
}
def read[T](dict: Map[String, String],
default: => Option[Any],
arg: ArgSig[_],
thunk: String => T): FailMaybe = {
dict.get(arg.name) match{
case None =>
tryEither(default.get, Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_))
case Some(x) =>
tryEither(thunk(x), Result.ParamError.Invalid(arg, x, _)).left.map(Seq(_))
}
}
/**
* Represents what comes out of an attempt to invoke an [[EntryPoint]].
* Could succeed with a value, but could fail in many different ways.
*/
sealed trait Result[+T]
object Result{
/**
* Invoking the [[EntryPoint]] was totally successful, and returned a
* result
*/
case class Success[T](value: T) extends Result[T]
/**
* Invoking the [[EntryPoint]] was not successful
*/
sealed trait Error extends Result[Nothing]
object Error{
/**
* Invoking the [[EntryPoint]] failed with an exception while executing
* code within it.
*/
case class Exception(t: Throwable) extends Error
/**
* Invoking the [[EntryPoint]] failed because the arguments provided
* did not line up with the arguments expected
*/
case class MismatchedArguments(missing: Seq[ArgSig[_]],
unknown: Seq[String],
duplicate: Seq[(ArgSig[_], Seq[String])],
incomplete: Option[ArgSig[_]]) extends Error
/**
* Invoking the [[EntryPoint]] failed because there were problems
* deserializing/parsing individual arguments
*/
case class InvalidArguments(values: Seq[ParamError]) extends Error
}
sealed trait ParamError
object ParamError{
/**
* Something went wrong trying to de-serialize the input parameter;
* the thrown exception is stored in [[ex]]
*/
case class Invalid(arg: ArgSig[_], value: String, ex: Throwable) extends ParamError
/**
* Something went wrong trying to evaluate the default value
* for this input parameter
*/
case class DefaultFailed(arg: ArgSig[_], ex: Throwable) extends ParamError
}
}
type FailMaybe = Either[Seq[Result.ParamError], Any]
type FailAll = Either[Seq[Result.ParamError], Seq[Any]]
def validate(args: Seq[FailMaybe]): Result[Seq[Any]] = {
val lefts = args.collect{case Left(x) => x}.flatten
if (lefts.nonEmpty) Result.Error.InvalidArguments(lefts)
else {
val rights = args.collect{case Right(x) => x}
Result.Success(rights)
}
}
}
class Router [C <: Context](val c: C) {
import c.universe._
def getValsOrMeths(curCls: Type): Iterable[MethodSymbol] = {
def isAMemberOfAnyRef(member: Symbol) = {
weakTypeOf[AnyRef].members.exists(_.name == member.name)
}
val extractableMembers = for {
member <- curCls.members
if !isAMemberOfAnyRef(member)
if !member.isSynthetic
if member.isPublic
if member.isTerm
memTerm = member.asTerm
if memTerm.isMethod
} yield memTerm.asMethod
extractableMembers flatMap { case memTerm =>
if (memTerm.isSetter || memTerm.isConstructor || memTerm.isGetter) Nil
else Seq(memTerm)
}
}
def extractMethod(meth: MethodSymbol, curCls: c.universe.Type): c.universe.Tree = {
val flattenedArgLists = meth.paramss.flatten
def hasDefault(i: Int) = {
val defaultName = s"${meth.name}$$default$$${i + 1}"
if (curCls.members.exists(_.name.toString == defaultName)) Some(defaultName)
else None
}
val argListSymbol = q"${c.fresh[TermName]("argsList")}"
val extrasSymbol = q"${c.fresh[TermName]("extras")}"
val defaults = for ((arg, i) <- flattenedArgLists.zipWithIndex) yield {
val arg = TermName(c.freshName())
hasDefault(i).map(defaultName => q"($arg: $curCls) => $arg.${newTermName(defaultName)}")
}
def getDocAnnotation(annotations: List[Annotation]) = {
val (docTrees, remaining) = annotations.partition(_.tpe =:= typeOf[Router.doc])
val docValues = for {
doc <- docTrees
if doc.scalaArgs.head.isInstanceOf[Literal]
l = doc.scalaArgs.head.asInstanceOf[Literal]
if l.value.value.isInstanceOf[String]
} yield l.value.value.asInstanceOf[String]
(remaining, docValues.headOption)
}
def unwrapVarargType(arg: Symbol) = {
val vararg = arg.typeSignature.typeSymbol == definitions.RepeatedParamClass
val unwrappedType =
if (!vararg) arg.typeSignature
else arg.typeSignature.asInstanceOf[TypeRef].args(0)
(vararg, unwrappedType)
}
val (_, methodDoc) = getDocAnnotation(meth.annotations)
val readArgSigs = for(
((arg, defaultOpt), i) <- flattenedArgLists.zip(defaults).zipWithIndex
) yield {
val (vararg, varargUnwrappedType) = unwrapVarargType(arg)
val default =
if (vararg) q"scala.Some(scala.Nil)"
else defaultOpt match {
case Some(defaultExpr) => q"scala.Some($defaultExpr())"
case None => q"scala.None"
}
val (docUnwrappedType, docOpt) = varargUnwrappedType match{
case t: AnnotatedType =>
val (remaining, docValue) = getDocAnnotation(t.annotations)
if (remaining.isEmpty) (t.underlying, docValue)
else (Compat.copyAnnotatedType(c)(t, remaining), docValue)
case t => (t, None)
}
val docTree = docOpt match{
case None => q"scala.None"
case Some(s) => q"scala.Some($s)"
}
val argSig = q"""
forge.discover.Router.ArgSig(
${arg.name.toString},
${docUnwrappedType.toString + (if(vararg) "*" else "")},
$docTree,
$defaultOpt
)
"""
val reader =
if(vararg) q"""
forge.discover.Router.readVarargs[$docUnwrappedType](
$argSig,
$extrasSymbol,
implicitly[scopt.Read[$docUnwrappedType]].reads(_)
)
""" else q"""
forge.discover.Router.read[$docUnwrappedType](
$argListSymbol,
$default,
$argSig,
implicitly[scopt.Read[$docUnwrappedType]].reads(_)
)
"""
(reader, argSig, vararg)
}
val (readArgs, argSigs, varargs) = readArgSigs.unzip3
val (argNames, argNameCasts) = flattenedArgLists.map { arg =>
val (vararg, unwrappedType) = unwrapVarargType(arg)
(
pq"${arg.name.toTermName}",
if (!vararg) q"${arg.name.toTermName}.asInstanceOf[$unwrappedType]"
else q"${arg.name.toTermName}.asInstanceOf[Seq[$unwrappedType]]: _*"
)
}.unzip
val arg = TermName(c.freshName())
q"""
forge.discover.Router.EntryPoint(
${meth.name.toString},
scala.Seq(..$argSigs),
${methodDoc match{
case None => q"scala.None"
case Some(s) => q"scala.Some($s)"
}},
${varargs.contains(true)},
($arg: $curCls, $argListSymbol: Map[String, String], $extrasSymbol: Seq[String]) =>
forge.discover.Router.validate(Seq(..$readArgs)) match{
case forge.discover.Router.Result.Success(List(..$argNames)) =>
forge.discover.Router.Result.Success($arg.${meth.name.toTermName}(..$argNameCasts))
case x: forge.discover.Router.Result.Error => x
}
)
"""
}
def getAllRoutesForClass(curCls: Type): Iterable[c.universe.Tree] = {
for{
t <- getValsOrMeths(curCls)
if t.annotations.exists(_.tpe =:= typeOf[Router.main])
} yield {
extractMethod(t, curCls)
}
}
}
|