Scala Quill filtering
===
If we only needed `from` and `MicroserviceName` it would be simple:
```scala
def listMicroservices(svcName: Option[ServiceName], msvcName: Option[MicroserviceName], from: Option[MicroserviceName]) = {
run(quote {
for {
s <- services.filter(s => lift(svcName).forall(_ == s.name))
m <- microservices
.filter(m => lift(msvcName).forall(_ == m.name))
.filter(m => lift(from).forall(_ < m.name)) // <---
.filter(_.serviceId == s.id)
} yield m
})
}
```
But since users can sort on an arbitrary field, `from` needs to be `Option[String]`, in which case it needs to be mapped with `MicroserviceName.apply`:
```scala
.filter(m => lift(from.map(MicroserviceName.apply)).forall(_ < m.name))
```
Now, unless we want `if()` blocks or `case` switches based on the `sortBy` field, both `MicroserviceName.apply` and `.name` will have to be method references:
```scala
val apply = MicroserviceName.apply
val nameGetter = (m: Microservice) => m.name
.filter(m => lift(from.map(apply)).forall(_ < getter(m)))
```
Unfortunately, quill doesn't let you make arbitrary function calls like that. You need to do something like this:
```scala
def withFilter[T] = quote {
(q: Query[T], f: Quoted[T => Boolean]) => q.filter(f(_))
}
def listMicroservices(svcName: Option[ServiceName], msvcName: Option[MicroserviceName], from: Option[String]) = {
val apply = MicroserviceName.apply
val nameGetter = (m: Microservice) => m.name
run(quote {
for {
s <- services.filter(s => lift(svcName).forall(_ == s.name))
m <- microservices
.filter(m => lift(msvcName).forall(_ == m.name))
.filter(_.serviceId == s.id)
} yield m
withFilter(microservices,(m: Microservice) => lift(from.map(apply)).forall(_ < nameGetter(m))) // <--
})
}
```
Of course, it's still specific to `Microservice` and `<`. To make it fully generic, we get this sweet Scala code. It's probably overboard.
```scala
/**
* Creates a (quoted) filter for the given operation. For example, if you want to check that a Microservice has a
* name that is less than the given value, you would invoke
*
* fieldOp(value, (m: Microservice) => m.name, MicroserviceName.apply, lt)
*
* (Also see the definition of `lt`.)
*/
def fieldOp[T, F <: AnyVal](value: Option[String], getter: T => F, apply: String => F, op: (F, F) => Quoted[Boolean]): Quoted[T => Boolean] =
(t: T) => lift(value.map(apply)).forall(op(_, getter(t)))
def withFilter[T] = quote {
(q: Query[T], f: Quoted[T => Boolean]) => q.filter(f(_))
}
def lt = (x: AnyVal, y: AnyVal) => AnyValQuotes(x) < y
def gt = (x: AnyVal, y: AnyVal) => AnyValQuotes(x) > y
def msvcNameComp(value: Option[String], op: (MicroserviceName, MicroserviceName) => Quoted[Boolean]): Quoted[Microservice => Boolean] =
fieldOp(value, (m: Microservice) => m.name, MicroserviceName.apply, op)
def listMicroservices(svcName: Option[ServiceName], msvcName: Option[MicroserviceName], from: Option[String]) = {
run(quote {
for {
s <- services.filter(s => lift(svcName).forall(_ == s.name))
m <- microservices
.filter(m => lift(msvcName).forall(_ == m.name))
.filter(_.serviceId == s.id)
} yield m
withFilter(microservices, msvcNameComp(from, lt))
})
}
```
**Update (2020-11-06 16:45 CT)**
It can also be done like this. The operator (`<`) is not generic, but it's cleaner:
```scala
def listMicroservices(svcName: Option[ServiceName], msvcName: Option[MicroserviceName],
from: Option[String], to: Option[String]) = {
val apply: Quoted[String => MicroserviceName] = MicroserviceName.apply(_)
val fieldGetter: Microservice => MicroserviceName = (m: Microservice) => m.name
val q = quote {
for {
s <- services.filter(s => lift(svcName).forall(_ == s.name))
m <- microservices
.filter(m => lift(msvcName).forall(_ == m.name))
.filter(m => lift(from.map(apply)).forall(_ < lift(fieldGetter(m))))
.filter(m => lift(to.map(apply)).forall(_ > lift(fieldGetter(m))))
.filter(_.serviceId == s.id)
} yield m
}
run(q)
}
```
Unfortunately this doesn't work (`Can't find Encoder for type 'AnyVal'`):
```scala
def listMicroservices(svcName: Option[ServiceName], msvcName: Option[MicroserviceName],
from: Option[String], to: Option[String], sortBy: SortField = SortField.Name) = {
var apply: Quoted[String => AnyVal] = null
var fieldGetter: Microservice => AnyVal = null
sortBy match {
case SortField.Name =>
apply = MicroserviceName.apply(_)
fieldGetter = (m: Microservice) => m.name
case SortField.Id =>
apply = MicroserviceId.apply(_)
fieldGetter = (m: Microservice) => m.id
}
val q = quote {
for {
s <- services.filter(s => lift(svcName).forall(_ == s.name))
m <- microservices
.filter(m => lift(msvcName).forall(_ == m.name))
.filter(m => lift(from.map(apply)).forall(_ < lift(fieldGetter(m))))
.filter(m => lift(to.map(apply)).forall(_ > lift(fieldGetter(m))))
.filter(_.serviceId == s.id)
} yield m
}
run(q)
}
```
So the query has to be broken up:
```scala
def listMicroservices(svcName: Option[ServiceName], msvcName: Option[MicroserviceName],
from: Option[String], to: Option[String], sortBy: SortField = SortField.Name,
limit: Int = 50) = {
val q = quote {
for {
s <- services
.filter(s => lift(svcName).forall(_ == s.name))
m <- microservices
.filter(m => lift(msvcName).forall(_ == m.name))
.filter(_.serviceId == s.id)
.take(limit)
} yield m
}
val r = sortBy match {
case SortField.Name => q.dynamic
.filter(m => lift(from.map(MicroserviceName.apply)).forall(_ < m.name))
.filter(m => lift(to.map(MicroserviceName.apply)).forall(_ > m.name))
.sortBy(_.name)
case SortField.Id => q.dynamic
.filter(m => lift(from.map(MicroserviceId.apply)).forall(_ < m.id))
.filter(m => lift(to.map(MicroserviceId.apply)).forall(_ > m.id))
.sortBy(_.id)
}
run(r)
}
```