Skip to content
Browse files

Refactored a bunch of shit.

  • Loading branch information...
1 parent da693e5 commit fcda55a825ad8b89d09a7dac22c9083bb0c3269c @adamalix adamalix committed Mar 6, 2012
Showing with 232 additions and 136 deletions.
  1. +225 −135 src/main/scala/com/foursquare/slashem/Ast.scala
  2. +7 −1 src/main/scala/com/foursquare/slashem/Schema.scala
View
360 src/main/scala/com/foursquare/slashem/Ast.scala
@@ -1,99 +1,173 @@
-// Copyright 2011 Foursquare Labs Inc. All Rights Reserved.
+// Copyright 2012 Foursquare Labs Inc. All Rights Reserved.
package com.foursquare.slashem
-import org.elasticsearch.index.query.{QueryBuilder => ElasticQueryBuilder,
- FilterBuilder => ElasticFilterBuilder,
- QueryFilterBuilder,
- QueryStringQueryBuilder,
- BoolQueryBuilder,
- BoolFilterBuilder,
- BoostingQueryBuilder,
- AndFilterBuilder,
- OrFilterBuilder,
- RangeQueryBuilder,
- RangeFilterBuilder};
-
-import org.elasticsearch.index.query.QueryBuilders._;
+import org.elasticsearch.index.query.{FilterBuilder => ElasticFilterBuilder,
+ FilterBuilders => EFilterBuilders,
+ QueryBuilder => ElasticQueryBuilder,
+ QueryBuilders => EQueryBuilders,
+ QueryStringQueryBuilder}
+/**
+ * Abstract Syntax Tree used to represent queries.
+ */
object Ast {
val escapePattern = """\b(OR|AND|or|and)\b""".r
- // ripped from lucene source code QueryParser.java
- def escape(q: String) = {
+ /**
+ * Ripped from lucene source code QueryParser.java
+ * http://bit.ly/AzKzV3
+ *
+ * @param q Query string to sanitize
+ */
+ def escape(q: String): String = {
val sb = new StringBuilder()
- q.trim.foreach{c =>
+ q.trim.foreach{c: Char =>
// These characters are part of the query syntax and must be escaped
if (c == '\\' || c == '+' || c == '-' || c == '!' || c == '(' || c == ')' || c == ':'
|| c == '^' || c == '[' || c == ']' || c == '\"' || c == '{' || c == '}' || c == '~'
|| c == '*' || c == '?' || c == '|' || c == '&') {
sb.append('\\')
}
- //Added (not part of QueryParser.java)
+ // Added (not part of QueryParser.java)
if (c != '\u2019') {
sb.append(c)
}
}
- //Added (not part of QueryParser.java)
- escapePattern.replaceAllIn(sb.toString,m => "\"" + m.group(0) + "\"")
+ // Added (not part of QueryParser.java)
+ escapePattern.replaceAllIn(sb.toString, m => {"\"" + m.group(0) + "\""})
}
- def quote(q: String) = "\"" + q + "\""
-
+ /**
+ * Return the input string in an escaped quotes
+ *
+ * @param q Query string to quote
+ */
+ def quote(q: String): String = "\"" + q + "\""
+ /**
+ * AbstractClause represents query clauses.
+ */
abstract class AbstractClause {
- def extend: String
+ /**
+ * Returns the clause's string representation
+ */
+ def extend(): String
+ /**
+ * Extend the query into its elastic search format
+ *
+ * @param qf List of @see WeightedField
+ * @param pf List of @see PhraseWeightedField
+ * @param mm Option[String] representing the minimum match percentage
+ * @return ElasticQueryBuilder representing the clause
+ */
def elasticExtend(qf: List[WeightedField],
pf: List[PhraseWeightedField],
mm: Option[String]): ElasticQueryBuilder
- //By default we can just use the QueryFilterBuilder and the query extender
+
+ /**
+ * Creates a filter from a list of weighted fields.
+ *
+ * @param qf List of weighted fields used to build a filter list.
+ * @return ElasticFilterBuilder
+ */
def elasticFilter(qf: List[WeightedField]): ElasticFilterBuilder = {
- new QueryFilterBuilder(this.elasticExtend(qf, Nil, None))
+ // By default we can just use the QueryFilterBuilder and the query extender
+ EFilterBuilders.queryFilter(this.elasticExtend(qf, Nil, None))
}
- def or(clauses: AbstractClause*) = {
+
+ /**
+ * Generate an OrClause from a list of clauses
+ *
+ * @param clauses a list of abstract clauses to OR together
+ * @return OrClause inputs ORed together
+ */
+ def or(clauses: AbstractClause*): OrClause = {
OrClause(this::clauses.toList)
}
- def and(clauses: AbstractClause*) = {
+
+ /**
+ * Generate an AndClause from a list of clauses
+ *
+ * @param clauses a list of abstract clauses to AND together
+ * @return AbstractClause* ANDed together @see AndClause
+ */
+ def and(clauses: AbstractClause*): AndClause = {
AndClause(this::clauses.toList)
}
}
- //You can use a OrClause to join two clauses
+ /**
+ * Case class representing a list of clauses ORed together
+ *
+ * You can use a OrClause() to join two or more clauses with an OR
+ */
case class OrClause(clauses: List[AbstractClause]) extends AbstractClause {
+ /**
+ * @inheritdoc
+ */
def extend(): String = {
clauses.map(c => "(" + c.extend + ")").mkString(" OR ")
}
- def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = new BoolQueryBuilder()
- clauses.map(_.elasticExtend(qf, pf, mm)).map(q.should(_))
+
+ /**
+ * @inheritdoc
+ */
+ def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField],
+ mm: Option[String]): ElasticQueryBuilder = {
+ val q = EQueryBuilders.boolQuery
+ clauses.map(c => c.elasticExtend(qf, pf, mm)).map(eqb => q.should(eqb))
q
}
- //By default we can just use the QueryFilterBuilder and the query extender
+ /**
+ * @inheritdoc
+ * By default we can just use the QueryFilterBuilder and the query extender
+ */
override def elasticFilter(qf: List[WeightedField]): ElasticFilterBuilder = {
- new OrFilterBuilder(clauses.map(_.elasticFilter(qf)):_*)
+ EFilterBuilders.orFilter(clauses.map(_.elasticFilter(qf)):_*)
}
}
+
+ /**
+ * Case class representing a list of clauses ANDed together
+ */
case class AndClause(clauses: List[AbstractClause]) extends AbstractClause {
+ /**
+ * @inheritdoc
+ */
def extend(): String = {
- clauses.map(c => "(" + c.extend + ")").mkString(" AND ")
- }
- def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = new BoolQueryBuilder()
- clauses.map(_.elasticExtend(qf, pf, mm)).map(q.must(_))
- q
- }
- //By default we can just use the QueryFilterBuilder and the query extender
+ clauses.map(c => "(" + c.extend() + ")").mkString(" AND ")
+ }
+ /**
+ * @inheritdoc
+ */
+ def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField],
+ mm: Option[String]): ElasticQueryBuilder = {
+ val query = EQueryBuilders.boolQuery
+ // Extend the clauses and add them to the ElasticQueryBuilder
+ clauses.map(ac => ac.elasticExtend(qf, pf, mm)).map(eqb => query.must(eqb))
+ query
+ }
+
+ /**
+ * @inheritdoc
+ * By default we can just use the QueryFilterBuilder and the query extender
+ */
override def elasticFilter(qf: List[WeightedField]): ElasticFilterBuilder = {
val filters = clauses.map(_.elasticFilter(qf))
- new AndFilterBuilder(filters:_*)
+ EFilterBuilders.andFilter(filters:_*)
}
}
-
- // A 'Clause' is something of the form 'field:(query)'
- // @param plus Set true for regular required or false to negate.
+ /**
+ * A 'Clause' is something of the form 'field:(query)'
+ * @param plus Defaults to true, used to negate queries (by setting to false).
+ */
case class Clause[T](fieldName: String, query: Query[T], plus: Boolean = true) extends AbstractClause {
+ /**
+ * @inheritdoc
+ */
def extend(): String = {
val q = query match {
case Group(x) => query
@@ -102,8 +176,8 @@ object Ast {
}
//If a field does not have a name then do not attempt to specify it
val qstr = fieldName match {
- case "" => q.extend
- case x => x + ":" + q.extend
+ case "" => q.extend()
+ case x => x + ":" + q.extend()
}
plus match {
case true => qstr
@@ -114,33 +188,43 @@ object Ast {
}
}
- def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- //If its on a specific field support that
+ /**
+ * @inheritdoc
+ */
+ def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField],
+ mm: Option[String]): ElasticQueryBuilder = {
+ // Support extending specific fields.
val fields = fieldName match {
case "" => qf
case _ => List(WeightedField(fieldName))
}
val baseQuery = query.elasticExtend(fields, pf, mm)
plus match {
case true => baseQuery
- case false => (new BoolQueryBuilder()).mustNot(baseQuery)
+ case false => EQueryBuilders.boolQuery.mustNot(baseQuery)
}
}
+
+ /**
+ * @inheritdoc
+ */
override def elasticFilter(qf: List[WeightedField]): ElasticFilterBuilder = {
- //If its on a specific field support that
+ // Support extending specific fields.
val fields = fieldName match {
case "" => qf
case _ => List(WeightedField(fieldName))
}
- val baseQuery = query.elasticFilter(fields)
+ val baseFilters = query.elasticFilter(fields)
plus match {
- case true => baseQuery
- case false => (new BoolFilterBuilder()).mustNot(baseQuery)
+ case true => baseFilters
+ case false => EFilterBuilders.boolFilter.mustNot(baseFilters)
}
}
-
}
+ /**
+ * Class representing a field
+ */
case class Field(fieldName: String) extends ScoreBoost {
def extend(): String = {
fieldName
@@ -151,17 +235,17 @@ object Ast {
}
//A field with a query weight
- case class WeightedField(fieldName: String, boost: Double = 1) extends ScoreBoost {
+ case class WeightedField(fieldName: String, weight: Double = 1) extends ScoreBoost {
def extend(): String = {
- boost match {
+ weight match {
case 1.0 => fieldName
- case x => fieldName+"^"+x
+ case x: Double => fieldName + "^" + x.toString
}
}
def elasticExtend(): String = {
- boost match {
- case 1.0 => "(doc['"+fieldName+"'].value)"
- case _ => "(doc['"+fieldName+"'].value *"+boost+")"
+ weight match {
+ case 1.0 => "(doc['" + fieldName + "'].value)"
+ case _ => "(doc['" + fieldName + "'].value *" + weight.toString + ")"
}
}
}
@@ -170,101 +254,106 @@ object Ast {
//pf => traditional phrase query
//pf2 => in edismax type queries two word shingle matches
//pf3 => in edismax type queries three word shingle matches
- case class PhraseWeightedField(fieldName: String, boost: Double = 1, pf: Boolean, pf2: Boolean, pf3: Boolean) {
+ case class PhraseWeightedField(fieldName: String, weight: Double = 1, pf: Boolean, pf2: Boolean, pf3: Boolean) {
def extend(): String = {
- boost match {
- case 1 => fieldName
- case x => fieldName+"^"+x
+ weight match {
+ case 1.0 => fieldName
+ case x: Double => fieldName + "^" + x.toString
}
}
}
abstract class Query[T]() {
- def extend: String
+ def extend(): String
def and(c: Query[T]): Query[T] = And(this, c)
def or(c: Query[T]): Query[T] = Or(this, c)
def boost(b: Float): Query[T] = Boost(this, b)
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder
def elasticFilter(qf: List[WeightedField]): ElasticFilterBuilder = {
- new QueryFilterBuilder(this.elasticExtend(qf, Nil, None))
+ EFilterBuilders.queryFilter(this.elasticExtend(qf, Nil, None))
}
-
}
- //Name doesn't have to be a field name for Solr
- //it could be "lat,lng". However for ES it must be
- //a field
+ /**
+ * A class that provides and API for boosting the score of a field.
+ *
+ * Name doesn't have to be a field name for Solr
+ * it could be "lat,lng". However for ES it must be
+ * a field
+ */
abstract class ScoreBoost {
def extend(): String
def elasticExtend(): String
}
case class GeoDist(name: String, lat: Double, lng: Double, distType: String = "") extends ScoreBoost {
- def extend = { distType match {
- case "square" => "sqedist(%s,%s,%s)".format(lat,lng,name)
- case _ => "dist(2,%s,%s,%s)".format(lat,lng,name)
- }}
- def elasticExtend = {
+ def extend(): String = {
+ distType match {
+ case "square" => "sqedist(%s,%s,%s)".format(lat,lng,name)
+ case _ => "dist(2,%s,%s,%s)".format(lat,lng,name)
+ }
+ }
+ def elasticExtend(): String = {
val distanceInKm = "doc['%s'].distanceInKm(%s,%s)".format(name,lat,lng)
distType match {
case "square" => "pow(%s,2.0)".format(distanceInKm)
case _ => distanceInKm
}
}
}
- case class Recip(query: ScoreBoost, x: Int, y: Int, z: Int) extends ScoreBoost{
- def extend = "recip(%s,%d,%d,%d)".format(query.extend,x,y,z)
- def elasticExtend = "%d.0*pow(((%d.0*(%s))+%d.0),-1.0)".format(y,x,query.elasticExtend,z)
+
+ case class Recip(query: ScoreBoost, x: Int, y: Int, z: Int) extends ScoreBoost {
+ def extend: String = "recip(%s,%d,%d,%d)".format(query.extend, x, y, z)
+ def elasticExtend(): String = "%d.0*pow(((%d.0*(%s))+%d.0),-1.0)".format(y, x, query.elasticExtend(), z)
}
case class Empty[T]() extends Query[T] {
- def extend = "\"\""
+ def extend(): String = "\"\""
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = new QueryStringQueryBuilder(this.extend)
- qf.map(f => q.field(f.fieldName,f.boost.toFloat))
+ val q = EQueryBuilders.queryString(this.extend())
+ qf.map(f => q.field(f.fieldName,f.weight.toFloat))
q
}
}
case class Phrase[T](query: T, escaped: Boolean = true) extends Query[T] {
- def extend = {'"' + escape(query.toString) + '"'}
+ def extend(): String = {'"' + escape(query.toString) + '"'}
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = new QueryStringQueryBuilder(this.extend)
- qf.map(f => q.field(f.fieldName,f.boost.toFloat))
+ val q = EQueryBuilders.queryString(this.extend())
+ qf.map(f => q.field(f.fieldName,f.weight.toFloat))
q
}
}
case class PhrasePrefix[T](query: T, escaped: Boolean = true) extends Query[T] {
- def extend = {'"' + escape(query.toString) + '*' + '"'}
+ def extend(): String = {'"' + escape(query.toString) + '*' + '"'}
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = disMaxQuery()
+ val q = EQueryBuilders.disMaxQuery()
q.tieBreaker(1)
qf.map(f => {
- val basePhrase = textPhraseQuery(f.fieldName,this.extend)
- val phraseQuery = f.boost match {
- case 1 => basePhrase
- case _ => basePhrase.boost(f.boost.toFloat)
+ val basePhrase = EQueryBuilders.textPhraseQuery(f.fieldName, this.extend())
+ val phraseQuery = f.weight match {
+ case 1.0 => basePhrase
+ case _ => basePhrase.boost(f.weight.toFloat)
}
q.add(phraseQuery)
- }
- )
+ })
q
}
}
case class Range[T](q1: Query[T],q2: Query[T]) extends Query[T] {
- def extend = {'['+q1.extend+" TO "+ q2.extend +']'}
+ def extend(): String = {'[' + q1.extend() + " TO " + q2.extend() +']'}
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = new RangeQueryBuilder(qf.head.fieldName)
+ val q = EQueryBuilders.rangeQuery(qf.head.fieldName)
q.from(q1)
q.to(q2)
}
//By default we can just use the QueryFilterBuilder and the query extender
override def elasticFilter(qf: List[WeightedField]): ElasticFilterBuilder = {
- val q = new RangeFilterBuilder(qf.head.fieldName)
+ val q = EFilterBuilders.rangeFilter(qf.head.fieldName)
q.from(q1)
q.to(q2)
}
@@ -275,40 +364,40 @@ object Ast {
//and the list of phrase boost fields and generate
//corresponding phrase boost queries.
case class BagOfWords[T](query: T) extends Query[T] {
- def extend = escape(query.toString)
+ def extend(): String = escape(query.toString)
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val normalq = new QueryStringQueryBuilder(this.extend)
+ val normalq = EQueryBuilders.queryString(this.extend())
//If we are matching 100% then set operation to "and"
mm match {
case Some("100%") => normalq.defaultOperator(QueryStringQueryBuilder.Operator.AND)
case _ => {}
}
- qf.map(f => normalq.field(f.fieldName,f.boost.toFloat))
- val qfnames = qf.map(_.fieldName).toSet
+ // Update the normal query with the query fields' names and their weights
+ qf.map(f => normalq.field(f.fieldName,f.weight.toFloat))
+ val qfnames: Set[String] = qf.map(_.fieldName).toSet
val queriesToGen = pf.filter(qfnames contains _.fieldName)
- queriesToGen match {
- case Nil => normalq
- case _ => {
- val q = disMaxQuery()
- q.tieBreaker(1)
- q.add(normalq)
- queriesToGen.map(f => {
- val basePhrase = textPhraseQuery(f.fieldName,this.extend)
- val phraseQuery = f.boost match {
- case 1 => basePhrase
- case _ => basePhrase.boost(f.boost.toFloat)
- }
- q.add(phraseQuery)
+ queriesToGen match {
+ case Nil => normalq
+ case _ => {
+ val q = EQueryBuilders.disMaxQuery
+ q.tieBreaker(1)
+ q.add(normalq)
+ queriesToGen.map(pwf => {
+ val basePhrase = EQueryBuilders.textPhraseQuery(pwf.fieldName,this.extend())
+ val phraseQuery = pwf.weight match {
+ case 1 => basePhrase
+ case _ => basePhrase.boost(pwf.weight.toFloat)
}
- )
- q
- }
+ q.add(phraseQuery)
+ })
+ q
}
+ }
}
}
case class Group[T](items: Query[T]) extends Query[T] {
- def extend = {"(%s)".format(items.extend)}
+ def extend(): String = {"(%s)".format(items.extend)}
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
items.elasticExtend(qf, pf, mm)
}
@@ -335,54 +424,55 @@ object Ast {
case class And[T](queries: Query[T]*) extends Query[T] {
def extend(): String = {
- "(" + queries.map(c => c.extend).mkString(" AND ") + ")"
+ "(" + queries.map(c => c.extend()).mkString(" AND ") + ")"
}
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = new BoolQueryBuilder()
- queries.map(_.elasticExtend(qf, pf, mm)).map(q.must(_))
- q
+ val eqb = EQueryBuilders.boolQuery
+ // Extend the queries and add them to the ElasticQueryBuilder
+ queries.map(q => q.elasticExtend(qf, pf, mm)).map(qb => eqb.must(qb))
+ eqb
}
//By default we can just use the QueryFilterBuilder and the query extender
override def elasticFilter(qf: List[WeightedField]): ElasticFilterBuilder = {
- new AndFilterBuilder(queries.map(_.elasticFilter(qf)):_*)
+ EFilterBuilders.andFilter(queries.map(_.elasticFilter(qf)):_*)
}
}
case class Or[T](queries: Query[T]*) extends Query[T] {
def extend(): String = {
- queries.map(c => c.extend).mkString(" OR ")
+ queries.map(c => c.extend()).mkString(" OR ")
}
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = new BoolQueryBuilder()
- queries.map(_.elasticExtend(qf, pf, mm)).map(q.should(_))
- q
+ val query = EQueryBuilders.boolQuery
+ queries.map(q => q.elasticExtend(qf, pf, mm)).map(q => query.should(q))
+ query
}
//By default we can just use the QueryFilterBuilder and the query extender
override def elasticFilter(qf: List[WeightedField]): ElasticFilterBuilder = {
- new OrFilterBuilder(queries.map(_.elasticFilter(qf)):_*)
+ EFilterBuilders.orFilter(queries.map(_.elasticFilter(qf)):_*)
}
}
case class Splat[T]() extends Query[T] {
- def extend = "*"
+ def extend(): String = "*"
//Is there a better way to do this?
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val q = new QueryStringQueryBuilder(this.extend)
- qf.map(f => q.field(f.fieldName,f.boost.toFloat))
+ val q = EQueryBuilders.queryString(this.extend())
+ qf.map(f => q.field(f.fieldName,f.weight.toFloat))
q
}
}
- case class Boost[T](q: Query[T], boost: Float) extends Query[T] {
- def extend = q.extend + "^" + boost.toString
+ case class Boost[T](q: Query[T], weight: Float) extends Query[T] {
+ def extend(): String = q.extend() + "^" + weight.toString
def elasticExtend(qf: List[WeightedField], pf: List[PhraseWeightedField], mm: Option[String]): ElasticQueryBuilder = {
- val boostedQuery = new BoostingQueryBuilder()
- if (boost > 0) {
+ val boostedQuery = EQueryBuilders.boostingQuery
+ if (weight > 0) {
boostedQuery.positive(q.elasticExtend(qf, pf, mm))
} else {
boostedQuery.negative(q.elasticExtend(qf, pf, mm))
}
- boostedQuery.boost(boost.abs)
+ boostedQuery.boost(weight.abs)
boostedQuery
}
}
View
8 src/main/scala/com/foursquare/slashem/Schema.scala
@@ -625,7 +625,7 @@ trait SolrSchema[M <: Record[M]] extends SlashemSchema[M] {
//Boost queries only impact scoring
val bq = qb.boostQueries.map({ x => ("bq" -> x.extend)})
- val qf = qb.queryFields.filter({x => x.boost != 0}).map({x => ("qf" -> x.extend)})
+ val qf = qb.queryFields.filter({x => x.weight != 0}).map({x => ("qf" -> x.extend)})
val pf = qb.phraseBoostFields.filter(x => x.pf).map({x => ("pf" -> x.extend)})++
qb.phraseBoostFields.filter(x => x.pf2).map({x => ("pf2" -> x.extend)})++
@@ -779,6 +779,9 @@ class SlashemIntListField[T <: Record[T]](owner: T) extends IntListField[T](owne
case _ => Empty
}
}
+/* override def contains(v: Iterable[Int]) = {
+ Clause[Iterable[Int]](queryName, Helpers.groupWithOr(v.map({x: Int => BagOfWords(x)})))
+ }*/
}
class SlashemStringListField[T <: Record[T]](owner: T) extends StringListField[T](owner) with SlashemField[List[String], T] {
@@ -985,6 +988,9 @@ class StringListField[T <: Record[T]](override val owner: T) extends Field[List[
case _ => Empty
}
}
+/* def contains(lst: Seq[String]) = {
+ Clause[Seq[String]]
+ }*/
override def setFromJValue(jv: net.liftweb.json.JsonAST.JValue) = Empty
override def liftSetFilterToBox(a: Box[ValueType]) = Empty
override def toBoxMyType(a: ValueType) = Empty

0 comments on commit fcda55a

Please sign in to comment.
Something went wrong with that request. Please try again.