Skip to content

Commit

Permalink
Create a more realistic tool
Browse files Browse the repository at this point in the history
  • Loading branch information
MrPowers committed Apr 28, 2021
1 parent dbacf23 commit b939a0f
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 30 deletions.
1 change: 0 additions & 1 deletion .scalafmt.conf
@@ -1,5 +1,4 @@
version = 2.6.3

align = more
maxColumn = 150
docstrings = JavaDoc
28 changes: 20 additions & 8 deletions src/main/scala/swoop/modelo/Bob.scala
@@ -1,13 +1,15 @@
package swoop.modelo

import scala.language.dynamics

case class ModeloBobException(smth: String) extends Exception(smth)
import org.apache.spark.sql.{DataFrame, SparkSession}

case class Bob(
templates: Map[String, String],
baseTemplateName: String,
inputParams: Map[String, List[String]] = Map.empty[String, List[String]],
required: Set[String] = Set.empty[String],
requireAtLeastOne: Set[String] = Set.empty[String]
requireAtLeastOne: Set[String] = Set.empty[String],
paramConverters: Map[String, List[String] => String] = Map.empty[String, List[String] => String]
) extends Dynamic {

def applyDynamic(name: String)(args: String*): Bob = {
Expand All @@ -19,11 +21,21 @@ case class Bob(
def requireAtLeastOne(value: Set[String]): Bob = copy(requireAtLeastOne = value)

def attributes: Map[String, Any] = {
if (!required.subsetOf(inputParams.keys.toSet))
throw ModeloBobException(s"You supplied these params [${inputParams.keys}] but all these are required [${required}]")
if (requireAtLeastOne.nonEmpty && requireAtLeastOne.intersect(inputParams.keys.toSet).isEmpty)
throw ModeloBobException(s"You supplied these params [${inputParams.keys}] but at least one of these are required [${requireAtLeastOne}]")
inputParams
ParamValidators.requireParams(inputParams, required)
ParamValidators.requireAtLeastOne(inputParams, requireAtLeastOne)
inputParams.map {
case (key, value) =>
(key, paramConverters(key)(value))
}
}

def render(): String = {
mustache(templates, baseTemplateName, attributes)
}

def dataframe: DataFrame = {
val spark = SparkSession.getActiveSession.get
spark.sql(render())
}

}
13 changes: 13 additions & 0 deletions src/main/scala/swoop/modelo/ParamConverters.scala
@@ -0,0 +1,13 @@
package swoop.modelo

object ParamConverters {

def multiMatch(strs: List[String]): String = {
strs.map(str => s"'${str}'").mkString("(", ",", ")")
}

def exactMatch(strs: List[String]): String = {
"'" + strs(0) + "'"
}

}
17 changes: 17 additions & 0 deletions src/main/scala/swoop/modelo/ParamValidators.scala
@@ -0,0 +1,17 @@
package swoop.modelo

case class ModeloValidationException(smth: String) extends Exception(smth)

object ParamValidators {

def requireAtLeastOne(params: Map[String, List[String]], required: Set[String]): Unit = {
if (required.nonEmpty && required.intersect(params.keys.toSet).isEmpty)
throw ModeloValidationException(s"You supplied these params [${params.keys}] but at least one of these are required [${required}]")
}

def requireParams(params: Map[String, List[String]], required: Set[String]): Unit = {
if (!required.subsetOf(params.keys.toSet))
throw ModeloValidationException(s"You supplied these params [${params.keys}] but all these are required [${required}]")
}

}
67 changes: 51 additions & 16 deletions src/test/scala/swoop/modelo/BobSpec.scala
@@ -1,41 +1,76 @@
package swoop.modelo

import com.github.mrpowers.spark.fast.tests.DataFrameComparer
import org.scalatest.{FunSpec, Matchers}

class BobSpec extends FunSpec with Matchers {
class BobSpec extends FunSpec with Matchers with SparkSessionTestWrapper with DataFrameComparer {

import spark.implicits._

it("dynamically builds input parameters") {
val b = Bob(required = Set("whatever", "cool"))
val someTool = Bob(
templates = Map("countryFiltered" -> "select * from my_table where country IN {{{countries}}}"),
baseTemplateName = "countryFiltered",
required = Set("whatever", "cool"),
paramConverters = Map("whatever" -> ParamConverters.multiMatch, "cool" -> ParamConverters.exactMatch)
)
val b = someTool
.whatever("aaa", "bbb")
.cool("ccc")
val expected = Map("whatever" -> List("aaa", "bbb"), "cool" -> List("ccc"))
val expected = Map("whatever" -> "('aaa','bbb')", "cool" -> "'ccc'")
b.attributes should be(expected)
}

it("can dynamically run queries") {
val df = Seq(
("li", "china"),
("luis", "colombia"),
("fernanda", "brasil")
).toDF("first_name", "country")
df.createOrReplaceTempView("my_table")
// technical users construct someTool
val someTool = Bob(
templates = Map("countryFiltered.mustache" -> "select * from my_table where country IN {{{countries}}}"),
baseTemplateName = "countryFiltered.mustache",
required = Set("countries"),
paramConverters = Map("countries" -> ParamConverters.multiMatch)
)
// less technical users run queries with this interface
val b = someTool.countries("china", "colombia")
val expected = Seq(
("li", "china"),
("luis", "colombia")
).toDF("first_name", "country")
assertSmallDataFrameEquality(b.dataframe, expected, orderedComparison = false)
}

it("errors out if a required param isn't supplied") {
val b = Bob(required = Set("fun"))
.whatever("aaa", "bbb")
val someTool = Bob(
templates = Map("countryFiltered.mustache" -> "select * from my_table where country IN {{{countries}}}"),
baseTemplateName = "countryFiltered.mustache",
required = Set("countries", "cat"),
paramConverters = Map("countries" -> ParamConverters.multiMatch, "cat" -> ParamConverters.exactMatch)
)
val b = someTool
.cool("ccc")
intercept[ModeloBobException] {
intercept[ModeloValidationException] {
b.attributes
}
}

it("errors out if none of the required params are supplied") {
val b = Bob(requireAtLeastOne = Set("fun"))
val someTool = Bob(
templates = Map("countryFiltered.mustache" -> "select * from my_table where country IN {{{countries}}}"),
baseTemplateName = "countryFiltered.mustache",
required = Set("countries"),
paramConverters = Map("countries" -> ParamConverters.multiMatch)
)
val b = someTool
.whatever("aaa", "bbb")
.cool("ccc")
intercept[ModeloBobException] {
intercept[ModeloValidationException] {
b.attributes
}
}

it("does not error out if at least one of the required params is supplied") {
val b = Bob(requireAtLeastOne = Set("cool"))
.whatever("aaa", "bbb")
.cool("ccc")
val expected = Map("whatever" -> List("aaa", "bbb"), "cool" -> List("ccc"))
b.attributes should be(expected)
}

}
10 changes: 5 additions & 5 deletions src/test/scala/swoop/modelo/ModeloSpec.scala
Expand Up @@ -8,7 +8,7 @@ class ModeloSpec extends FunSpec with Matchers with SparkSessionTestWrapper with
describe("mustache") {

it("generates a string from a template file") {
val path = os.pwd/"src"/"test"/"resources"/"template1.mustache"
val path = os.pwd / "src" / "test" / "resources" / "template1.mustache"
val expected = "select * from cool_view where age > 30"
mustache(path, Map("viewName" -> "cool_view", "minAge" -> 30)) should be(expected)
}
Expand All @@ -20,8 +20,8 @@ class ModeloSpec extends FunSpec with Matchers with SparkSessionTestWrapper with
}

it("can render a template with a partial for files") {
val path = os.pwd/"src"/"test"/"resources"/"base.mustache"
val res = mustache(path, Map("names" -> List(Map("name"-> "Marcela"), Map("name" -> "Luisa"))))
val path = os.pwd / "src" / "test" / "resources" / "base.mustache"
val res = mustache(path, Map("names" -> List(Map("name" -> "Marcela"), Map("name" -> "Luisa"))))
val expected =
"""<h2>Names</h2>
|<strong>Marcela</strong>
Expand All @@ -37,8 +37,8 @@ class ModeloSpec extends FunSpec with Matchers with SparkSessionTestWrapper with
|{{/names}}
|""".stripMargin
val templatePartial = "<strong>{{name}}</strong>"
val templates = Map("base.mustache" -> templateBase, "user.mustache"-> templatePartial)
val attrs =Map("names" -> List(Map("name"-> "Marcela"), Map("name" -> "Luisa")))
val templates = Map("base.mustache" -> templateBase, "user.mustache" -> templatePartial)
val attrs = Map("names" -> List(Map("name" -> "Marcela"), Map("name" -> "Luisa")))
val res = mustache(templates, "base.mustache", attrs)
val expected =
"""<h2>Names</h2>
Expand Down

0 comments on commit b939a0f

Please sign in to comment.