Skip to content

Commit

Permalink
Mustachio (#25)
Browse files Browse the repository at this point in the history
Adds in a Mustache template engine, tested against the official spec.
  • Loading branch information
alterationx10 authored Jan 16, 2025
1 parent 2a0bc7c commit 078c45e
Show file tree
Hide file tree
Showing 24 changed files with 2,350 additions and 12 deletions.
4 changes: 2 additions & 2 deletions branch/project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@

//> using repository sonatype:public

//> using test.dep org.scalameta::munit:1.0.3
//> using test.dep org.scalameta::munit:1.0.4
//> using test.dep org.testcontainers:testcontainers:1.20.4
//> using test.dep org.postgresql:postgresql:42.7.4
//> using test.dep org.postgresql:postgresql:42.7.5
//> using test.resourceDir ../test_resources

//> using publish.version 0.0.1-SNAPSHOT
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import dev.wishingtree.branch.friday.Json
import dev.wishingtree.branch.friday.{Json, JsonCodec}
import munit.FunSuite

class JsonSpec extends FunSuite {
Expand All @@ -17,4 +17,40 @@ class JsonSpec extends FunSuite {
} yield assertEquals(person, Person("Alice", 42))
}

test("Parses \"") {
val json =
"""
| {
| "name": "Ampersand",
| "desc": "Ampersand should interpolate without HTML escaping.",
| "data": {
| "forbidden": "& \" < >"
| },
| "template": "These characters should not be HTML escaped: {{&forbidden}}\n",
| "expected": "These characters should not be HTML escaped: & \" < >\n"
| }
|""".stripMargin

val result = Json.parse(json)
assert(result.isRight)
}

test("Derive a Decoder that has a Json field") {
case class JsClass(name: String, json: Json) derives JsonCodec
val json =
"""
|{
| "name": "Alice",
| "json": {
| "age": 42
| }
|}
|""".stripMargin
assert(Json.parse(json).isRight)
// We can parse the json, but there's an issue with the decoder
val decoder = summon[JsonCodec[JsClass]]
assert(decoder.decode(json).isSuccess)
println(decoder.decode(json))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ trait Parsers[Parser[+_]] {

def quoted: Parser[String] = string("\"") *> thru("\"").map(_.dropRight(1))

def escapedThru(s: String) = regex(
("((?:[^\"\\\\]|\\\\.)*)" + Pattern.quote(s)).r
)

def escapedQuoted: Parser[String] =
quoted.label("string literal").token
(string("\"") *> escapedThru("\"").map(_.dropRight(1))).token

def doubleString: Parser[String] =
regex("[-+]?([0-9]*\\.)?[0-9]+([eE][-+]?[0-9]+)?".r).token
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.wishingtree.branch.mustachio

import java.util.regex.Pattern

private[mustachio] case class Delimiter(open: String, close: String) {

def closing(str: String): String =
s"$open/$str$close"

def section(str: String): String =
s"$open#$str$close"

def inversion(str: String): String =
s"$open^$str$close"

def partial(str: String): String =
s"$open>$str$close"

def comment(str: String): String =
s"$open!$str$close"

lazy val isDefault: Boolean =
Delimiter.default.open.equals(open) &&
Delimiter.default.close.equals(close)
}

private[mustachio] object Delimiter {
val default: Delimiter = Delimiter("{{", "}}")

def replaceDefaultWith(content: String, newDelimiter: Delimiter): String =
content
.replaceAll(Pattern.quote(default.open), newDelimiter.open)
.replaceAll(Pattern.quote(default.close), newDelimiter.close)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package dev.wishingtree.branch.mustachio

import dev.wishingtree.branch.friday.{Json, JsonDecoder}

import scala.io.Source
import scala.util.{Try, Using}

case class Spec(
name: String,
desc: String,
data: Json,
template: String,
expected: String,
partials: Json
)

object Spec {

// Because we're reading the tests from a file,
// it will escape the newlines, tabs, and carriage returns.
// We need to unescape them.
extension (s: String) {
def unescape: String =
s
.replaceAll("\\\\n", "\n")
.replaceAll("\\\\r", "\r")
.replaceAll("\\\\t", "\t")
}

given decoder: JsonDecoder[Spec] with {
def decode(json: Json): Try[Spec] = Try {
for {
name <- json ? "name"
desc <- json ? "desc"
data <- json ? "data"
template <- json ? "template"
expected <- json ? "expected"
partials <- (json ? "partials").orElse(Some(Json.obj()))
} yield Spec(
name.strVal.unescape,
desc.strVal.unescape,
data,
template.strVal.unescape,
expected.strVal.unescape,
partials
)
}.map(_.get)
}

}

case class SpecSuite(tests: IndexedSeq[Spec])

object SpecSuite {

given decoder: JsonDecoder[SpecSuite] with {
def decode(json: Json): Try[SpecSuite] = Try {
for {
tests <- json ? "tests"
} yield SpecSuite(tests.arrVal.map(Spec.decoder.decode(_).get))
}.map(_.get)
}

}

trait MustacheSpecSuite extends munit.FunSuite {

def runSpec(
spec: Spec
)(implicit loc: munit.Location): Unit = {
test(spec.name) {
val context = Stache.fromJson(spec.data)
val partials = Option(Stache.fromJson(spec.partials))
assertEquals(
Mustachio.render(
spec.template,
context,
partials
),
spec.expected
)
}
}

/** Attempts to load and parse a Moustache spec suite from a resource file.
* @return
*/
def specSuite(resource: String): SpecSuite =
Using(Source.fromResource(resource)) { source =>
SpecSuite.decoder.decode(source.mkString)
}.flatten
.getOrElse(throw new Exception("Failed to parse json for specSuite"))
}
Loading

0 comments on commit 078c45e

Please sign in to comment.