├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sc ├── core └── src │ ├── main │ └── scala │ │ └── net │ │ └── maffoo │ │ └── jsonquote │ │ ├── Lex.scala │ │ ├── Macros.scala │ │ ├── Parser.scala │ │ ├── Util.scala │ │ └── literal │ │ ├── Json.scala │ │ ├── Parse.scala │ │ ├── Writes.scala │ │ └── package.scala │ └── test │ ├── resources │ └── sample.json │ └── scala │ └── net │ └── maffoo │ └── jsonquote │ └── literal │ └── Test.scala ├── json4s └── src │ ├── main │ └── scala │ │ └── net │ │ └── maffoo │ │ └── jsonquote │ │ └── json4s │ │ ├── Compat.scala │ │ ├── Parse.scala │ │ ├── Splice.scala │ │ ├── Writes.scala │ │ └── package.scala │ └── test │ └── scala │ └── net │ └── maffoo │ └── jsonquote │ └── json4s │ └── Test.scala ├── lift └── src │ ├── main │ └── scala │ │ └── net │ │ └── maffoo │ │ └── jsonquote │ │ └── lift │ │ ├── Parse.scala │ │ ├── Splice.scala │ │ ├── Writes.scala │ │ └── package.scala │ └── test │ └── scala │ └── net │ └── maffoo │ └── jsonquote │ └── lift │ └── Test.scala ├── mill ├── play └── src │ ├── main │ └── scala │ │ └── net │ │ └── maffoo │ │ └── jsonquote │ │ └── play │ │ ├── Parse.scala │ │ ├── Splice.scala │ │ └── package.scala │ └── test │ └── scala │ └── net │ └── maffoo │ └── jsonquote │ └── play │ └── Test.scala ├── spray └── src │ ├── main │ └── scala │ │ └── net │ │ └── maffoo │ │ └── jsonquote │ │ └── spray │ │ ├── Parse.scala │ │ ├── Splice.scala │ │ └── package.scala │ └── test │ └── scala │ └── net │ └── maffoo │ └── jsonquote │ └── spray │ └── Test.scala └── upickle └── src ├── main └── scala │ └── net │ └── maffoo │ └── jsonquote │ └── upickle │ ├── Parse.scala │ ├── Splice.scala │ └── package.scala └── test └── scala └── net └── maffoo └── jsonquote └── upickle └── Test.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .cache 4 | .cache-main 5 | .cache-tests 6 | .classpath 7 | .coursier 8 | .eclipse-target 9 | .project 10 | .settings/ 11 | 12 | # sbt specific 13 | dist/* 14 | target/ 15 | lib_managed/ 16 | src_managed/ 17 | project/boot/ 18 | project/plugins/project/ 19 | 20 | # mill output directory 21 | out/ 22 | 23 | # Scala-IDE specific 24 | .scala_dependencies 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | sudo: false 3 | scala: 4 | - "2.11.12" 5 | - "2.12.10" 6 | - "2.13.1" 7 | jdk: 8 | - openjdk8 9 | script: ./mill "jsonquote[${TRAVIS_SCALA_VERSION}].__.test" 10 | cache: 11 | directories: 12 | - $HOME/.cache/mill 13 | - .coursier 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Matthew Neeley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jsonquote 2 | ========= 3 | 4 | [![Build Status](https://secure.travis-ci.org/maffoo/jsonquote.svg?branch=master)](http://travis-ci.org/maffoo/jsonquote) 5 | 6 | jsonquote is a little library that lets you build JSON in Scala using string interpolation. 7 | It uses macros to parse and validate your json at compile time, and to ensure that the values 8 | you are trying to interpolate are of the appropriate types for the places in the json where 9 | they are to be interpolated. jsonquote supports play-json, spray-json, and lift-json. 10 | 11 | 12 | Using jsonquote 13 | --------------- 14 | 15 | jsonquote is built for scala 2.11, 2.12, and 2.13, and published on bintray. To include it in your 16 | project, simply add the desired artifact as a maven dependency for the json library you would 17 | like to use: 18 | ```scala 19 | resolvers += Resolver.jcenterRepo 20 | 21 | // use the basic 'literal' json support built in to jsonquote 22 | libraryDependencies += "net.maffoo" %% "jsonquote-core" % "0.6.1" 23 | 24 | // use one of the supported third-party json libraries 25 | libraryDependencies += "net.maffoo" %% "jsonquote-json4s" % "0.6.1" 26 | libraryDependencies += "net.maffoo" %% "jsonquote-lift" % "0.6.1" 27 | libraryDependencies += "net.maffoo" %% "jsonquote-play" % "0.6.1" 28 | libraryDependencies += "net.maffoo" %% "jsonquote-spray" % "0.6.1" 29 | 30 | ``` 31 | 32 | 33 | Examples 34 | -------- 35 | 36 | Here are some examples of how the interpolation works. First, import everything: 37 | ```scala 38 | scala> import net.maffoo.jsonquote.play._ 39 | import net.maffoo.jsonquote.play._ 40 | 41 | scala> import play.api.libs.json._ 42 | import play.api.libs.json._ 43 | ``` 44 | 45 | Now, we can interpolate single values: 46 | ```scala 47 | scala> val hi = "Hello, world!" 48 | hi: String = Hello, world! 49 | 50 | scala> json"$hi" 51 | res0: play.api.libs.json.JsValue = "Hello, world!" 52 | 53 | scala> json"[$hi, $hi]" 54 | res1: play.api.libs.json.JsValue = ["Hello, world!","Hello, world!"] 55 | 56 | scala> json"{greeting: $hi}" 57 | res2: play.api.libs.json.JsValue = {"greeting":"Hello, world!"} 58 | ``` 59 | 60 | field names: 61 | ```scala 62 | scala> val foo = "bar" 63 | foo: String = bar 64 | 65 | scala> json"{$foo: 123}" 66 | res3: play.api.libs.json.JsValue = {"bar":123} 67 | ``` 68 | 69 | key-value pairs in objects: 70 | ```scala 71 | scala> val item = "msg" -> "yippee!" 72 | item: (String, String) = (msg,yippee!) 73 | 74 | scala> json"{$item}" 75 | res4: play.api.libs.json.JsValue = {"msg":"yippee!"} 76 | ``` 77 | 78 | We can also interpolate multiple values or fields from Iterable or Option all at once 79 | (the .. syntax here was chosen to mirror that used in quasiquotes for scala macros): 80 | ```scala 81 | scala> val numbers = List(1,2,3,4,5) 82 | numbers: List[Int] = List(1, 2, 3, 4, 5) 83 | 84 | scala> json"[..$numbers]" 85 | res5: play.api.libs.json.JsValue = [1,2,3,4,5] 86 | 87 | scala> val (a, b) = (Some("a" -> "here"), None) 88 | a: Some[(String, String)] = Some((a,here)) 89 | b: None.type = None 90 | 91 | scala> json"{..$a, ..$b}" 92 | res6: play.api.libs.json.JsValue = {"a":"here"} 93 | ``` 94 | 95 | Note that when interpolating multiple values, you may see type errors if you try to 96 | map over collections. This is due to interactions between scala's type inference 97 | on collection builders, macro defs, and the StringContext method call into which 98 | the compiler transforms the interpolated string. This requires giving the compiler 99 | a hint about what concrete collection type you want: 100 | ```scala 101 | scala> val xs = Seq("a" -> 1, "b" -> 2) 102 | xs: Seq[(String, Int)] = List((a,1), (b,2)) 103 | 104 | scala> json"[..${xs.map(_._1)}]" 105 | :15: error: required Iterable[_] but got Any 106 | json"[..${xs.map(_._1)}]" 107 | ^ 108 | 109 | scala> json"[..${xs.map(_._1).toSeq}]" 110 | res1: play.api.libs.json.JsArray = ["a","b"] 111 | ``` 112 | 113 | 114 | Alternately, we can define optional fields with a name but optional value that 115 | will be dropped from the final result if the value is None: 116 | ```scala 117 | scala> val (some, none) = (Some("hello!"), None) 118 | some: Some[String] = Some(hello!) 119 | none: None.type = None 120 | 121 | scala> json"{msg:? $some, msg2:? $none}" 122 | res7: play.api.libs.json.JsObject = {"msg":"hello!"} 123 | ``` 124 | 125 | Variables of type JsValue are interpolated as-is: 126 | ```scala 127 | scala> val list = json"[1,2,3,4]" 128 | list: play.api.libs.json.JsValue = [1,2,3,4] 129 | 130 | scala> json"{list: $list}" 131 | res8: play.api.libs.json.JsValue = {"list":[1,2,3,4]} 132 | ``` 133 | 134 | Other variables are implicitly converted to JsValue and the compiler 135 | will complain if there is no implicit conversion in scope: 136 | ```scala 137 | scala> case class Foo(a: String, b: Int) 138 | defined class Foo 139 | 140 | scala> val foo = Foo("a", 1) 141 | foo: Foo = Foo(a,1) 142 | 143 | scala> json"{foo: $foo}" 144 | :17: error: could not find implicit value of type Writes[Foo] 145 | json"{foo: $foo}" 146 | ^ 147 | 148 | scala> implicit val fooFormat = Json.writes[Foo] 149 | fooFormat: play.api.libs.json.OWrites[Foo] = play.api.libs.json.OWrites$$anon$2@2f2b6ef9 150 | 151 | scala> json"{foo: $foo}" 152 | res9: play.api.libs.json.JsValue = {"foo":{"a":"a","b":1}} 153 | ``` 154 | 155 | Even without any interpolation, we get compile-time checking of our json literals, with 156 | some syntactic niceties like the ability to omit quotes around field names, and inline 157 | comments in your json literals: 158 | ```scala 159 | scala> val list = json"[1, 2, 3" 160 | error: exception during macro expansion: 161 | java.lang.IllegalArgumentException: requirement failed: expected ',' but got EOF 162 | 163 | scala> json"{ a: 1, b: 2 }" 164 | res18: play.api.libs.json.JsValue = {"a":1,"b":2} 165 | 166 | scala> json"{ a: 1 /* this is awesome */, b: 2 /* this is, too */ } // that was great" 167 | res19: play.api.libs.json.JsValue = {"a":1,"b":2} 168 | ``` 169 | -------------------------------------------------------------------------------- /build.sc: -------------------------------------------------------------------------------- 1 | import mill.scalalib._ 2 | import mill.scalalib.api.Util 3 | import mill.scalalib.publish._ 4 | 5 | object jsonquote extends mill.Cross[JsonQuote]("2.11.12", "2.12.10", "2.13.1") 6 | class JsonQuote(crossVersion: String) extends mill.Module { 7 | trait Module extends CrossSbtModule with PublishModule { 8 | def publishVersion = "0.6.1" 9 | def pomSettings = PomSettings( 10 | description = "Build json using scala string interpolation", 11 | organization = "net.maffoo", 12 | url = "https://github.com/maffoo/jsonquote", 13 | licenses = Seq(License.MIT), 14 | versionControl = VersionControl.github("maffoo", "jsonquote"), 15 | developers = Seq( 16 | Developer("maffoo", "Matthew Neeley","https://github.com/maffoo") 17 | ) 18 | ) 19 | 20 | def crossScalaVersion = crossVersion 21 | def scalacOptions = Seq("-feature", "-deprecation") 22 | 23 | def testModuleDeps: Seq[TestModule] = Nil 24 | object test extends Tests { 25 | def moduleDeps = super.moduleDeps ++ testModuleDeps 26 | def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.8") 27 | def testFrameworks = Seq("org.scalatest.tools.Framework") 28 | } 29 | } 30 | 31 | object core extends Module { 32 | override def millSourcePath = super.millSourcePath / os.up / os.up / "core" 33 | def artifactName = "jsonquote-core" 34 | def ivyDeps = Agg(ivy"${scalaOrganization()}:scala-reflect:${scalaVersion()}") 35 | } 36 | 37 | class Interop(name: String, ivyDeps0: Dep*)(implicit ctx: mill.define.Ctx) extends Module { 38 | override def millSourcePath = super.millSourcePath / os.up / os.up / name 39 | def artifactName = s"jsonquote-$name" 40 | def moduleDeps = Seq(core) 41 | def testModuleDeps = Seq(core.test) 42 | def ivyDeps = Agg.from(ivyDeps0) 43 | def scalaBinaryVersion = mill.T { Util.scalaBinaryVersion(scalaVersion()) } 44 | } 45 | 46 | object json4s extends Interop("json4s", ivy"org.json4s::json4s-native:3.6.7") 47 | 48 | object lift extends Interop("lift", ivy"net.liftweb::lift-json:3.4.0") 49 | 50 | object play extends Interop("play") { 51 | def ivyDeps = mill.T { 52 | val playVersion = scalaBinaryVersion() match { 53 | case "2.11" => "2.7.4" 54 | case _ => "2.8.0" 55 | } 56 | Agg(ivy"com.typesafe.play::play-json:${playVersion}") 57 | } 58 | } 59 | 60 | object spray extends Interop("spray", ivy"io.spray::spray-json:1.3.5") 61 | 62 | object upickle extends Interop("upickle") { 63 | def ivyDeps = mill.T { 64 | val upickleVersion = scalaBinaryVersion() match { 65 | case "2.11" => "0.7.4" 66 | case _ => "0.8.0" 67 | } 68 | Agg( 69 | ivy"com.lihaoyi::upickle-core:${upickleVersion}", 70 | ivy"com.lihaoyi::upickle:${upickleVersion}" 71 | ) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /core/src/main/scala/net/maffoo/jsonquote/Lex.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | import scala.collection.BufferedIterator 4 | 5 | sealed trait Token 6 | object Token { 7 | abstract class Literal(val value: String) extends Token { 8 | override def toString = s"'$value'" 9 | } 10 | 11 | case class STRING(value: String) extends Token 12 | case class NUMBER(value: BigDecimal) extends Token 13 | case class IDENT(value: String) extends Token 14 | case object SPLICE extends Token 15 | case object EOF extends Token 16 | case object OBJECT_START extends Literal("{") 17 | case object OBJECT_END extends Literal("}") 18 | case object ARRAY_START extends Literal("[") 19 | case object ARRAY_END extends Literal("]") 20 | case object REPEAT extends Literal("..") 21 | case object OPTIONAL extends Literal("?") 22 | case object COLON extends Literal(":") 23 | case object COMMA extends Literal(",") 24 | case object TRUE extends Literal("true") 25 | case object FALSE extends Literal("false") 26 | case object NULL extends Literal("null") 27 | } 28 | 29 | case class Pos(string: String, offset: Int) 30 | 31 | object Lex { 32 | import Token._ 33 | import Util._ 34 | 35 | type State = BufferedIterator[(Char, Pos)] 36 | 37 | def apply(s: String): Iterator[(Token, Pos)] = lex(s) ++ Iterator((EOF, Pos(s, s.length))) 38 | 39 | def apply(parts: Seq[String]): Iterator[(Token, Pos)] = { 40 | (for { 41 | (part, i) <- parts.iterator.zipWithIndex 42 | splice = if (i == 0) Iterator() else Iterator((SPLICE, Pos(part, part.length))) 43 | (token, pos) <- splice ++ lex(part) 44 | } yield (token, pos)) ++ Iterator((EOF, Pos(parts.last, parts.last.length))) 45 | } 46 | 47 | def lex(s: String): Iterator[(Token, Pos)] = 48 | lex(s.iterator.zipWithIndex.map { case (c, i) => (c, Pos(s, i)) }.buffered) 49 | 50 | def lex(implicit it: State): Iterator[(Token, Pos)] = new Iterator[(Token, Pos)] { 51 | def hasNext: Boolean = { 52 | skipWhitespace 53 | it.hasNext 54 | } 55 | 56 | def next(): (Token, Pos) = { 57 | skipWhitespace 58 | val (c, pos) = it.head 59 | val token = c match { 60 | case '{' => lexLiteral(OBJECT_START) 61 | case '}' => lexLiteral(OBJECT_END) 62 | case '[' => lexLiteral(ARRAY_START) 63 | case ']' => lexLiteral(ARRAY_END) 64 | case '.' => lexLiteral(REPEAT) 65 | case '?' => lexLiteral(OPTIONAL) 66 | case ':' => lexLiteral(COLON) 67 | case ',' => lexLiteral(COMMA) 68 | case '"' => lexString 69 | case c if c.isDigit || c == '-' => lexNumber 70 | case c if c.isLetter || c == '_' => lexToken 71 | } 72 | (token, pos) 73 | } 74 | } 75 | 76 | def lexLiteral(lit: Literal)(implicit it: State): Literal = { 77 | for (c <- lit.value) expect[Char](c) 78 | lit 79 | } 80 | 81 | def lexToken(implicit it: State): Token = { 82 | val b = new StringBuilder 83 | b += accept(c => c.isLetter || c == '_') 84 | b ++= acceptRun(c => c.isLetterOrDigit || c == '_' || c == '-') 85 | b.toString match { 86 | case "true" => TRUE 87 | case "false" => FALSE 88 | case "null" => NULL 89 | case s => IDENT(s) 90 | } 91 | } 92 | 93 | def lexString(implicit it: State): Token = { 94 | val b = new StringBuilder 95 | expect[Char]('"') 96 | while (it.head._1 != '"') b += lexChar 97 | expect[Char]('"') 98 | STRING(b.toString) 99 | } 100 | 101 | def lexChar(implicit it: State): Char = { 102 | it.head._1 match { 103 | case '\\' => 104 | it.next() 105 | it.head._1 match { 106 | case '\\' => it.next()._1 107 | case '"' => it.next()._1 108 | case '/' => it.next()._1 109 | case 'b' => it.next(); '\b' 110 | case 'f' => it.next(); '\f' 111 | case 'n' => it.next(); '\n' 112 | case 'r' => it.next(); '\r' 113 | case 't' => it.next(); '\t' 114 | case 'u' => 115 | it.next() 116 | val digits = for (_ <- 1 to 4) yield accept(HEX_DIGIT) 117 | Integer.parseInt(digits.mkString, 16).asInstanceOf[Char] 118 | } 119 | 120 | case _ => 121 | it.next()._1 122 | } 123 | } 124 | 125 | val DIGIT = "0123456789" 126 | val HEX_DIGIT = "0123456789abcdefABCDEF" 127 | 128 | def lexNumber(implicit it: State): Token = { 129 | val b = new StringBuilder 130 | b ++= acceptOpt("-") 131 | b ++= lexInt 132 | acceptOpt(".").foreach { c => 133 | b += c 134 | b += accept(DIGIT) 135 | b ++= acceptRun(DIGIT) 136 | } 137 | acceptOpt("eE").foreach { c => 138 | b += c 139 | b ++= acceptOpt("+-") 140 | b += accept(DIGIT) 141 | b ++= acceptRun(DIGIT) 142 | } 143 | NUMBER(BigDecimal(b.toString)) 144 | } 145 | 146 | def lexInt(implicit it: State): String = { 147 | it.head._1 match { 148 | case '0' => it.next()._1.toString 149 | case '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => 150 | it.next()._1 +: acceptRun(DIGIT) 151 | } 152 | } 153 | 154 | @scala.annotation.tailrec 155 | private def skipWhitespace(implicit it: State): Unit = { 156 | while (it.hasNext && it.head._1.isWhitespace) it.next() 157 | if (it.hasNext && it.head._1 == '/') { 158 | skipComment 159 | skipWhitespace 160 | } 161 | } 162 | 163 | private def skipComment(implicit it: State): Unit = { 164 | expect('/') 165 | acceptOpt("/*") match { 166 | case Some('/') => skipLineComment 167 | case Some('*') => skipBlockComment 168 | case _ => sys.error("expected // or /* to start comment") 169 | } 170 | } 171 | 172 | private def skipLineComment(implicit it: State): Unit = { 173 | acceptRun(!"\r\n".contains(_)) 174 | acceptRun("\r\n".contains(_)) 175 | } 176 | 177 | @scala.annotation.tailrec 178 | private def skipBlockComment(implicit it: State): Unit = { 179 | acceptRun(_ != '*') 180 | expect('*') 181 | acceptOpt("/") match { 182 | case None => skipBlockComment 183 | case Some(_) => // done 184 | } 185 | } 186 | 187 | private def accept(f: Char => Boolean)(implicit it: State): Char = { 188 | val (c, _) = it.next() 189 | require(f(c)) 190 | c 191 | } 192 | 193 | private def acceptOpt(f: Char => Boolean)(implicit it: State): Option[Char] = 194 | if (it.hasNext && f(it.head._1)) Some(it.next()._1) else None 195 | 196 | private def acceptRun(f: Char => Boolean)(implicit it: State): String = { 197 | val b = new StringBuilder 198 | var done = false 199 | while (!done) { 200 | acceptOpt(f) match { 201 | case Some(c) => b += c 202 | case None => done = true 203 | } 204 | } 205 | b.toString 206 | } 207 | 208 | private def accept(s: String)(implicit it: State): Char = accept(s contains _) 209 | private def acceptOpt(s: String)(implicit it: State): Option[Char] = acceptOpt(s contains _) 210 | private def acceptRun(s: String)(implicit it: State): String = acceptRun(s contains _) 211 | } 212 | -------------------------------------------------------------------------------- /core/src/main/scala/net/maffoo/jsonquote/Macros.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | object Macros { 4 | type Context = scala.reflect.macros.blackbox.Context 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/scala/net/maffoo/jsonquote/Parser.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | import scala.collection.BufferedIterator 4 | 5 | trait Parser[V, F] { 6 | import Token._ 7 | import Util._ 8 | 9 | def apply(parts: Seq[String]): V = apply(Lex(parts)) 10 | 11 | def apply(it: Iterator[(Token, Pos)]): V = { 12 | val result = parseValue(it.buffered) 13 | expect[Token](EOF)(it) 14 | result 15 | } 16 | 17 | def expect[A](a: A)(implicit it: Iterator[(A, Pos)]): Unit = { 18 | val (next, pos) = it.next() 19 | if (next != a) throw JsonError(s"expected $a but got $next", pos) 20 | } 21 | 22 | def parseValue(implicit it: BufferedIterator[(Token, Pos)]): V = { 23 | val (tok, pos) = it.head 24 | tok match { 25 | case OBJECT_START => parseObject(it) 26 | case ARRAY_START => parseArray(it) 27 | case NUMBER(n) => it.next(); makeNumber(n) 28 | case STRING(s) => it.next(); makeString(s) 29 | case TRUE => it.next(); makeBoolean(true) 30 | case FALSE => it.next(); makeBoolean(false) 31 | case NULL => it.next(); makeNull() 32 | case SPLICE => it.next(); makeSpliceValue() 33 | case tok => throw JsonError(s"unexpected token: $tok", pos) 34 | } 35 | } 36 | 37 | def parseArray(implicit it: BufferedIterator[(Token, Pos)]): V = { 38 | expect[Token](ARRAY_START) 39 | val elements = if (it.head._1 == ARRAY_END) Nil else parseElements 40 | expect[Token](ARRAY_END) 41 | makeArray(elements) 42 | } 43 | 44 | def parseElements(implicit it: BufferedIterator[(Token, Pos)]): Seq[V] = { 45 | val members = Seq.newBuilder[V] 46 | def advance(): Unit = { 47 | if (it.head._1 == REPEAT) { 48 | it.next() 49 | expect[Token](SPLICE) 50 | members += makeSpliceValues() 51 | } else { 52 | members += parseValue 53 | } 54 | } 55 | advance() 56 | while (it.head._1 != ARRAY_END) { 57 | expect[Token](COMMA) 58 | advance() 59 | } 60 | members.result 61 | } 62 | 63 | def parseObject(implicit it: BufferedIterator[(Token, Pos)]): V = { 64 | expect[Token](OBJECT_START) 65 | val members = if (it.head._1 == OBJECT_END) Nil else parseMembers 66 | expect[Token](OBJECT_END) 67 | makeObject(members) 68 | } 69 | 70 | def parseMembers(implicit it: BufferedIterator[(Token, Pos)]): Seq[F] = { 71 | val members = Seq.newBuilder[F] 72 | def advance(): Unit = { 73 | if (it.head._1 == REPEAT) { 74 | it.next() 75 | expect[Token](SPLICE) 76 | members += makeSpliceFields() 77 | } else { 78 | members += parsePair 79 | } 80 | } 81 | advance() 82 | while (it.head._1 != OBJECT_END) { 83 | expect[Token](COMMA) 84 | advance() 85 | } 86 | members.result 87 | } 88 | 89 | def parsePair(implicit it: BufferedIterator[(Token, Pos)]): F = { 90 | val (tok, pos) = it.next() 91 | tok match { 92 | case STRING(k) => 93 | expect[Token](COLON) 94 | it.head._1 match { 95 | case OPTIONAL => 96 | it.next() 97 | expect[Token](SPLICE) 98 | makeSpliceFieldOpt(k) 99 | 100 | case _ => makeField(k, parseValue) 101 | } 102 | 103 | case IDENT(k) => 104 | expect[Token](COLON) 105 | it.head._1 match { 106 | case OPTIONAL => 107 | it.next() 108 | expect[Token](SPLICE) 109 | makeSpliceFieldOpt(k) 110 | 111 | case _ => makeField(k, parseValue) 112 | } 113 | 114 | case SPLICE => 115 | it.head._1 match { 116 | case COLON => 117 | it.next() 118 | it.head._1 match { 119 | case OPTIONAL => 120 | it.next() 121 | expect[Token](SPLICE) 122 | makeSpliceFieldNameOpt() 123 | 124 | case _ => 125 | makeSpliceFieldName(parseValue) 126 | } 127 | 128 | case _ => 129 | makeSpliceField() 130 | } 131 | 132 | case tok => 133 | throw JsonError(s"expected field but got $tok", pos) 134 | } 135 | } 136 | 137 | // abstract methods for constructing AST objects from parse results 138 | def makeObject(fields: Iterable[F]): V 139 | def makeArray(elements: Iterable[V]): V 140 | def makeNumber(n: BigDecimal): V 141 | def makeString(s: String): V 142 | def makeBoolean(b: Boolean): V 143 | def makeNull(): V 144 | def makeSpliceValue(): V 145 | def makeSpliceValues(): V 146 | 147 | def makeField(k: String, v: V): F 148 | def makeSpliceField(): F 149 | def makeSpliceFields(): F 150 | def makeSpliceFieldNameOpt(): F 151 | def makeSpliceFieldName(v: V): F 152 | def makeSpliceFieldOpt(k: String): F 153 | } 154 | -------------------------------------------------------------------------------- /core/src/main/scala/net/maffoo/jsonquote/Util.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | case class JsonError(msg: String, position: Pos) extends Exception(msg) 4 | 5 | object Util { 6 | def expect[A](a: A)(implicit it: Iterator[(A, Pos)]): Unit = { 7 | val (next, pos) = it.next() 8 | if (next != a) throw JsonError(s"expected $a but got $next", pos) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/scala/net/maffoo/jsonquote/literal/Json.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.literal 2 | 3 | class Json private[jsonquote] (val s: String) extends AnyVal { 4 | override def toString = s 5 | } 6 | 7 | object Json { 8 | /** 9 | * Parse a json string at runtime, marking it as valid with the Json value class. 10 | */ 11 | def apply(s: String): Json = Parse(Seq(s)) match { 12 | case Seq(Chunk(s)) => new Json(s) 13 | } 14 | 15 | /** 16 | * Quote strings for inclusion as JSON strings. 17 | */ 18 | def quoteString (s : String) : String = "\"" + s.flatMap { 19 | case '"' => """\"""" 20 | case '\\' => """\\""" 21 | case '/' => """\/""" 22 | case '\b' => """\b""" 23 | case '\f' => """\f""" 24 | case '\n' => """\n""" 25 | case '\r' => """\r""" 26 | case '\t' => """\t""" 27 | case c if c.isControl => f"\\u$c%04x" 28 | case c => c.toString 29 | } + "\"" 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/scala/net/maffoo/jsonquote/literal/Parse.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.literal 2 | 3 | import scala.collection.BufferedIterator 4 | 5 | import net.maffoo.jsonquote._ 6 | import net.maffoo.jsonquote.Token._ 7 | import net.maffoo.jsonquote.Util._ 8 | import net.maffoo.jsonquote.literal.Json.quoteString 9 | 10 | sealed trait Segment 11 | case class Chunk(s: String) extends Segment 12 | case object SpliceValue extends Segment 13 | case object SpliceValues extends Segment 14 | case object SpliceField extends Segment 15 | case object SpliceFields extends Segment 16 | case object SpliceFieldName extends Segment 17 | case object SpliceFieldNameOpt extends Segment 18 | case class SpliceFieldOpt(k: String) extends Segment 19 | 20 | object Parse { 21 | /** 22 | * Helper method used when splicing literal json strings. 23 | * 24 | * This avoids the extra commas which would otherwise occur 25 | * when we splice an empty Iterable or Option. There are three 26 | * cases to consider: 27 | * 28 | * val xs = Nil 29 | * json"[ ..$xs, 2 ]" - leading comma 30 | * json"[ 1, ..$xs, 2]" - double comma 31 | * json"[ 1, ..$xs ]" - trailing comma 32 | * 33 | * To handle the first case, we check when adding a segment 34 | * beginning with a comma whether the last character was a 35 | * list or object opener. If so, we drop the comma. 36 | * 37 | * To handle the latter two cases, we check when adding a 38 | * segment whether the last character was a comma and the 39 | * next character is a comma or object or list closer. 40 | * If so, we drop the last comma added. 41 | */ 42 | def coalesce(segments: String*): Json = { 43 | def isComma(c: Char) = c == ',' 44 | def isCommaOrCloser(c: Char) = ",]}" contains c 45 | def isOpener(c: Char) = "{[" contains c 46 | 47 | val it = segments.iterator 48 | val b = new StringBuilder 49 | while (it.hasNext) { 50 | val s = it.next 51 | if (b.nonEmpty && isOpener(b.last) && s.nonEmpty && isComma(s.head)) { 52 | b.append(s.drop(1)) 53 | } else if (b.nonEmpty && isComma(b.last) && s.nonEmpty && isCommaOrCloser(s.head)) { 54 | b.deleteCharAt(b.length - 1) 55 | b.append(s) 56 | } else { 57 | b.append(s) 58 | } 59 | } 60 | new Json(b.toString) 61 | } 62 | 63 | def apply(s: Seq[String]): Seq[Segment] = apply(Lex(s)) 64 | 65 | def apply(it: Iterator[(Token, Pos)]): Seq[Segment] = { 66 | val segments = parseValue(it.buffered).buffered 67 | expect[Token](EOF)(it) 68 | val out = IndexedSeq.newBuilder[Segment] 69 | while (segments.hasNext) { 70 | segments.head match { 71 | case _: Chunk => 72 | val chunk = new StringBuilder 73 | while (segments.hasNext && segments.head.isInstanceOf[Chunk]) { 74 | chunk ++= segments.next.asInstanceOf[Chunk].s 75 | } 76 | out += Chunk(chunk.toString) 77 | case _ => 78 | out += segments.next 79 | } 80 | } 81 | out.result 82 | } 83 | 84 | def parseValue(implicit it: BufferedIterator[(Token, Pos)]): Iterator[Segment] = { 85 | val (tok, pos) = it.head 86 | tok match { 87 | case OBJECT_START => parseObject 88 | case ARRAY_START => parseArray 89 | case NUMBER(n) => it.next(); Iterator(Chunk(n.toString)) 90 | case STRING(s) => it.next(); Iterator(Chunk(quoteString(s))) 91 | case TRUE => it.next(); Iterator(Chunk("true")) 92 | case FALSE => it.next(); Iterator(Chunk("false")) 93 | case NULL => it.next(); Iterator(Chunk("null")) 94 | case SPLICE => it.next(); Iterator(SpliceValue) 95 | case tok => throw JsonError(s"unexpected token: $tok", pos) 96 | } 97 | } 98 | 99 | def parseArray(implicit it: BufferedIterator[(Token, Pos)]): Iterator[Segment] = { 100 | expect[Token](ARRAY_START) 101 | val elems = if (it.head._1 == ARRAY_END) Iterator.empty else parseElements 102 | expect[Token](ARRAY_END) 103 | Iterator(Chunk("[")) ++ elems ++ Iterator(Chunk("]")) 104 | } 105 | 106 | def parseElements(implicit it: BufferedIterator[(Token, Pos)]): Iterator[Segment] = { 107 | val b = Seq.newBuilder[Segment] 108 | def advance(first: Boolean): Iterator[Segment] = { 109 | if (it.head._1 == REPEAT) { 110 | it.next() 111 | expect[Token](SPLICE) 112 | Iterator(SpliceValues) 113 | } else { 114 | parseValue 115 | } 116 | } 117 | b ++= advance(first = true) 118 | while (it.head._1 != ARRAY_END) { 119 | expect[Token](COMMA) 120 | b += Chunk(",") 121 | b ++= advance(first = false) 122 | } 123 | b.result.iterator 124 | } 125 | 126 | def parseObject(implicit it: BufferedIterator[(Token, Pos)]): Iterator[Segment] = { 127 | expect[Token](OBJECT_START) 128 | val members = if (it.head._1 == OBJECT_END) Iterator.empty else parseMembers 129 | expect[Token](OBJECT_END) 130 | Iterator(Chunk("{")) ++ members ++ Iterator(Chunk("}")) 131 | } 132 | 133 | def parseMembers(implicit it: BufferedIterator[(Token, Pos)]): Iterator[Segment] = { 134 | val b = Seq.newBuilder[Segment] 135 | def advance(first: Boolean): Iterator[Segment] = { 136 | if (it.head._1 == REPEAT) { 137 | it.next() 138 | expect[Token](SPLICE) 139 | Iterator(SpliceFields) 140 | } else { 141 | parsePair(first) 142 | } 143 | } 144 | b ++= advance(first = true) 145 | while (it.head._1 != OBJECT_END) { 146 | expect[Token](COMMA) 147 | b += Chunk(",") 148 | b ++= advance(first = false) 149 | } 150 | b.result.iterator 151 | } 152 | 153 | def parsePair(first: Boolean)(implicit it: BufferedIterator[(Token, Pos)]): Iterator[Segment] = { 154 | val (tok, pos) = it.next() 155 | tok match { 156 | case STRING(k) => 157 | expect[Token](COLON) 158 | it.head._1 match { 159 | case OPTIONAL => 160 | it.next() 161 | expect[Token](SPLICE) 162 | Iterator(SpliceFieldOpt(k)) 163 | 164 | case _ => Iterator(Chunk(quoteString(k)), Chunk(":")) ++ parseValue 165 | } 166 | 167 | case IDENT(k) => 168 | expect[Token](COLON) 169 | it.head._1 match { 170 | case OPTIONAL => 171 | it.next() 172 | expect[Token](SPLICE) 173 | Iterator(SpliceFieldOpt(k)) 174 | 175 | case _ => Iterator(Chunk(quoteString(k)), Chunk(":")) ++ parseValue 176 | } 177 | 178 | case SPLICE => 179 | it.head._1 match { 180 | case COLON => 181 | it.next() 182 | it.head._1 match { 183 | case OPTIONAL => 184 | it.next() 185 | expect[Token](SPLICE) 186 | Iterator(SpliceFieldNameOpt) 187 | 188 | case _ => 189 | Iterator(SpliceFieldName, Chunk(":")) ++ parseValue 190 | } 191 | 192 | case _ => 193 | Iterator(SpliceField) 194 | } 195 | 196 | case tok => 197 | throw JsonError(s"expected field but got $tok", pos) 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /core/src/main/scala/net/maffoo/jsonquote/literal/Writes.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.literal 2 | 3 | import net.maffoo.jsonquote.literal.Json.quoteString 4 | 5 | // Typeclasses for converting to literal Json. 6 | // Borrowed from the Writes mechanism in play-json. 7 | 8 | trait Writes[A] { 9 | def write(a: A): Json 10 | } 11 | 12 | // Converters for basic scala types. 13 | object Writes { 14 | implicit object JsonWrites extends Writes[Json] { 15 | def write(a: Json): Json = a 16 | } 17 | 18 | implicit object BoolWrites extends Writes[Boolean] { 19 | def write(b: Boolean): Json = new Json(b.toString) 20 | } 21 | 22 | implicit object ByteWrites extends Writes[Byte] { 23 | def write(n: Byte): Json = new Json(n.toString) 24 | } 25 | 26 | implicit object ShortWrites extends Writes[Short] { 27 | def write(n: Short): Json = new Json(n.toString) 28 | } 29 | 30 | implicit object IntWrites extends Writes[Int] { 31 | def write(n: Int): Json = new Json(n.toString) 32 | } 33 | 34 | implicit object LongWrites extends Writes[Long] { 35 | def write(n: Long): Json = new Json(n.toString) 36 | } 37 | 38 | implicit object DoubleWrites extends Writes[Double] { 39 | def write(n: Double): Json = new Json(n.toString) 40 | } 41 | 42 | implicit object StringWrites extends Writes[String] { 43 | def write(s: String): Json = new Json(quoteString(s)) 44 | } 45 | 46 | // implicit def optionWrites[A: Writes]: Writes[Option[A]] = new Writes[Option[A]] { 47 | // def write(o: Option[A]): Json = o match { 48 | // case Some(a) => implicitly[Writes[A]].write(a) 49 | // case None => Json("null") 50 | // } 51 | // } 52 | 53 | implicit def seqWrites[A: Writes]: Writes[Seq[A]] = new Writes[Seq[A]] { 54 | def write(s: Seq[A]): Json = { 55 | val writer = implicitly[Writes[A]] 56 | new Json(s.map(writer.write).mkString("[", ",", "]")) 57 | } 58 | } 59 | 60 | implicit def mapWrites[A: Writes]: Writes[Map[String, A]] = new Writes[Map[String, A]] { 61 | def write(m: Map[String, A]): Json = { 62 | val keyWriter = StringWrites 63 | val valWriter = implicitly[Writes[A]] 64 | new Json(m.map { case (k, v) => s"${keyWriter.write(k)}:${valWriter.write(v)}" }.mkString("{", ",", "}")) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/src/main/scala/net/maffoo/jsonquote/literal/package.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | import net.maffoo.jsonquote.Macros._ 4 | import scala.language.experimental.macros 5 | 6 | package object literal { 7 | implicit class RichJsonStringContext(val sc: StringContext) extends AnyVal { 8 | def json(args: Any*): Json = macro jsonImpl 9 | } 10 | 11 | def jsonImpl(c: Context)(args: c.Expr[Any]*): c.Expr[Json] = { 12 | import c.universe._ 13 | 14 | // fully-qualified symbols and types (for hygiene) 15 | val writesT = tq"_root_.net.maffoo.jsonquote.literal.Writes" 16 | val strWriter = q"_root_.net.maffoo.jsonquote.literal.Writes.StringWrites" 17 | 18 | // convert the given json segment to a string, escaping spliced values as needed 19 | def splice(segment: Segment)(implicit args: Iterator[Tree]): Tree = segment match { 20 | case SpliceValue => spliceValue(args.next) 21 | case SpliceValues => spliceValues(args.next) 22 | 23 | case SpliceField => spliceField(args.next) 24 | case SpliceFields => spliceFields(args.next) 25 | case SpliceFieldName => spliceFieldName(args.next) 26 | case SpliceFieldNameOpt => spliceFieldNameOpt(args.next, args.next) 27 | case SpliceFieldOpt(k) => spliceFieldOpt(k, args.next) 28 | 29 | case Chunk(s) => Literal(Constant(s)) 30 | } 31 | 32 | def spliceValue(e: Tree): Tree = e.tpe match { 33 | case t if t <:< c.typeOf[Json] => q"$e.toString" 34 | case t => inferWriter(e, t); q"implicitly[$writesT[$t]].write($e).toString" 35 | } 36 | 37 | def spliceValues(e: Tree): Tree = e.tpe match { 38 | case t if t <:< c.typeOf[Nil.type] => Literal(Constant("")) 39 | case t if t <:< c.typeOf[Iterable[Json]] => 40 | q"""$e.mkString(",")""" 41 | case t if t <:< c.typeOf[Iterable[Any]] => 42 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[Nothing]] :: Nil))(0) 43 | val writer = inferWriter(e, valueTpe) 44 | q"""$e.map($writer.write).mkString(",")""" 45 | 46 | case t if t <:< c.typeOf[None.type] => Literal(Constant("")) 47 | case t if t <:< c.typeOf[Option[Json]] => 48 | q"""$e.map(_.toString).getOrElse("")""" 49 | case t if t <:< c.typeOf[Option[Any]] => 50 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 51 | val writer = inferWriter(e, valueTpe) 52 | q"""$e.map($writer.write).map(_.toString).getOrElse("")""" 53 | 54 | case t => c.abort(e.pos, s"required Iterable[_] but got $t") 55 | } 56 | 57 | def spliceField(e: Tree): Tree = e.tpe match { 58 | case t if t <:< c.typeOf[(String, Json)] => 59 | q"""val (k, v) = $e; $strWriter.write(k).toString + ":" + v""" 60 | case t if t <:< c.typeOf[(String, Any)] => 61 | val valueTpe = typeParams(lub(t :: c.typeOf[(String, Nothing)] :: Nil))(1) 62 | val writer = inferWriter(e, valueTpe) 63 | q"""val (k, v) = $e; $strWriter.write(k).toString + ":" + $writer.write(v).toString""" 64 | 65 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 66 | } 67 | 68 | def spliceFields(e: Tree): Tree = e.tpe match { 69 | case t if t <:< c.typeOf[Nil.type] => Literal(Constant("")) 70 | case t if t <:< c.typeOf[Iterable[(String, Json)]] => 71 | q"""$e.map { case (k, v) => $strWriter.write(k).toString + ":" + v.toString }.mkString(",")""" 72 | case t if t <:< c.typeOf[Iterable[(String, Any)]] => 73 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[(String, Nothing)]] :: Nil))(2) 74 | val writer = inferWriter(e, valueTpe) 75 | q"""$e.map { case (k, v) => $strWriter.write(k).toString + ":" + $writer.write(v).toString }.mkString(",")""" 76 | 77 | case t if t <:< c.typeOf[None.type] => Literal(Constant("")) 78 | case t if t <:< c.typeOf[Option[(String, Json)]] => 79 | q"""$e.map { case (k, v) => $strWriter.write(k).toString + ":" + v.toString }.getOrElse("")""" 80 | case t if t <:< c.typeOf[Option[(String, Any)]] => 81 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[(String, Nothing)]] :: Nil))(2) 82 | val writer = inferWriter(e, valueTpe) 83 | q"""$e.map { case (k, v) => $strWriter.write(k).toString + ":" + $writer.write(v).toString }.getOrElse("")""" 84 | 85 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 86 | } 87 | 88 | def spliceFieldOpt(k: String, e: Tree): Tree = e.tpe match { 89 | case t if t <:< c.typeOf[None.type] => Literal(Constant("")) 90 | case t if t <:< c.typeOf[Option[Json]] => 91 | q"""$e.map { v => $strWriter.write($k).toString + ":" + v.toString }.getOrElse("")""" 92 | case t if t <:< c.typeOf[Option[Any]] => 93 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 94 | val writer = inferWriter(e, valueTpe) 95 | q"""$e.map { v => $strWriter.write($k).toString + ":" + $writer.write(v).toString }.getOrElse("")""" 96 | 97 | case t => c.abort(e.pos, s"required Option[_] but got $t") 98 | } 99 | 100 | def spliceFieldNameOpt(k: Tree, v: Tree): Tree = { 101 | k.tpe match { 102 | case t if t =:= c.typeOf[String] => 103 | case t => c.abort(k.pos, s"required String but got $t") 104 | } 105 | v.tpe match { 106 | case t if t <:< c.typeOf[None.type] => Literal(Constant("")) 107 | case t if t <:< c.typeOf[Option[Json]] => 108 | q"""$v.map { v => $strWriter.write($k).toString + ":" + v.toString }.getOrElse("")""" 109 | case t if t <:< c.typeOf[Option[Any]] => 110 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 111 | val writer = inferWriter(v, valueTpe) 112 | q"""$v.map { v => $strWriter.write($k).toString + ":" + $writer.write(v).toString }.getOrElse("")""" 113 | 114 | case t => c.abort(v.pos, s"required Option[_] but got $t") 115 | } 116 | } 117 | 118 | def spliceFieldName(e: Tree): Tree = e.tpe match { 119 | case t if t =:= c.typeOf[String] => q"""$strWriter.write($e).toString""" 120 | case t => c.abort(e.pos, s"required String but got $t") 121 | } 122 | 123 | // return a list of type parameters in the given type 124 | // example: List[(String, Int)] => Seq(Tuple2, String, Int) 125 | def typeParams(tpe: Type): Seq[Type] = { 126 | val b = Iterable.newBuilder[Type] 127 | tpe.foreach(b += _) 128 | b.result.drop(2).grouped(2).map(_.head).toIndexedSeq 129 | } 130 | 131 | // locate an implicit Writes[T] for the given type 132 | def inferWriter(e: Tree, t: Type): Tree = { 133 | val writerTpe = appliedType(c.typeOf[Writes[_]], List(t)) 134 | c.inferImplicitValue(writerTpe) match { 135 | case EmptyTree => c.abort(e.pos, s"could not find implicit value of type Writes[$t]") 136 | case tree => tree 137 | } 138 | } 139 | 140 | // Parse the string context parts into a json AST with holes, and then 141 | // typecheck/convert args to the appropriate types and splice them in. 142 | c.prefix.tree match { 143 | case Apply(_, List(Apply(_, partTrees))) => 144 | val parts = partTrees map { case Literal(Constant(const: String)) => const } 145 | val positions = (parts zip partTrees.map(_.pos)).toMap 146 | val segments = try { 147 | Parse(parts) 148 | } catch { 149 | case JsonError(msg, Pos(s, ofs)) => 150 | val pos = positions(s) 151 | c.abort(pos.withPoint(pos.point + ofs), msg) 152 | } 153 | val argsIter = args.iterator.map(_.tree) 154 | val trees = segments.map(s => splice(s)(argsIter)) 155 | c.Expr[Json](q"_root_.net.maffoo.jsonquote.literal.Parse.coalesce(..$trees)") 156 | 157 | case _ => 158 | c.abort(c.enclosingPosition, "invalid") 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /core/src/test/scala/net/maffoo/jsonquote/literal/Test.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.literal 2 | 3 | import org.scalatest.{FunSuite, Matchers} 4 | import scala.io.{Codec, Source} 5 | 6 | case class Foo(bar: String, baz: String) 7 | 8 | class LiteralTest extends FunSuite with Matchers { 9 | 10 | import Writes._ 11 | 12 | implicit object FooWrites extends Writes[Foo] { 13 | def write(foo: Foo) = json"{ bar: ${foo.bar}, baz: ${foo.baz} }" 14 | } 15 | 16 | def check(js: Json, s: String): Unit = { js should equal (Json(s)) } 17 | 18 | //json"""{test: ${Map("test" -> "foo")}}""" 19 | 20 | test("can parse plain json") { 21 | check(json""" "hello!" """, """"hello!"""") 22 | } 23 | 24 | test("can use bare identifiers for object keys") { 25 | check(json"{ test0: 0 }", """{ "test0": 0 }""") 26 | check(json"{ test: 1 }", """{ "test": 1 }""") 27 | check(json"{ test-2: 2 }", """{ "test-2": 2 }""") 28 | check(json"{ test_3: 3 }", """{ "test_3": 3 }""") 29 | check(json"{ _test-4: 4 }", """{ "_test-4": 4 }""") 30 | } 31 | 32 | test("can inject value with implicit Writes") { 33 | val foo = Foo(bar = "a", baz = "b") 34 | check(json"$foo", """{"bar": "a", "baz": "b"}""") 35 | } 36 | 37 | test("can inject values with implicit Writes") { 38 | val foos = List(Foo("1", "2"), Foo("3", "4")) 39 | check(json"[..$foos]", """[{"bar":"1", "baz":"2"}, {"bar":"3", "baz":"4"}]""") 40 | check(json"[..$Nil]", """[]""") 41 | } 42 | 43 | test("can inject Option values") { 44 | val vOpt = Some(Json("1")) 45 | check(json"[..$vOpt]", """[1]""") 46 | check(json"[..$None]" , """[]""") 47 | } 48 | 49 | test("can inject Option values with implicit Writes") { 50 | val vOpt = Some(1) 51 | check(json"[..$vOpt]", """[1]""") 52 | } 53 | 54 | test("can mix values, Iterables and Options in array") { 55 | val a = List("a") 56 | val b = Some("b") 57 | val c = Nil 58 | val d = None 59 | check(json"""[1, ..$a, 2, ..$b, 3, ..$c, 4, ..$d, 5]""", """[1, "a", 2, "b", 3, 4, 5]""") 60 | } 61 | 62 | test("can inject Tuple2 as object field") { 63 | val kv = "test" -> Foo("1", "2") 64 | check(json"{$kv}", """{"test": {"bar":"1", "baz":"2"}}""") 65 | } 66 | 67 | test("can inject multiple fields") { 68 | val kvs = Seq("a" -> 1, "b" -> 2) 69 | check(json"{..$kvs}", """{"a": 1, "b": 2}""") 70 | check(json"{..$Nil}", """{}""") 71 | } 72 | 73 | test("can inject just a field name") { 74 | val key = "foo" 75 | check(json"{$key: 1}", """{"foo": 1}""") 76 | } 77 | 78 | test("can inject Option fields") { 79 | val kvOpt = Some("a" -> Json("1")) 80 | check(json"{..$kvOpt}", """{"a": 1}""") 81 | check(json"{..$None}", """{}""") 82 | } 83 | 84 | test("can inject Option fields with implicit Writes") { 85 | val kvOpt = Some("a" -> Foo("1", "2")) 86 | check(json"{..$kvOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 87 | } 88 | 89 | test("can inject Option field values") { 90 | val vOpt = Some(Json("1")) 91 | check(json"{a:? $vOpt}", """{"a": 1}""") 92 | check(json"{a:? $None}", """{}""") 93 | check(json"{a:? $None, b: 1}", """{"b": 1}""") 94 | check(json"{b: 1, a:? $None}", """{"b": 1}""") 95 | 96 | val k = "a" 97 | check(json"{$k:? $vOpt}", """{"a": 1}""") 98 | check(json"{$k:? $None}", """{}""") 99 | check(json"{$k:? $None, b: 1}", """{"b": 1}""") 100 | check(json"{b: 1, $k:? $None}", """{"b": 1}""") 101 | } 102 | 103 | test("can inject Option field values with implicit Writes") { 104 | val vOpt = Some(Foo("1", "2")) 105 | check(json"{a:? $vOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 106 | } 107 | 108 | test("can mix values, Iterables and Options in object") { 109 | val a = List("a" -> 10) 110 | val b = Some("b" -> 20) 111 | val c = Nil 112 | val d = None 113 | check( 114 | json"""{i:1, ..$a, i:2, ..$b, i:3, ..$c, i:4, ..$d, i:5}""", 115 | """{"i":1, "a":10, "i":2, "b":20, "i":3, "i":4, "i":5}""" 116 | ) 117 | } 118 | 119 | test("can nest jsonquote templates") { 120 | 121 | // adapted from the play docs: http://www.playframework.com/documentation/2.1.x/ScalaJson 122 | 123 | val jsonObject = Json("""{ 124 | users: [ 125 | { name: "Bob", age: 31, email: "bob@gmail.com" }, 126 | { name: "Kiki", age: 25 } 127 | ] 128 | }""") 129 | 130 | val users = Seq(("Bob", 31, Some("bob@gmail.com")), ("Kiki", 25, None)) 131 | 132 | // TODO: find a way to avoid the need for .toSeq here 133 | val quoteA = json"""{ 134 | users: [..${ 135 | users.map { case (name, age, email) => 136 | json"""{ 137 | name: $name, 138 | age: $age, 139 | email:? $email 140 | }""" 141 | }.toSeq 142 | }] 143 | }""" 144 | 145 | // we have a Writes to convert Seq[Json] to Json 146 | // still need the .toSeq here 147 | val quoteB = json"""{ 148 | users: ${ 149 | users.map { case (name, age, email) => 150 | json"""{ 151 | name: $name, 152 | age: $age, 153 | email:? $email 154 | }""" 155 | }.toSeq 156 | } 157 | }""" 158 | 159 | // types inferred properly here 160 | val mapped = users.map { case (name, age, email) => 161 | json"""{ 162 | name: $name, 163 | age: $age, 164 | email:? $email 165 | }""" 166 | } 167 | val quoteC = json"""{ 168 | users: [..$mapped] 169 | }""" 170 | 171 | quoteA should equal (jsonObject) 172 | quoteB should equal (jsonObject) 173 | quoteC should equal (jsonObject) 174 | } 175 | 176 | test("json parser can handle crazy javascript") { 177 | implicit val codec = Codec.UTF8 178 | val source = Source.fromURL(getClass.getResource("/sample.json")) 179 | Json(source.getLines.mkString) 180 | } 181 | 182 | test("json parser can handle inline comments") { 183 | check( 184 | json"""{ /* this is a test */ foo: "foo" } // trailing comment""", 185 | """{"foo":"foo"}""" 186 | ) 187 | check( 188 | json"""{ 189 | // some stuff here: ] 190 | key /* the key */ : // value next 191 | "value" /* that was the value */ 192 | , 193 | 194 | /********************************* 195 | * block comments can span lines * 196 | * unlike // comments * 197 | *********************************/ 198 | key2 : "value2" 199 | }""", 200 | """{"key":"value","key2":"value2"}""" 201 | ) 202 | } 203 | 204 | test("coalescing strips leading, trailing and internal double commas") { 205 | check(json"[..$Nil, 2]", "[2]") 206 | check(json"[1, ..$Nil]", "[1]") 207 | check(json"[1, ..$Nil, 2]", "[1,2]") 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /json4s/src/main/scala/net/maffoo/jsonquote/json4s/Compat.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.json4s 2 | 3 | import org.json4s._ 4 | import org.json4s.native.JsonMethods._ 5 | 6 | object Compat { 7 | def compactRender(json: JValue): String = compact(render(json)) 8 | } 9 | -------------------------------------------------------------------------------- /json4s/src/main/scala/net/maffoo/jsonquote/json4s/Parse.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.json4s 2 | 3 | import org.json4s._ 4 | import net.maffoo.jsonquote.Parser 5 | 6 | object Parse extends Parser[JValue, JField] { 7 | def makeObject(fields: Iterable[JField]): JValue = JObject(fields.toList) 8 | def makeArray(elements: Iterable[JValue]): JValue = JArray(elements.toList) 9 | def makeNumber(n: BigDecimal): JValue = if (n.isWhole) JInt(n.toBigInt) else JDouble(n.toDouble) 10 | def makeString(s: String): JValue = JString(s) 11 | def makeBoolean(b: Boolean): JValue = JBool(b) 12 | def makeNull(): JValue = JNull 13 | def makeSpliceValue(): JValue = SpliceValue() 14 | def makeSpliceValues(): JValue = SpliceValues() 15 | 16 | def makeField(k: String, v: JValue): JField = JField(k, v) 17 | def makeSpliceField(): JField = SpliceField() 18 | def makeSpliceFields(): JField = SpliceFields() 19 | def makeSpliceFieldNameOpt: JField = SpliceFieldNameOpt() 20 | def makeSpliceFieldName(v: JValue): JField = SpliceFieldName(v) 21 | def makeSpliceFieldOpt(k: String): JField = SpliceFieldOpt(k) 22 | } 23 | -------------------------------------------------------------------------------- /json4s/src/main/scala/net/maffoo/jsonquote/json4s/Splice.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.json4s 2 | 3 | import org.json4s._ 4 | 5 | // Sentinel values to mark splice points in the json AST. 6 | // When pattern matching to detect these, we use reference equality instead 7 | // of value equality, because we want to treat them specially even though 8 | // they may be valid values. 9 | 10 | class Sentinel[A <: AnyRef](inst: A) { 11 | def apply() = inst 12 | def unapply(a: A): Boolean = a eq inst 13 | } 14 | 15 | object SpliceValue extends Sentinel[JValue](new JObject(Nil)) 16 | object SpliceValues extends Sentinel[JValue](new JObject(Nil)) 17 | 18 | object SpliceField extends Sentinel[JField](new JField("", JNull)) 19 | object SpliceFields extends Sentinel[JField](new JField("", JNull)) 20 | 21 | object SpliceFieldNameOpt extends Sentinel[JField](new JField("", JNull)) 22 | 23 | object SpliceFieldName { 24 | def apply(x: JValue) = JField(null, x) 25 | def unapply(f: JField): Option[JValue] = f match { 26 | case JField(null, x) => Some(x) 27 | case _ => None 28 | } 29 | } 30 | 31 | object SpliceFieldOpt { 32 | def apply(k: String) = JField(k, null) 33 | def unapply(f: JField): Option[String] = f match { 34 | case JField(k, null) => Some(k) 35 | case _ => None 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /json4s/src/main/scala/net/maffoo/jsonquote/json4s/Writes.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.json4s 2 | 3 | import org.json4s._ 4 | 5 | // Typeclasses for converting to json4s JValues. 6 | // Borrowed from the Writes mechanism in play-json. 7 | 8 | trait Writes[-A] { 9 | def write(a: A): JValue 10 | } 11 | 12 | // Converters for basic scala types. 13 | object Writes { 14 | implicit object JValueWrites extends Writes[JValue] { 15 | def write(a: JValue): JValue = a 16 | } 17 | 18 | implicit object BoolWrites extends Writes[Boolean] { 19 | def write(b: Boolean): JValue = JBool(b) 20 | } 21 | 22 | implicit object ByteWrites extends Writes[Byte] { 23 | def write(n: Byte): JValue = JInt(n) 24 | } 25 | 26 | implicit object ShortWrites extends Writes[Short] { 27 | def write(n: Short): JValue = JInt(n) 28 | } 29 | 30 | implicit object IntWrites extends Writes[Int] { 31 | def write(n: Int): JValue = JInt(n) 32 | } 33 | 34 | implicit object LongWrites extends Writes[Long] { 35 | def write(n: Long): JValue = JInt(n) 36 | } 37 | 38 | implicit object DoubleWrites extends Writes[Double] { 39 | def write(n: Double): JValue = JDouble(n) 40 | } 41 | 42 | implicit object StringWrites extends Writes[String] { 43 | def write(s: String): JValue = JString(s) 44 | } 45 | 46 | // implicit def optionWrites[A: Writes]: Writes[Option[A]] = new Writes[Option[A]] { 47 | // def write(o: Option[A]): JValue = o match { 48 | // case Some(a) => implicitly[Writes[A]].write(a) 49 | // case None => JNull 50 | // } 51 | // } 52 | 53 | implicit def seqWrites[A: Writes]: Writes[Seq[A]] = new Writes[Seq[A]] { 54 | def write(s: Seq[A]): JValue = { 55 | val writer = implicitly[Writes[A]] 56 | JArray(s.map(writer.write).toList) 57 | } 58 | } 59 | 60 | implicit def mapWrites[A: Writes]: Writes[Map[String, A]] = new Writes[Map[String, A]] { 61 | def write(m: Map[String, A]): JValue = { 62 | val writer = implicitly[Writes[A]] 63 | JObject(m.map { case (k, v) => JField(k, writer.write(v)) }.toList) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /json4s/src/main/scala/net/maffoo/jsonquote/json4s/package.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | import org.json4s._ 4 | import net.maffoo.jsonquote.Macros._ 5 | import scala.language.experimental.macros 6 | 7 | package object json4s { 8 | /** 9 | * Rendering JValue produces valid json literal 10 | */ 11 | implicit class Json4sToLiteralJson(val json: JValue) extends AnyVal { 12 | def toLiteral: literal.Json = new literal.Json(Compat.compactRender(json)) 13 | } 14 | 15 | implicit class RichJsonSringContext(val sc: StringContext) extends AnyVal { 16 | def json(args: Any*): JValue = macro jsonImpl 17 | } 18 | 19 | def jsonImpl(c: Context)(args: c.Expr[Any]*): c.Expr[JValue] = { 20 | import c.universe._ 21 | 22 | // fully-qualified symbols and types (for hygiene) 23 | val bigInt = q"_root_.scala.math.BigInt" 24 | val list = q"_root_.scala.collection.immutable.List" 25 | val nil = q"_root_.scala.collection.immutable.Nil" 26 | val seq = q"_root_.scala.Seq" 27 | val jArray = q"_root_.org.json4s.JArray" 28 | val jField = q"_root_.org.json4s.JField" 29 | val jObject = q"_root_.org.json4s.JObject" 30 | val writesT = tq"_root_.net.maffoo.jsonquote.json4s.Writes" 31 | 32 | // convert the given json AST to a tree with arguments spliced in at the correct locations 33 | def splice(js: JValue)(implicit args: Iterator[Tree]): Tree = js match { 34 | case SpliceValue() => spliceValue(args.next) 35 | case SpliceValues() => c.abort(c.enclosingPosition, "cannot splice values at top level") 36 | 37 | case JObject(members) => 38 | val seqMembers = members.collect { 39 | case SpliceFields() => 40 | case SpliceFieldNameOpt() => 41 | case SpliceFieldOpt(k) => 42 | } 43 | if (seqMembers.isEmpty) { 44 | val ms = members.map { 45 | case SpliceField() => spliceField(args.next) 46 | case SpliceFieldName(v) => q"$jField(${spliceFieldName(args.next)}, ${splice(v)})" 47 | case JField(k, v) => q"$jField($k, ${splice(v)})" 48 | } 49 | q"$jObject($list(..$ms))" 50 | } else { 51 | val ms = members.map { 52 | case SpliceField() => q"$seq(${spliceField(args.next)})" 53 | case SpliceFields() => spliceFields(args.next) 54 | case SpliceFieldNameOpt() => spliceFieldNameOpt(args.next, args.next) 55 | case SpliceFieldName(v) => q"$seq($jField(${spliceFieldName(args.next)}, ${splice(v)}))" 56 | case SpliceFieldOpt(k) => spliceFieldOpt(k, args.next) 57 | case JField(k, v) => q"$seq($jField($k, ${splice(v)}))" 58 | } 59 | q"$jObject($list(..$ms).flatten)" 60 | } 61 | 62 | case JArray(elements) => 63 | val seqElems = elements.collect { 64 | case SpliceValues() => 65 | } 66 | if (seqElems.isEmpty) { 67 | val es = elements.map { 68 | case SpliceValue() => spliceValue(args.next) 69 | case e => splice(e) 70 | } 71 | q"$jArray($list(..$es))" 72 | } else { 73 | val es = elements.map { 74 | case SpliceValue() => q"$seq(${spliceValue(args.next)})" 75 | case SpliceValues() => spliceValues(args.next) 76 | case e => q"$seq(${splice(e)})" 77 | } 78 | q"$jArray($list(..$es).flatten)" 79 | } 80 | 81 | case JString(s) => q"_root_.org.json4s.JString($s)" 82 | case JDouble(n) => q"_root_.org.json4s.JDouble($n)" 83 | case JInt(n) => q"_root_.org.json4s.JInt($bigInt(${n.toString}))" 84 | case JBool(b) => q"_root_.org.json4s.JBool($b)" 85 | case JNull => q"_root_.org.json4s.JNull" 86 | } 87 | 88 | def spliceValue(e: Tree): Tree = e.tpe match { 89 | case t if t <:< c.typeOf[JValue] => e 90 | case t => 91 | inferWriter(e, t) 92 | q"implicitly[$writesT[$t]].write($e)" 93 | } 94 | 95 | def spliceValues(e: Tree): Tree = e.tpe match { 96 | case t if t <:< c.typeOf[Iterable[JValue]] => e 97 | case t if t <:< c.typeOf[Iterable[Any]] => 98 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[Nothing]] :: Nil))(0) 99 | val writer = inferWriter(e, valueTpe) 100 | q"$e.map($writer.write)" 101 | 102 | case t if t <:< c.typeOf[None.type] => nil 103 | case t if t <:< c.typeOf[Option[JValue]] => e 104 | case t if t <:< c.typeOf[Option[Any]] => 105 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 106 | val writer = inferWriter(e, valueTpe) 107 | q"$e.toIterable.map($writer.write)" 108 | 109 | case t => c.abort(e.pos, s"required Iterable[_] but got $t") 110 | } 111 | 112 | def spliceField(e: Tree): Tree = e.tpe match { 113 | case t if t <:< c.typeOf[JField] => e 114 | case t if t <:< c.typeOf[(String, JValue)] => 115 | q"val (k, v) = $e; $jField(k, v)" 116 | case t if t <:< c.typeOf[(String, Any)] => 117 | val valueTpe = typeParams(lub(t :: c.typeOf[(String, Nothing)] :: Nil))(1) 118 | val writer = inferWriter(e, valueTpe) 119 | q"val (k, v) = $e; $jField(k, $writer.write(v))" 120 | 121 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 122 | } 123 | 124 | def spliceFields(e: Tree): Tree = e.tpe match { 125 | case t if t <:< c.typeOf[Iterable[JField]] => e 126 | case t if t <:< c.typeOf[Iterable[(String, JValue)]] => 127 | q"$e.map { case (k, v) => $jField(k, v) }" 128 | case t if t <:< c.typeOf[Iterable[(String, Any)]] => 129 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[(String, Nothing)]] :: Nil))(2) 130 | val writer = inferWriter(e, valueTpe) 131 | q"$e.map { case (k, v) => $jField(k, $writer.write(v)) }" 132 | 133 | case t if t <:< c.typeOf[None.type] => nil 134 | case t if t <:< c.typeOf[Option[JField]] => 135 | q"$e.toIterable" 136 | case t if t <:< c.typeOf[Option[(String, JValue)]] => 137 | q"$e.toIterable.map { case (k, v) => $jField(k, v) }" 138 | case t if t <:< c.typeOf[Option[(String, Any)]] => 139 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[(String, Nothing)]] :: Nil))(2) 140 | val writer = inferWriter(e, valueTpe) 141 | q"$e.toIterable.map { case (k, v) => $jField(k, $writer.write(v)) }" 142 | 143 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 144 | } 145 | 146 | def spliceFieldOpt(k: String, e: Tree): Tree = e.tpe match { 147 | case t if t <:< c.typeOf[None.type] => nil 148 | case t if t <:< c.typeOf[Option[JValue]] => q"$e.toIterable.map(v => $jField($k, v))" 149 | case t if t <:< c.typeOf[Option[Any]] => 150 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 151 | val writer = inferWriter(e, valueTpe) 152 | q"$e.toIterable.map { v => $jField($k, $writer.write(v)) }" 153 | 154 | case t => c.abort(e.pos, s"required Option[_] but got $t") 155 | } 156 | 157 | def spliceFieldName(e: Tree): Tree = e.tpe match { 158 | case t if t =:= c.typeOf[String] => e 159 | case t => c.abort(e.pos, s"required String but got $t") 160 | } 161 | 162 | def spliceFieldNameOpt(k: Tree, v: Tree): Tree = { 163 | k.tpe match { 164 | case t if t =:= c.typeOf[String] => 165 | case t => c.abort(k.pos, s"required String but got $t") 166 | } 167 | v.tpe match { 168 | case t if t <:< c.typeOf[None.type] => nil 169 | case t if t <:< c.typeOf[Option[JValue]] => q"$v.toIterable.map(v => $jField($k, v))" 170 | case t if t <:< c.typeOf[Option[Any]] => 171 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 172 | val writer = inferWriter(v, valueTpe) 173 | q"$v.toIterable.map { v => $jField($k, $writer.write(v)) }" 174 | 175 | case t => c.abort(v.pos, s"required Option[_] but got $t") 176 | } 177 | } 178 | 179 | // return a list of type parameters in the given type 180 | // example: List[(String, Int)] => Seq(Tuple2, String, Int) 181 | def typeParams(tpe: Type): Seq[Type] = { 182 | val b = Iterable.newBuilder[Type] 183 | tpe.foreach(b += _) 184 | b.result.drop(2).grouped(2).map(_.head).toIndexedSeq 185 | } 186 | 187 | // locate an implicit Writes[T] for the given type 188 | def inferWriter(e: Tree, t: Type): Tree = { 189 | val writerTpe = appliedType(c.typeOf[Writes[_]], List(t)) 190 | c.inferImplicitValue(writerTpe) match { 191 | case EmptyTree => c.abort(e.pos, s"could not find implicit value of type Writes[$t]") 192 | case tree => tree 193 | } 194 | } 195 | 196 | // Parse the string context parts into a json AST with holes, and then 197 | // typecheck/convert args to the appropriate types and splice them in. 198 | c.prefix.tree match { 199 | case Apply(_, List(Apply(_, partTrees))) => 200 | val parts = partTrees map { case Literal(Constant(const: String)) => const } 201 | val positions = (parts zip partTrees.map(_.pos)).toMap 202 | val js = try { 203 | Parse(parts) 204 | } catch { 205 | case JsonError(msg, Pos(s, ofs)) => 206 | val pos = positions(s) 207 | c.abort(pos.withPoint(pos.point + ofs), msg) 208 | } 209 | c.Expr[JValue](splice(js)(args.iterator.map(_.tree))) 210 | 211 | case _ => 212 | c.abort(c.enclosingPosition, "invalid") 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /json4s/src/test/scala/net/maffoo/jsonquote/json4s/Test.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.json4s 2 | 3 | import org.json4s._ 4 | import org.json4s.native.JsonMethods.parse 5 | import net.maffoo.jsonquote.json4s.Writes._ 6 | import org.scalatest.{FunSuite, Matchers} 7 | 8 | case class Foo(bar: String, baz: String) 9 | 10 | class Json4sTest extends FunSuite with Matchers { 11 | 12 | implicit val formats = DefaultFormats 13 | 14 | implicit object FooWrites extends Writes[Foo] { 15 | def write(foo: Foo): JValue = json"{ bar: ${foo.bar}, baz: ${foo.baz} }" 16 | } 17 | 18 | def check(js: JValue, s: String): Unit = { 19 | val JArray(List(expected)) = parse(s"[$s]") 20 | js should equal (expected) 21 | } 22 | 23 | //json"""{test: ${Map("test" -> "foo")}}""" 24 | 25 | test("can parse plain json") { 26 | check(json""" "hello!" """, """"hello!"""") 27 | } 28 | 29 | test("can use bare identifiers for object keys") { 30 | check(json"{ test0: 0 }", """{ "test0": 0 }""") 31 | check(json"{ test: 1 }", """{ "test": 1 }""") 32 | check(json"{ test-2: 2 }", """{ "test-2": 2 }""") 33 | check(json"{ test_3: 3 }", """{ "test_3": 3 }""") 34 | check(json"{ _test-4: 4 }", """{ "_test-4": 4 }""") 35 | } 36 | 37 | test("can inject value with implicit Writes") { 38 | val foo = Foo(bar = "a", baz = "b") 39 | check(json"$foo", """{"bar": "a", "baz": "b"}""") 40 | } 41 | 42 | test("can inject values with implicit Writes") { 43 | val bool = true 44 | val foos = List(Foo("1", "2"), Foo("3", "4")) 45 | check(json"$bool", """true""") 46 | check(json"[..$foos]", """[{"bar":"1", "baz":"2"}, {"bar":"3", "baz":"4"}]""") 47 | check(json"[..$Nil]", """[]""") 48 | } 49 | 50 | test("can inject a map of writeable values") { 51 | val foos: Map[String, Int] = scala.collection.immutable.SortedMap("a" -> 1, "b" -> 2) 52 | check(json"$foos", """{"a":1, "b":2}""") 53 | } 54 | 55 | test("can inject Option values") { 56 | val vOpt = Some(JInt(1)) 57 | check(json"[..$vOpt]", """[1]""") 58 | check(json"[..$None]" , """[]""") 59 | } 60 | 61 | test("can inject Option values with implicit Writes") { 62 | val vOpt = Some(1) 63 | check(json"[..$vOpt]", """[1]""") 64 | } 65 | 66 | test("can mix values, Iterables and Options in array") { 67 | val a = List("a") 68 | val b = Some("b") 69 | val c = Nil 70 | val d = None 71 | check(json"""[1, ..$a, 2, ..$b, 3, ..$c, 4, ..$d, 5]""", """[1, "a", 2, "b", 3, 4, 5]""") 72 | } 73 | 74 | test("can inject Tuple2 as object field") { 75 | val kv = "test" -> Foo("1", "2") 76 | check(json"{$kv}", """{"test": {"bar":"1", "baz":"2"}}""") 77 | } 78 | 79 | test("can inject multiple fields") { 80 | val kvs = Seq("a" -> 1, "b" -> 2) 81 | check(json"{..$kvs}", """{"a": 1, "b": 2}""") 82 | check(json"{..$Nil}", """{}""") 83 | } 84 | 85 | test("can inject just a field name") { 86 | val key = "foo" 87 | check(json"{$key: 1}", """{"foo": 1}""") 88 | } 89 | 90 | test("can inject Option fields") { 91 | val kvOpt = Some("a" -> JInt(1)) 92 | check(json"{..$kvOpt}", """{"a": 1}""") 93 | check(json"{..$None}", """{}""") 94 | } 95 | 96 | test("can inject Option fields with implicit Writes") { 97 | val kvOpt = Some("a" -> Foo("1", "2")) 98 | check(json"{..$kvOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 99 | } 100 | 101 | test("can inject Option field values") { 102 | val vOpt = Some(JInt(1)) 103 | check(json"{a:? $vOpt}", """{"a": 1}""") 104 | check(json"{a:? $None}", """{}""") 105 | 106 | val k = "a" 107 | check(json"{$k:? $vOpt}", """{"a": 1}""") 108 | check(json"{$k:? $None}", """{}""") 109 | } 110 | 111 | test("can inject Option field values with implicit Writes") { 112 | val vOpt = Some(Foo("1", "2")) 113 | check(json"{a:? $vOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 114 | } 115 | 116 | test("can mix values, Iterables and Options in object") { 117 | val a = List("a" -> 10) 118 | val b = Some("b" -> 20) 119 | val c = Nil 120 | val d = None 121 | check( 122 | json"""{i:1, ..$a, i:2, ..$b, i:3, ..$c, i:4, ..$d, i:5}""", 123 | """{"i":1, "a":10, "i":2, "b":20, "i":3, "i":4, "i":5}""" 124 | ) 125 | } 126 | 127 | test("can nest jsonquote templates") { 128 | 129 | // adapted from the play docs: http://www.playframework.com/documentation/2.1.x/ScalaJson 130 | import org.json4s.JsonDSL._ 131 | 132 | val jsonObject = 133 | JObject(List( 134 | JField("users", 135 | JArray(List( 136 | ("name" -> "Bob") ~ ("age" -> 31) ~ ("email" -> "bob@gmail.com"), 137 | ("name" -> "Kiki") ~ ("age" -> 25) 138 | )) 139 | ) 140 | )) 141 | 142 | val users = Seq(("Bob", 31, Some("bob@gmail.com")), ("Kiki", 25, None)) 143 | 144 | // TODO: find a way to avoid the need for .toSeq here 145 | val quoteA = json"""{ 146 | users: [..${ 147 | users.map { case (name, age, email) => 148 | json"""{ 149 | name: $name, 150 | age: $age, 151 | email:? $email 152 | }""" 153 | }.toSeq 154 | }] 155 | }""" 156 | 157 | // we defined a Serializer for Seq[JValue] 158 | // still need the .toSeq here 159 | val quoteB = json"""{ 160 | users: ${ 161 | users.map { case (name, age, email) => 162 | json"""{ 163 | name: $name, 164 | age: $age, 165 | email:? $email 166 | }""" 167 | }.toSeq 168 | } 169 | }""" 170 | 171 | // types inferred properly here 172 | val mapped = users.map { case (name, age, email) => 173 | json"""{ 174 | name: $name, 175 | age: $age, 176 | email:? $email 177 | }""" 178 | } 179 | val quoteC = json"""{ 180 | users: [..$mapped] 181 | }""" 182 | 183 | quoteA should equal (jsonObject) 184 | quoteB should equal (jsonObject) 185 | quoteC should equal (jsonObject) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /lift/src/main/scala/net/maffoo/jsonquote/lift/Parse.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.lift 2 | 3 | import net.liftweb.json._ 4 | import net.maffoo.jsonquote.Parser 5 | 6 | object Parse extends Parser[JValue, JField] { 7 | def makeObject(fields: Iterable[JField]): JValue = JObject(fields.toList) 8 | def makeArray(elements: Iterable[JValue]): JValue = JArray(elements.toList) 9 | def makeNumber(n: BigDecimal): JValue = if (n.isWhole) JInt(n.toBigInt) else JDouble(n.toDouble) 10 | def makeString(s: String): JValue = JString(s) 11 | def makeBoolean(b: Boolean): JValue = JBool(b) 12 | def makeNull(): JValue = JNull 13 | def makeSpliceValue(): JValue = SpliceValue() 14 | def makeSpliceValues(): JValue = SpliceValues() 15 | 16 | def makeField(k: String, v: JValue): JField = JField(k, v) 17 | def makeSpliceField(): JField = SpliceField() 18 | def makeSpliceFields(): JField = SpliceFields() 19 | def makeSpliceFieldNameOpt: JField = SpliceFieldNameOpt() 20 | def makeSpliceFieldName(v: JValue): JField = SpliceFieldName(v) 21 | def makeSpliceFieldOpt(k: String): JField = SpliceFieldOpt(k) 22 | } 23 | -------------------------------------------------------------------------------- /lift/src/main/scala/net/maffoo/jsonquote/lift/Splice.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.lift 2 | 3 | import net.liftweb.json._ 4 | 5 | // Sentinel values to mark splice points in the json AST. 6 | // When pattern matching to detect these, we use reference equality instead 7 | // of value equality, because we want to treat them specially even though 8 | // they may be valid values. 9 | 10 | class Sentinel[A <: AnyRef](inst: A) { 11 | def apply() = inst 12 | def unapply(a: A): Boolean = a eq inst 13 | } 14 | 15 | object SpliceValue extends Sentinel[JValue](new JObject(Nil)) 16 | object SpliceValues extends Sentinel[JValue](new JObject(Nil)) 17 | 18 | object SpliceField extends Sentinel[JField](new JField("", JNull)) 19 | object SpliceFields extends Sentinel[JField](new JField("", JNull)) 20 | 21 | object SpliceFieldNameOpt extends Sentinel[JField](new JField("", JNull)) 22 | 23 | object SpliceFieldName { 24 | def apply(x: JValue) = JField(null, x) 25 | def unapply(f: JField): Option[JValue] = f match { 26 | case JField(null, x) => Some(x) 27 | case _ => None 28 | } 29 | } 30 | 31 | object SpliceFieldOpt { 32 | def apply(k: String) = JField(k, null) 33 | def unapply(f: JField): Option[String] = f match { 34 | case JField(k, null) => Some(k) 35 | case _ => None 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lift/src/main/scala/net/maffoo/jsonquote/lift/Writes.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.lift 2 | 3 | import net.liftweb.json._ 4 | 5 | // Typeclasses for converting to lift JValues. 6 | // Borrowed from the Writes mechanism in play-json. 7 | 8 | trait Writes[-A] { 9 | def write(a: A): JValue 10 | } 11 | 12 | // Converters for basic scala types. 13 | object Writes { 14 | implicit object JValueWrites extends Writes[JValue] { 15 | def write(a: JValue): JValue = a 16 | } 17 | 18 | implicit object BoolWrites extends Writes[Boolean] { 19 | def write(b: Boolean): JValue = JBool(b) 20 | } 21 | 22 | implicit object ByteWrites extends Writes[Byte] { 23 | def write(n: Byte): JValue = JInt(n) 24 | } 25 | 26 | implicit object ShortWrites extends Writes[Short] { 27 | def write(n: Short): JValue = JInt(n) 28 | } 29 | 30 | implicit object IntWrites extends Writes[Int] { 31 | def write(n: Int): JValue = JInt(n) 32 | } 33 | 34 | implicit object LongWrites extends Writes[Long] { 35 | def write(n: Long): JValue = JInt(n) 36 | } 37 | 38 | implicit object DoubleWrites extends Writes[Double] { 39 | def write(n: Double): JValue = JDouble(n) 40 | } 41 | 42 | implicit object StringWrites extends Writes[String] { 43 | def write(s: String): JValue = JString(s) 44 | } 45 | 46 | // implicit def optionWrites[A: Writes]: Writes[Option[A]] = new Writes[Option[A]] { 47 | // def write(o: Option[A]): JValue = o match { 48 | // case Some(a) => implicitly[Writes[A]].write(a) 49 | // case None => JNull 50 | // } 51 | // } 52 | 53 | implicit def seqWrites[A: Writes]: Writes[Seq[A]] = new Writes[Seq[A]] { 54 | def write(s: Seq[A]): JValue = { 55 | val writer = implicitly[Writes[A]] 56 | JArray(s.map(writer.write).toList) 57 | } 58 | } 59 | 60 | implicit def mapWrites[A: Writes]: Writes[Map[String, A]] = new Writes[Map[String, A]] { 61 | def write(m: Map[String, A]): JValue = { 62 | val writer = implicitly[Writes[A]] 63 | JObject(m.map { case (k, v) => JField(k, writer.write(v)) }.toList) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lift/src/main/scala/net/maffoo/jsonquote/lift/package.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | import net.liftweb.json._ 4 | import net.maffoo.jsonquote.Macros._ 5 | import scala.language.experimental.macros 6 | 7 | package object lift { 8 | /** 9 | * Rendering JValue produces valid json literal 10 | */ 11 | implicit class LiftToLiteralJson(val json: JValue) extends AnyVal { 12 | def toLiteral: literal.Json = new literal.Json(compactRender(json)) 13 | } 14 | 15 | implicit class RichJsonSringContext(val sc: StringContext) extends AnyVal { 16 | def json(args: Any*): JValue = macro jsonImpl 17 | } 18 | 19 | def jsonImpl(c: Context)(args: c.Expr[Any]*): c.Expr[JValue] = { 20 | import c.universe._ 21 | 22 | // fully-qualified symbols and types (for hygiene) 23 | val bigInt = q"_root_.scala.math.BigInt" 24 | val list = q"_root_.scala.collection.immutable.List" 25 | val nil = q"_root_.scala.collection.immutable.Nil" 26 | val seq = q"_root_.scala.Seq" 27 | val jArray = q"_root_.net.liftweb.json.JArray" 28 | val jField = q"_root_.net.liftweb.json.JField" 29 | val jObject = q"_root_.net.liftweb.json.JObject" 30 | val writesT = tq"_root_.net.maffoo.jsonquote.lift.Writes" 31 | 32 | // convert the given json AST to a tree with arguments spliced in at the correct locations 33 | def splice(js: JValue)(implicit args: Iterator[Tree]): Tree = js match { 34 | case SpliceValue() => spliceValue(args.next) 35 | case SpliceValues() => c.abort(c.enclosingPosition, "cannot splice values at top level") 36 | 37 | case JObject(members) => 38 | val seqMembers = members.collect { 39 | case SpliceFields() => 40 | case SpliceFieldNameOpt() => 41 | case SpliceFieldOpt(k) => 42 | } 43 | if (seqMembers.isEmpty) { 44 | val ms = members.map { 45 | case SpliceField() => spliceField(args.next) 46 | case SpliceFieldName(v) => q"$jField(${spliceFieldName(args.next)}, ${splice(v)})" 47 | case JField(k, v) => q"$jField($k, ${splice(v)})" 48 | } 49 | q"$jObject($list(..$ms))" 50 | } else { 51 | val ms = members.map { 52 | case SpliceField() => q"$seq(${spliceField(args.next)})" 53 | case SpliceFields() => spliceFields(args.next) 54 | case SpliceFieldNameOpt() => spliceFieldNameOpt(args.next, args.next) 55 | case SpliceFieldName(v) => q"$seq($jField(${spliceFieldName(args.next)}, ${splice(v)}))" 56 | case SpliceFieldOpt(k) => spliceFieldOpt(k, args.next) 57 | case JField(k, v) => q"$seq($jField($k, ${splice(v)}))" 58 | } 59 | q"$jObject($list(..$ms).flatten)" 60 | } 61 | 62 | case JArray(elements) => 63 | val seqElems = elements.collect { 64 | case SpliceValues() => 65 | } 66 | if (seqElems.isEmpty) { 67 | val es = elements.map { 68 | case SpliceValue() => spliceValue(args.next) 69 | case e => splice(e) 70 | } 71 | q"$jArray($list(..$es))" 72 | } else { 73 | val es = elements.map { 74 | case SpliceValue() => q"$seq(${spliceValue(args.next)})" 75 | case SpliceValues() => spliceValues(args.next) 76 | case e => q"$seq(${splice(e)})" 77 | } 78 | q"$jArray($list(..$es).flatten)" 79 | } 80 | 81 | case JString(s) => q"_root_.net.liftweb.json.JString($s)" 82 | case JDouble(n) => q"_root_.net.liftweb.json.JDouble($n)" 83 | case JInt(n) => q"_root_.net.liftweb.json.JInt($bigInt(${n.toString}))" 84 | case JBool(b) => q"_root_.net.liftweb.json.JBool($b)" 85 | case JNull => q"_root_.net.liftweb.json.JNull" 86 | } 87 | 88 | def spliceValue(e: Tree): Tree = e.tpe match { 89 | case t if t <:< c.typeOf[JValue] => e 90 | case t => 91 | inferWriter(e, t) 92 | q"implicitly[$writesT[$t]].write($e)" 93 | } 94 | 95 | def spliceValues(e: Tree): Tree = e.tpe match { 96 | case t if t <:< c.typeOf[Iterable[JValue]] => e 97 | case t if t <:< c.typeOf[Iterable[Any]] => 98 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[Nothing]] :: Nil))(0) 99 | val writer = inferWriter(e, valueTpe) 100 | q"$e.map($writer.write)" 101 | 102 | case t if t <:< c.typeOf[None.type] => nil 103 | case t if t <:< c.typeOf[Option[JValue]] => e 104 | case t if t <:< c.typeOf[Option[Any]] => 105 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 106 | val writer = inferWriter(e, valueTpe) 107 | q"$e.toIterable.map($writer.write)" 108 | 109 | case t => c.abort(e.pos, s"required Iterable[_] but got $t") 110 | } 111 | 112 | def spliceField(e: Tree): Tree = e.tpe match { 113 | case t if t <:< c.typeOf[JField] => e 114 | case t if t <:< c.typeOf[(String, JValue)] => 115 | q"val (k, v) = $e; $jField(k, v)" 116 | case t if t <:< c.typeOf[(String, Any)] => 117 | val valueTpe = typeParams(lub(t :: c.typeOf[(String, Nothing)] :: Nil))(1) 118 | val writer = inferWriter(e, valueTpe) 119 | q"val (k, v) = $e; $jField(k, $writer.write(v))" 120 | 121 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 122 | } 123 | 124 | def spliceFields(e: Tree): Tree = e.tpe match { 125 | case t if t <:< c.typeOf[Iterable[JField]] => e 126 | case t if t <:< c.typeOf[Iterable[(String, JValue)]] => 127 | q"$e.map { case (k, v) => $jField(k, v) }" 128 | case t if t <:< c.typeOf[Iterable[(String, Any)]] => 129 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[(String, Nothing)]] :: Nil))(2) 130 | val writer = inferWriter(e, valueTpe) 131 | q"$e.map { case (k, v) => $jField(k, $writer.write(v)) }" 132 | 133 | case t if t <:< c.typeOf[None.type] => nil 134 | case t if t <:< c.typeOf[Option[JField]] => 135 | q"$e.toIterable" 136 | case t if t <:< c.typeOf[Option[(String, JValue)]] => 137 | q"$e.toIterable.map { case (k, v) => $jField(k, v) }" 138 | case t if t <:< c.typeOf[Option[(String, Any)]] => 139 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[(String, Nothing)]] :: Nil))(2) 140 | val writer = inferWriter(e, valueTpe) 141 | q"$e.toIterable.map { case (k, v) => $jField(k, $writer.write(v)) }" 142 | 143 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 144 | } 145 | 146 | def spliceFieldOpt(k: String, e: Tree): Tree = e.tpe match { 147 | case t if t <:< c.typeOf[None.type] => nil 148 | case t if t <:< c.typeOf[Option[JValue]] => q"$e.toIterable.map(v => $jField($k, v))" 149 | case t if t <:< c.typeOf[Option[Any]] => 150 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 151 | val writer = inferWriter(e, valueTpe) 152 | q"$e.toIterable.map { v => $jField($k, $writer.write(v)) }" 153 | 154 | case t => c.abort(e.pos, s"required Option[_] but got $t") 155 | } 156 | 157 | def spliceFieldName(e: Tree): Tree = e.tpe match { 158 | case t if t =:= c.typeOf[String] => e 159 | case t => c.abort(e.pos, s"required String but got $t") 160 | } 161 | 162 | def spliceFieldNameOpt(k: Tree, v: Tree): Tree = { 163 | k.tpe match { 164 | case t if t =:= c.typeOf[String] => 165 | case t => c.abort(k.pos, s"required String but got $t") 166 | } 167 | v.tpe match { 168 | case t if t <:< c.typeOf[None.type] => nil 169 | case t if t <:< c.typeOf[Option[JValue]] => q"$v.toIterable.map(v => $jField($k, v))" 170 | case t if t <:< c.typeOf[Option[Any]] => 171 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 172 | val writer = inferWriter(v, valueTpe) 173 | q"$v.toIterable.map { v => $jField($k, $writer.write(v)) }" 174 | 175 | case t => c.abort(v.pos, s"required Option[_] but got $t") 176 | } 177 | } 178 | 179 | // return a list of type parameters in the given type 180 | // example: List[(String, Int)] => Seq(Tuple2, String, Int) 181 | def typeParams(tpe: Type): Seq[Type] = { 182 | val b = Iterable.newBuilder[Type] 183 | tpe.foreach(b += _) 184 | b.result.drop(2).grouped(2).map(_.head).toIndexedSeq 185 | } 186 | 187 | // locate an implicit Writes[T] for the given type 188 | def inferWriter(e: Tree, t: Type): Tree = { 189 | val writerTpe = appliedType(c.typeOf[Writes[_]], List(t)) 190 | c.inferImplicitValue(writerTpe) match { 191 | case EmptyTree => c.abort(e.pos, s"could not find implicit value of type Writes[$t]") 192 | case tree => tree 193 | } 194 | } 195 | 196 | // Parse the string context parts into a json AST with holes, and then 197 | // typecheck/convert args to the appropriate types and splice them in. 198 | c.prefix.tree match { 199 | case Apply(_, List(Apply(_, partTrees))) => 200 | val parts = partTrees map { case Literal(Constant(const: String)) => const } 201 | val positions = (parts zip partTrees.map(_.pos)).toMap 202 | val js = try { 203 | Parse(parts) 204 | } catch { 205 | case JsonError(msg, Pos(s, ofs)) => 206 | val pos = positions(s) 207 | c.abort(pos.withPoint(pos.point + ofs), msg) 208 | } 209 | c.Expr[JValue](splice(js)(args.iterator.map(_.tree))) 210 | 211 | case _ => 212 | c.abort(c.enclosingPosition, "invalid") 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lift/src/test/scala/net/maffoo/jsonquote/lift/Test.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.lift 2 | 3 | import net.liftweb.json._ 4 | import net.maffoo.jsonquote.lift.Writes._ 5 | import org.scalatest.{FunSuite, Matchers} 6 | 7 | case class Foo(bar: String, baz: String) 8 | 9 | class LiftTest extends FunSuite with Matchers { 10 | 11 | implicit val formats = DefaultFormats 12 | 13 | implicit object FooWrites extends Writes[Foo] { 14 | def write(foo: Foo): JValue = json"{ bar: ${foo.bar}, baz: ${foo.baz} }" 15 | } 16 | 17 | def check(js: JValue, s: String): Unit = { js should equal (parse(s)) } 18 | 19 | //json"""{test: ${Map("test" -> "foo")}}""" 20 | 21 | test("can parse plain json") { 22 | check(json""" "hello!" """, """"hello!"""") 23 | } 24 | 25 | test("can use bare identifiers for object keys") { 26 | check(json"{ test0: 0 }", """{ "test0": 0 }""") 27 | check(json"{ test: 1 }", """{ "test": 1 }""") 28 | check(json"{ test-2: 2 }", """{ "test-2": 2 }""") 29 | check(json"{ test_3: 3 }", """{ "test_3": 3 }""") 30 | check(json"{ _test-4: 4 }", """{ "_test-4": 4 }""") 31 | } 32 | 33 | test("can inject value with implicit Writes") { 34 | val foo = Foo(bar = "a", baz = "b") 35 | check(json"$foo", """{"bar": "a", "baz": "b"}""") 36 | } 37 | 38 | test("can inject values with implicit Writes") { 39 | val bool = true 40 | val foos = List(Foo("1", "2"), Foo("3", "4")) 41 | check(json"$bool", """true""") 42 | check(json"[..$foos]", """[{"bar":"1", "baz":"2"}, {"bar":"3", "baz":"4"}]""") 43 | check(json"[..$Nil]", """[]""") 44 | } 45 | 46 | test("can inject a map of writeable values") { 47 | val foos: Map[String, Int] = scala.collection.immutable.SortedMap("a" -> 1, "b" -> 2) 48 | check(json"$foos", """{"a":1, "b":2}""") 49 | } 50 | 51 | test("can inject Option values") { 52 | val vOpt = Some(JInt(1)) 53 | check(json"[..$vOpt]", """[1]""") 54 | check(json"[..$None]" , """[]""") 55 | } 56 | 57 | test("can inject Option values with implicit Writes") { 58 | val vOpt = Some(1) 59 | check(json"[..$vOpt]", """[1]""") 60 | } 61 | 62 | test("can mix values, Iterables and Options in array") { 63 | val a = List("a") 64 | val b = Some("b") 65 | val c = Nil 66 | val d = None 67 | check(json"""[1, ..$a, 2, ..$b, 3, ..$c, 4, ..$d, 5]""", """[1, "a", 2, "b", 3, 4, 5]""") 68 | } 69 | 70 | test("can inject Tuple2 as object field") { 71 | val kv = "test" -> Foo("1", "2") 72 | check(json"{$kv}", """{"test": {"bar":"1", "baz":"2"}}""") 73 | } 74 | 75 | test("can inject multiple fields") { 76 | val kvs = Seq("a" -> 1, "b" -> 2) 77 | check(json"{..$kvs}", """{"a": 1, "b": 2}""") 78 | check(json"{..$Nil}", """{}""") 79 | } 80 | 81 | test("can inject just a field name") { 82 | val key = "foo" 83 | check(json"{$key: 1}", """{"foo": 1}""") 84 | } 85 | 86 | test("can inject Option fields") { 87 | val kvOpt = Some("a" -> JInt(1)) 88 | check(json"{..$kvOpt}", """{"a": 1}""") 89 | check(json"{..$None}", """{}""") 90 | } 91 | 92 | test("can inject Option fields with implicit Writes") { 93 | val kvOpt = Some("a" -> Foo("1", "2")) 94 | check(json"{..$kvOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 95 | } 96 | 97 | test("can inject Option field values") { 98 | val vOpt = Some(JInt(1)) 99 | check(json"{a:? $vOpt}", """{"a": 1}""") 100 | check(json"{a:? $None}", """{}""") 101 | 102 | val k = "a" 103 | check(json"{$k:? $vOpt}", """{"a": 1}""") 104 | check(json"{$k:? $None}", """{}""") 105 | } 106 | 107 | test("can inject Option field values with implicit Writes") { 108 | val vOpt = Some(Foo("1", "2")) 109 | check(json"{a:? $vOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 110 | } 111 | 112 | test("can mix values, Iterables and Options in object") { 113 | val a = List("a" -> 10) 114 | val b = Some("b" -> 20) 115 | val c = Nil 116 | val d = None 117 | check( 118 | json"""{i:1, ..$a, i:2, ..$b, i:3, ..$c, i:4, ..$d, i:5}""", 119 | """{"i":1, "a":10, "i":2, "b":20, "i":3, "i":4, "i":5}""" 120 | ) 121 | } 122 | 123 | test("can nest jsonquote templates") { 124 | 125 | // adapted from the play docs: http://www.playframework.com/documentation/2.1.x/ScalaJson 126 | import net.liftweb.json.JsonDSL._ 127 | 128 | val jsonObject = 129 | JObject(List( 130 | JField("users", 131 | JArray(List( 132 | ("name" -> "Bob") ~ ("age" -> 31) ~ ("email" -> "bob@gmail.com"), 133 | ("name" -> "Kiki") ~ ("age" -> 25) 134 | )) 135 | ) 136 | )) 137 | 138 | val users = Seq(("Bob", 31, Some("bob@gmail.com")), ("Kiki", 25, None)) 139 | 140 | // TODO: find a way to avoid the need for .toSeq here 141 | val quoteA = json"""{ 142 | users: [..${ 143 | users.map { case (name, age, email) => 144 | json"""{ 145 | name: $name, 146 | age: $age, 147 | email:? $email 148 | }""" 149 | }.toSeq 150 | }] 151 | }""" 152 | 153 | // we defined a Serializer for Seq[JValue] 154 | // still need the .toSeq here 155 | val quoteB = json"""{ 156 | users: ${ 157 | users.map { case (name, age, email) => 158 | json"""{ 159 | name: $name, 160 | age: $age, 161 | email:? $email 162 | }""" 163 | }.toSeq 164 | } 165 | }""" 166 | 167 | // types inferred properly here 168 | val mapped = users.map { case (name, age, email) => 169 | json"""{ 170 | name: $name, 171 | age: $age, 172 | email:? $email 173 | }""" 174 | } 175 | val quoteC = json"""{ 176 | users: [..$mapped] 177 | }""" 178 | 179 | quoteA should equal (jsonObject) 180 | quoteB should equal (jsonObject) 181 | quoteC should equal (jsonObject) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /mill: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This is a wrapper script, that automatically download mill from GitHub release pages 4 | # You can give the required mill version with MILL_VERSION env variable 5 | # If no version is given, it falls back to the value of DEFAULT_MILL_VERSION 6 | DEFAULT_MILL_VERSION=0.9.5 7 | 8 | set -e 9 | 10 | if [ -z "$MILL_VERSION" ] ; then 11 | if [ -f ".mill-version" ] ; then 12 | MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)" 13 | elif [ -f "mill" ] && [ "$BASH_SOURCE" != "mill" ] ; then 14 | MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2) 15 | else 16 | MILL_VERSION=$DEFAULT_MILL_VERSION 17 | fi 18 | fi 19 | 20 | if [ "x${XDG_CACHE_HOME}" != "x" ] ; then 21 | MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download" 22 | else 23 | MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download" 24 | fi 25 | MILL_EXEC_PATH="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}" 26 | 27 | version_remainder="$MILL_VERSION" 28 | MILL_MAJOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" 29 | MILL_MINOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" 30 | 31 | if [ ! -x "$MILL_EXEC_PATH" ] ; then 32 | mkdir -p $MILL_DOWNLOAD_PATH 33 | if [ "$MILL_MAJOR_VERSION" -gt 0 ] || [ "$MILL_MINOR_VERSION" -ge 5 ] ; then 34 | ASSEMBLY="-assembly" 35 | fi 36 | DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download 37 | MILL_DOWNLOAD_URL="https://github.com/lihaoyi/mill/releases/download/${MILL_VERSION%%-*}/$MILL_VERSION${ASSEMBLY}" 38 | curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL" 39 | chmod +x "$DOWNLOAD_FILE" 40 | mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH" 41 | unset DOWNLOAD_FILE 42 | unset MILL_DOWNLOAD_URL 43 | fi 44 | 45 | unset MILL_DOWNLOAD_PATH 46 | unset MILL_VERSION 47 | 48 | exec $MILL_EXEC_PATH "$@" 49 | -------------------------------------------------------------------------------- /play/src/main/scala/net/maffoo/jsonquote/play/Parse.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.play 2 | 3 | import net.maffoo.jsonquote.Parser 4 | import _root_.play.api.libs.json._ 5 | 6 | object Parse extends Parser[JsValue, (String, JsValue)] { 7 | def makeObject(fields: Iterable[(String, JsValue)]): JsValue = JsObject(fields.toIndexedSeq) 8 | def makeArray(elements: Iterable[JsValue]): JsValue = JsArray(elements.toIndexedSeq) 9 | def makeNumber(n: BigDecimal): JsValue = JsNumber(n) 10 | def makeString(s: String): JsValue = JsString(s) 11 | def makeBoolean(b: Boolean): JsValue = JsBoolean(b) 12 | def makeNull(): JsValue = JsNull 13 | def makeSpliceValue(): JsValue = SpliceValue() 14 | def makeSpliceValues(): JsValue = SpliceValues() 15 | 16 | def makeField(k: String, v: JsValue): (String, JsValue) = (k, v) 17 | def makeSpliceField(): (String, JsValue) = SpliceField() 18 | def makeSpliceFields(): (String, JsValue) = SpliceFields() 19 | def makeSpliceFieldNameOpt: (String, JsValue) = SpliceFieldNameOpt() 20 | def makeSpliceFieldName(v: JsValue): (String, JsValue) = SpliceFieldName(v) 21 | def makeSpliceFieldOpt(k: String): (String, JsValue) = SpliceFieldOpt(k) 22 | } 23 | -------------------------------------------------------------------------------- /play/src/main/scala/net/maffoo/jsonquote/play/Splice.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.play 2 | 3 | import _root_.play.api.libs.json._ 4 | import scala.util.Random 5 | 6 | // Sentinel values to mark splice points in the json AST. 7 | // Because play uses Map to represent json objects, we use unique 8 | // random keys beginning with a known prefix to mark field splice points. 9 | 10 | class Sentinel[A <: AnyRef](inst: A) { 11 | def apply(): A = inst 12 | def unapply(a: A): Boolean = a eq inst 13 | } 14 | 15 | object SpliceValue extends Sentinel[JsValue](JsObject(Map("__splice_value__" -> JsNull))) 16 | object SpliceValues extends Sentinel[JsValue](JsObject(Map("__splice_values__" -> JsNull))) 17 | 18 | object SpliceField { 19 | def apply(): (String, JsValue) = ("__splice_field__%016X".format(Random.nextLong), JsNull) 20 | def unapply(a: (String, JsValue)): Boolean = a match { 21 | case (f, JsNull) if f.startsWith("__splice_field__") => true 22 | case _ => false 23 | } 24 | } 25 | 26 | object SpliceFields { 27 | def apply(): (String, JsValue) = ("__splice_fields__%016X".format(Random.nextLong), JsNull) 28 | def unapply(a: (String, JsValue)): Boolean = a match { 29 | case (f, JsNull) if f.startsWith("__splice_fields__") => true 30 | case _ => false 31 | } 32 | } 33 | 34 | object SpliceFieldNameOpt { 35 | def apply(): (String, JsValue) = ("__splice_field_name_opt__%016X".format(Random.nextLong), JsNull) 36 | def unapply(f: (String, JsValue)): Boolean = f match { 37 | case (f, JsNull) if f.startsWith("__splice_field_name_opt__") => true 38 | case _ => false 39 | } 40 | } 41 | 42 | object SpliceFieldName { 43 | def apply(x: JsValue) = ("__splice_field_name__%016X".format(Random.nextLong), x) 44 | def unapply(f: (String, JsValue)): Option[JsValue] = f match { 45 | case (f, x) if f.startsWith("__splice_field_name__") => Some(x) 46 | case _ => None 47 | } 48 | } 49 | 50 | object SpliceFieldOpt { 51 | def apply(k: String) = (k, null) 52 | def unapply(f: (String, JsValue)): Option[String] = f match { 53 | case (k, null) => Some(k) 54 | case _ => None 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /play/src/main/scala/net/maffoo/jsonquote/play/package.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | import net.maffoo.jsonquote.Macros._ 4 | import _root_.play.api.libs.json._ 5 | import scala.language.experimental.macros 6 | 7 | package object play { 8 | /** 9 | * Rendering JsValue produces valid json literal 10 | */ 11 | implicit class PlayToLiteralJson(val json: JsValue) extends AnyVal { 12 | def toLiteral: literal.Json = new literal.Json(Json.stringify(json)) 13 | } 14 | 15 | implicit class RichJsonSringContext(val sc: StringContext) extends AnyVal { 16 | def json(args: Any*): JsValue = macro jsonImpl 17 | } 18 | 19 | def jsonImpl(c: Context)(args: c.Expr[Any]*): c.Expr[JsValue] = { 20 | import c.universe._ 21 | 22 | // fully-qualified symbols and types (for hygiene) 23 | val bigDecimal = q"_root_.scala.math.BigDecimal" 24 | val nil = q"_root_.scala.collection.immutable.Nil" 25 | val seq = q"_root_.scala.Seq" 26 | val indexedSeq = q"_root_.scala.IndexedSeq" 27 | val jsArray = q"_root_.play.api.libs.json.JsArray" 28 | val jsObject = q"_root_.play.api.libs.json.JsObject" 29 | val writesT = tq"_root_.play.api.libs.json.Writes" 30 | 31 | // convert the given json AST to a tree with arguments spliced in at the correct locations 32 | def splice(js: JsValue)(implicit args: Iterator[Tree]): Tree = js match { 33 | case SpliceValue() => spliceValue(args.next) 34 | case SpliceValues() => c.abort(c.enclosingPosition, "cannot splice values at top level") 35 | 36 | case JsObject(members) => 37 | val seqMembers = members.collect { 38 | case SpliceFields() => 39 | case SpliceFieldNameOpt() => 40 | case SpliceFieldOpt(k) => 41 | } 42 | if (seqMembers.isEmpty) { 43 | val ms = members.map { 44 | case SpliceField() => spliceField(args.next) 45 | case SpliceFieldName(v) => q"(${spliceFieldName(args.next)}, ${splice(v)})" 46 | case (k, v) => q"($k, ${splice(v)})" 47 | } 48 | q"$jsObject($indexedSeq(..$ms))" 49 | } else { 50 | val ms = members.map { 51 | case SpliceField() => q"$seq(${spliceField(args.next)})" 52 | case SpliceFields() => spliceFields(args.next) 53 | case SpliceFieldNameOpt() => spliceFieldNameOpt(args.next, args.next) 54 | case SpliceFieldName(v) => q"$seq((${spliceFieldName(args.next)}, ${splice(v)}))" 55 | case SpliceFieldOpt(k) => spliceFieldOpt(k, args.next) 56 | case (k, v) => q"$seq(($k, ${splice(v)}))" 57 | } 58 | q"$jsObject($indexedSeq(..$ms).flatten)" 59 | } 60 | 61 | case JsArray(elements) => 62 | val seqElems = elements.collect { 63 | case SpliceValues() => 64 | } 65 | if (seqElems.isEmpty) { 66 | val es = elements.map { 67 | case SpliceValue() => spliceValue(args.next) 68 | case e => splice(e) 69 | } 70 | q"$jsArray($indexedSeq(..$es))" 71 | } else { 72 | val es = elements.map { 73 | case SpliceValue() => q"$seq(${spliceValue(args.next)})" 74 | case SpliceValues() => spliceValues(args.next) 75 | case e => q"$seq(${splice(e)})" 76 | } 77 | q"$jsArray($indexedSeq(..$es).flatten)" 78 | } 79 | 80 | case JsString(s) => q"_root_.play.api.libs.json.JsString($s)" 81 | case JsNumber(n) => q"_root_.play.api.libs.json.JsNumber($bigDecimal(${n.toString}))" 82 | case JsBoolean(true) => q"_root_.play.api.libs.json.JsBoolean(true)" 83 | case JsBoolean(false) => q"_root_.play.api.libs.json.JsBoolean(false)" 84 | case JsNull => q"_root_.play.api.libs.json.JsNull" 85 | } 86 | 87 | def spliceValue(e: Tree): Tree = e.tpe match { 88 | case t if t <:< c.typeOf[JsValue] => e 89 | case t => 90 | inferWriter(e, t) 91 | q"implicitly[$writesT[$t]].writes($e)" 92 | } 93 | 94 | def spliceValues(e: Tree): Tree = e.tpe match { 95 | case t if t <:< c.typeOf[Iterable[JsValue]] => e 96 | case t if t <:< c.typeOf[Iterable[Any]] => 97 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[Nothing]] :: Nil))(0) 98 | val writer = inferWriter(e, valueTpe) 99 | q"$e.map($writer.writes)" 100 | 101 | case t if t <:< c.typeOf[None.type] => nil 102 | case t if t <:< c.typeOf[Option[JsValue]] => e 103 | case t if t <:< c.typeOf[Option[Any]] => 104 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 105 | val writer = inferWriter(e, valueTpe) 106 | q"$e.toIterable.map($writer.writes)" 107 | 108 | case t => c.abort(e.pos, s"required Iterable[_] but got $t") 109 | } 110 | 111 | def spliceField(e: Tree): Tree = e.tpe match { 112 | case t if t <:< c.typeOf[(String, JsValue)] => e 113 | case t if t <:< c.typeOf[(String, Any)] => 114 | val valueTpe = typeParams(lub(t :: c.typeOf[(String, Nothing)] :: Nil))(1) 115 | val writer = inferWriter(e, valueTpe) 116 | q"val (k, v) = $e; (k, $writer.writes(v))" 117 | 118 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 119 | } 120 | 121 | def spliceFields(e: Tree): Tree = e.tpe match { 122 | case t if t <:< c.typeOf[Iterable[(String, JsValue)]] => e 123 | case t if t <:< c.typeOf[Iterable[(String, Any)]] => 124 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[(String, Nothing)]] :: Nil))(2) 125 | val writer = inferWriter(e, valueTpe) 126 | q"$e.map { case (k, v) => (k, $writer.writes(v)) }" 127 | 128 | case t if t <:< c.typeOf[None.type] => nil 129 | case t if t <:< c.typeOf[Option[(String, JsValue)]] => e 130 | case t if t <:< c.typeOf[Option[(String, Any)]] => 131 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[(String, Nothing)]] :: Nil))(2) 132 | val writer = inferWriter(e, valueTpe) 133 | q"$e.toIterable.map { case (k, v) => (k, $writer.writes(v)) }" 134 | 135 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 136 | } 137 | 138 | def spliceFieldOpt(k: String, e: Tree): Tree = e.tpe match { 139 | case t if t <:< c.typeOf[None.type] => nil 140 | case t if t <:< c.typeOf[Option[JsValue]] => q"$e.toIterable.map(v => ($k, v))" 141 | case t if t <:< c.typeOf[Option[Any]] => 142 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 143 | val writer = inferWriter(e, valueTpe) 144 | q"$e.toIterable.map { v => ($k, $writer.writes(v)) }" 145 | 146 | case t => c.abort(e.pos, s"required Option[_] but got $t") 147 | } 148 | 149 | def spliceFieldName(e: Tree): Tree = e.tpe match { 150 | case t if t =:= c.typeOf[String] => e 151 | case t => c.abort(e.pos, s"required String but got $t") 152 | } 153 | 154 | def spliceFieldNameOpt(k: Tree, v: Tree): Tree = { 155 | k.tpe match { 156 | case t if t =:= c.typeOf[String] => 157 | case t => c.abort(k.pos, s"required String but got $t") 158 | } 159 | v.tpe match { 160 | case t if t <:< c.typeOf[None.type] => nil 161 | case t if t <:< c.typeOf[Option[JsValue]] => q"$v.toIterable.map(v => ($k, v))" 162 | case t if t <:< c.typeOf[Option[Any]] => 163 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 164 | val writer = inferWriter(v, valueTpe) 165 | q"$v.toIterable.map { v => ($k, $writer.writes(v)) }" 166 | 167 | case t => c.abort(v.pos, s"required Option[_] but got $t") 168 | } 169 | } 170 | 171 | // return a list of type parameters in the given type 172 | // example: List[(String, Int)] => Seq(Tuple2, String, Int) 173 | def typeParams(tpe: Type): Seq[Type] = { 174 | val b = Iterable.newBuilder[Type] 175 | tpe.foreach(b += _) 176 | b.result.drop(2).grouped(2).map(_.head).toIndexedSeq 177 | } 178 | 179 | // locate an implicit Writes[T] for the given type 180 | def inferWriter(e: Tree, t: Type): Tree = { 181 | val writerTpe = appliedType(c.typeOf[Writes[_]], List(t)) 182 | c.inferImplicitValue(writerTpe) match { 183 | case EmptyTree => c.abort(e.pos, s"could not find implicit value of type Writes[$t]") 184 | case tree => tree 185 | } 186 | } 187 | 188 | // Parse the string context parts into a json AST with holes, and then 189 | // typecheck/convert args to the appropriate types and splice them in. 190 | c.prefix.tree match { 191 | case Apply(_, List(Apply(_, partTrees))) => 192 | val parts = partTrees map { case Literal(Constant(const: String)) => const } 193 | val positions = (parts zip partTrees.map(_.pos)).toMap 194 | val js = try { 195 | Parse(parts) 196 | } catch { 197 | case JsonError(msg, Pos(s, ofs)) => 198 | val pos = positions(s) 199 | c.abort(pos.withPoint(pos.point + ofs), msg) 200 | } 201 | c.Expr[JsValue](splice(js)(args.iterator.map(_.tree))) 202 | 203 | case _ => 204 | c.abort(c.enclosingPosition, "invalid") 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /play/src/test/scala/net/maffoo/jsonquote/play/Test.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.play 2 | 3 | import _root_.play.api.libs.json._ 4 | import org.scalatest.{FunSuite, Matchers} 5 | 6 | case class Foo(bar: String, baz: String) 7 | 8 | class PlayTest extends FunSuite with Matchers { 9 | 10 | implicit val fooFormat = Json.writes[Foo] 11 | 12 | def check(js: JsValue, s: String): Unit = { js should equal (Json.parse(s)) } 13 | 14 | //case class Baz(s: String) 15 | //json"""{test: ${Baz("test")}}""" 16 | 17 | test("can parse plain json") { 18 | check(json""" "hello!" """, """"hello!"""") 19 | } 20 | 21 | test("can use bare identifiers for object keys") { 22 | check(json"{ test0: 0 }", """{ "test0": 0 }""") 23 | check(json"{ test: 1 }", """{ "test": 1 }""") 24 | check(json"{ test-2: 2 }", """{ "test-2": 2 }""") 25 | check(json"{ test_3: 3 }", """{ "test_3": 3 }""") 26 | check(json"{ _test-4: 4 }", """{ "_test-4": 4 }""") 27 | } 28 | 29 | test("can inject value with implicit Writes") { 30 | val foo = Foo(bar = "a", baz = "b") 31 | check(json"$foo", """{"bar": "a", "baz": "b"}""") 32 | } 33 | 34 | test("can inject values with implicit Writes") { 35 | val foos = List(Foo("1", "2"), Foo("3", "4")) 36 | check(json"[..$foos]", """[{"bar":"1", "baz":"2"}, {"bar":"3", "baz":"4"}]""") 37 | check(json"[..$Nil]", """[]""") 38 | } 39 | 40 | test("can inject Option values") { 41 | val vOpt = Some(JsNumber(1)) 42 | check(json"[..$vOpt]", """[1]""") 43 | check(json"[..$None]" , """[]""") 44 | } 45 | 46 | test("can inject Option values with implicit Writes") { 47 | val vOpt = Some(1) 48 | check(json"[..$vOpt]", """[1]""") 49 | } 50 | 51 | test("can mix values, Iterables and Options in array") { 52 | val a = List("a") 53 | val b = Some("b") 54 | val c = Nil 55 | val d = None 56 | check(json"""[1, ..$a, 2, ..$b, 3, ..$c, 4, ..$d, 5]""", """[1, "a", 2, "b", 3, 4, 5]""") 57 | } 58 | 59 | test("can inject Tuple2 as object field") { 60 | val kv = "test" -> Foo("1", "2") 61 | check(json"{$kv}", """{"test": {"bar":"1", "baz":"2"}}""") 62 | } 63 | 64 | test("can inject multiple fields") { 65 | val kvs = Seq("a" -> 1, "b" -> 2) 66 | check(json"{..$kvs}", """{"a": 1, "b": 2}""") 67 | check(json"{..$Nil}", """{}""") 68 | } 69 | 70 | test("can inject just a field name") { 71 | val key = "foo" 72 | check(json"{$key: 1}", """{"foo": 1}""") 73 | } 74 | 75 | test("can inject Option fields") { 76 | val kvOpt = Some("a" -> JsNumber(1)) 77 | check(json"{..$kvOpt}", """{"a": 1}""") 78 | check(json"{..$None}", """{}""") 79 | } 80 | 81 | test("can inject Option fields with implicit Writes") { 82 | val kvOpt = Some("a" -> Foo("1", "2")) 83 | check(json"{..$kvOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 84 | } 85 | 86 | test("can inject Option field values") { 87 | val vOpt = Some(JsNumber(1)) 88 | check(json"{a:? $vOpt}", """{"a": 1}""") 89 | check(json"{a:? $None}", """{}""") 90 | 91 | val k = "a" 92 | check(json"{$k:? $vOpt}", """{"a": 1}""") 93 | check(json"{$k:? $None}", """{}""") 94 | } 95 | 96 | test("can inject Option field values with implicit Writes") { 97 | val vOpt = Some(Foo("1", "2")) 98 | check(json"{a:? $vOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 99 | } 100 | 101 | test("can mix values, Iterables and Options in object") { 102 | val a = List("a" -> 10) 103 | val b = Some("b" -> 20) 104 | val c = Nil 105 | val d = None 106 | check( 107 | json"""{i:1, ..$a, j:2, ..$b, k:3, ..$c, l:4, ..$d, m:5}""", 108 | """{"i":1, "a":10, "j":2, "b":20, "k":3, "l":4, "m":5}""" 109 | ) 110 | } 111 | 112 | test("can nest jsonquote templates") { 113 | 114 | // example taken from the play docs: http://www.playframework.com/documentation/2.1.x/ScalaJson 115 | import play.api.libs.json.Json.toJson 116 | 117 | val jsonObject = toJson( 118 | Map( 119 | "users" -> Seq( 120 | toJson( 121 | Map( 122 | "name" -> toJson("Bob"), 123 | "age" -> toJson(31), 124 | "email" -> toJson("bob@gmail.com") 125 | ) 126 | ), 127 | toJson( 128 | Map( 129 | "name" -> toJson("Kiki"), 130 | "age" -> toJson(25), 131 | "email" -> JsNull 132 | ) 133 | ) 134 | ) 135 | ) 136 | ) 137 | 138 | val users = Seq(("Bob", 31, Some("bob@gmail.com")), ("Kiki", 25, None)) 139 | 140 | // TODO: find a way to avoid the need for .toSeq here 141 | val quoteA = json"""{ 142 | users: [..${ 143 | users.map { case (name, age, email) => 144 | json"""{ 145 | name: $name, 146 | age: $age, 147 | email: $email 148 | }""" 149 | }.toSeq 150 | }] 151 | }""" 152 | 153 | // play already knows how to convert Seq[JsValue] to json array 154 | // still need the .toSeq here 155 | val quoteB = json"""{ 156 | users: ${ 157 | users.map { case (name, age, email) => 158 | json"""{ 159 | name: $name, 160 | age: $age, 161 | email: $email 162 | }""" 163 | }.toSeq 164 | } 165 | }""" 166 | 167 | // types inferred properly here 168 | val mapped = users.map { case (name, age, email) => 169 | json"""{ 170 | name: $name, 171 | age: $age, 172 | email: $email 173 | }""" 174 | } 175 | val quoteC = json"""{ 176 | users: [..$mapped] 177 | }""" 178 | 179 | quoteA should equal (jsonObject) 180 | quoteB should equal (jsonObject) 181 | quoteC should equal (jsonObject) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /spray/src/main/scala/net/maffoo/jsonquote/spray/Parse.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.spray 2 | 3 | import net.maffoo.jsonquote.Parser 4 | import scala.collection.immutable.ListMap 5 | import _root_.spray.json._ 6 | 7 | object Parse extends Parser[JsValue, (String, JsValue)] { 8 | def makeObject(fields: Iterable[(String, JsValue)]): JsValue = JsObject(ListMap(fields.toSeq: _*)) // preserve iteration order until splicing is done 9 | def makeArray(elements: Iterable[JsValue]): JsValue = JsArray(elements.toSeq: _*) 10 | def makeNumber(n: BigDecimal): JsValue = JsNumber(n) 11 | def makeString(s: String): JsValue = JsString(s) 12 | def makeBoolean(b: Boolean): JsValue = JsBoolean(b) 13 | def makeNull(): JsValue = JsNull 14 | def makeSpliceValue(): JsValue = SpliceValue() 15 | def makeSpliceValues(): JsValue = SpliceValues() 16 | 17 | def makeField(k: String, v: JsValue): (String, JsValue) = (k, v) 18 | def makeSpliceField(): (String, JsValue) = SpliceField() 19 | def makeSpliceFields(): (String, JsValue) = SpliceFields() 20 | def makeSpliceFieldNameOpt(): (String, JsValue) = SpliceFieldNameOpt() 21 | def makeSpliceFieldName(v: JsValue): (String, JsValue) = SpliceFieldName(v) 22 | def makeSpliceFieldOpt(k: String): (String, JsValue) = SpliceFieldOpt(k) 23 | } 24 | -------------------------------------------------------------------------------- /spray/src/main/scala/net/maffoo/jsonquote/spray/Splice.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.spray 2 | 3 | import scala.util.Random 4 | import spray.json._ 5 | 6 | // Sentinel values to mark splice points in the json AST. 7 | // Because spray uses Map to represent json objects, we use unique 8 | // random keys beginning with a known prefix to mark field splice points. 9 | 10 | class Sentinel[A <: AnyRef](inst: A) { 11 | def apply() = inst 12 | def unapply(a: A): Boolean = a == inst 13 | } 14 | 15 | object SpliceValue extends Sentinel[JsValue](JsObject(Map("__splice_value__" -> JsNull))) 16 | object SpliceValues extends Sentinel[JsValue](JsObject(Map("__splice_values__" -> JsNull))) 17 | 18 | object SpliceField { 19 | def apply(): (String, JsValue) = ("__splice_field__%016X".format(Random.nextLong), JsNull) 20 | def unapply(a: (String, JsValue)): Boolean = a match { 21 | case (f, JsNull) if f.startsWith("__splice_field__") => true 22 | case _ => false 23 | } 24 | } 25 | 26 | object SpliceFields { 27 | def apply(): (String, JsValue) = ("__splice_fields__%016X".format(Random.nextLong), JsNull) 28 | def unapply(a: (String, JsValue)): Boolean = a match { 29 | case (f, JsNull) if f.startsWith("__splice_fields__") => true 30 | case _ => false 31 | } 32 | } 33 | 34 | object SpliceFieldNameOpt { 35 | def apply(): (String, JsValue) = ("__splice_field_name_opt__%016X".format(Random.nextLong), JsNull) 36 | def unapply(f: (String, JsValue)): Boolean = f match { 37 | case (f, JsNull) if f.startsWith("__splice_field_name_opt__") => true 38 | case _ => false 39 | } 40 | } 41 | 42 | object SpliceFieldName { 43 | def apply(x: JsValue) = ("__splice_field_name__%016X".format(Random.nextLong), x) 44 | def unapply(f: (String, JsValue)): Option[JsValue] = f match { 45 | case (f, x) if f.startsWith("__splice_field_name__") => Some(x) 46 | case _ => None 47 | } 48 | } 49 | 50 | object SpliceFieldOpt { 51 | def apply(k: String) = (k, null) 52 | def unapply(f: (String, JsValue)): Option[String] = f match { 53 | case (k, null) => Some(k) 54 | case _ => None 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /spray/src/main/scala/net/maffoo/jsonquote/spray/package.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | import net.maffoo.jsonquote.Macros._ 4 | import _root_.spray.json._ 5 | import scala.language.experimental.macros 6 | 7 | package object spray { 8 | /** 9 | * Rendering JsValue produces valid json literal 10 | */ 11 | implicit class SprayToLiteralJson(val json: JsValue) extends AnyVal { 12 | def toLiteral: literal.Json = new literal.Json(CompactPrinter(json)) 13 | } 14 | 15 | implicit class RichJsonSringContext(val sc: StringContext) extends AnyVal { 16 | def json(args: Any*): JsValue = macro jsonImpl 17 | } 18 | 19 | def jsonImpl(c: Context)(args: c.Expr[Any]*): c.Expr[JsValue] = { 20 | import c.universe._ 21 | 22 | // fully-qualified symbols and types (for hygiene) 23 | val bigDecimal = q"_root_.scala.math.BigDecimal" 24 | val nil = q"_root_.scala.collection.immutable.Nil" 25 | val seq = q"_root_.scala.Seq" 26 | val indexedSeq = q"_root_.scala.IndexedSeq" 27 | val jsArray = q"_root_.spray.json.JsArray" 28 | val jsObject = q"_root_.spray.json.JsObject" 29 | val writerT = tq"_root_.spray.json.JsonWriter" 30 | 31 | // convert the given json AST to a tree with arguments spliced in at the correct locations 32 | def splice(js: JsValue)(implicit args: Iterator[Tree]): Tree = js match { 33 | case SpliceValue() => spliceValue(args.next) 34 | case SpliceValues() => c.abort(c.enclosingPosition, "cannot splice values at top level") 35 | 36 | case JsObject(members) => 37 | val seqMembers = members.collect { 38 | case SpliceFields() => 39 | case SpliceFieldNameOpt() => 40 | case SpliceFieldOpt(k) => 41 | } 42 | if (seqMembers.isEmpty) { 43 | val ms = members.toSeq.map { 44 | case SpliceField() => spliceField(args.next) 45 | case SpliceFieldName(v) => q"(${spliceFieldName(args.next)}, ${splice(v)})" 46 | case (k, v) => q"($k, ${splice(v)})" 47 | } 48 | q"$jsObject(..$ms)" 49 | } else { 50 | val ms = members.toSeq.map { 51 | case SpliceField() => q"$seq(${spliceField(args.next)})" 52 | case SpliceFields() => spliceFields(args.next) 53 | case SpliceFieldNameOpt() => spliceFieldNameOpt(args.next, args.next) 54 | case SpliceFieldName(v) => q"$seq((${spliceFieldName(args.next)}, ${splice(v)}))" 55 | case SpliceFieldOpt(k) => spliceFieldOpt(k, args.next) 56 | case (k, v) => q"$seq(($k, ${splice(v)}))" 57 | } 58 | q"$jsObject($indexedSeq(..$ms).flatten: _*)" 59 | } 60 | 61 | case JsArray(elements) => 62 | val seqElems = elements.collect { 63 | case SpliceValues() => 64 | } 65 | if (seqElems.isEmpty) { 66 | val es = elements.map { 67 | case SpliceValue() => spliceValue(args.next) 68 | case e => splice(e) 69 | } 70 | q"$jsArray(..$es)" 71 | } else { 72 | val es = elements.map { 73 | case SpliceValue() => q"$seq(${spliceValue(args.next)})" 74 | case SpliceValues() => spliceValues(args.next) 75 | case e => q"$seq(${splice(e)})" 76 | } 77 | q"$jsArray($indexedSeq(..$es).flatten: _*)" 78 | } 79 | 80 | case JsString(s) => q"_root_.spray.json.JsString($s)" 81 | case JsNumber(n) => q"_root_.spray.json.JsNumber($bigDecimal(${n.toString}))" 82 | case JsBoolean(true) => q"_root_.spray.json.JsBoolean(true)" 83 | case JsBoolean(false) => q"_root_.spray.json.JsBoolean(false)" 84 | case JsNull => q"_root_.spray.json.JsNull" 85 | } 86 | 87 | def spliceValue(e: Tree): Tree = e.tpe match { 88 | case t if t <:< c.typeOf[JsValue] => e 89 | case t => 90 | inferWriter(e, t) 91 | q"implicitly[$writerT[$t]].write($e)" 92 | } 93 | 94 | def spliceValues(e: Tree): Tree = e.tpe match { 95 | case t if t <:< c.typeOf[Iterable[JsValue]] => e 96 | case t if t <:< c.typeOf[Iterable[Any]] => 97 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[Nothing]] :: Nil))(0) 98 | val writer = inferWriter(e, valueTpe) 99 | q"$e.map($writer.write)" 100 | 101 | case t if t <:< c.typeOf[None.type] => nil 102 | case t if t <:< c.typeOf[Option[JsValue]] => e 103 | case t if t <:< c.typeOf[Option[Any]] => 104 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 105 | val writer = inferWriter(e, valueTpe) 106 | q"$e.toIterable.map($writer.write)" 107 | 108 | case t => c.abort(e.pos, s"required Iterable[_] but got $t") 109 | } 110 | 111 | def spliceField(e: Tree): Tree = e.tpe match { 112 | case t if t <:< c.typeOf[(String, JsValue)] => e 113 | case t if t <:< c.typeOf[(String, Any)] => 114 | val valueTpe = typeParams(lub(t :: c.typeOf[(String, Nothing)] :: Nil))(1) 115 | val writer = inferWriter(e, valueTpe) 116 | q"val (k, v) = $e; (k, $writer.write(v))" 117 | 118 | case t => c.abort(e.pos, s"required (String, _) but got $t") 119 | } 120 | 121 | def spliceFields(e: Tree): Tree = e.tpe match { 122 | case t if t <:< c.typeOf[Iterable[(String, JsValue)]] => e 123 | case t if t <:< c.typeOf[Iterable[(String, Any)]] => 124 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[(String, Nothing)]] :: Nil))(2) 125 | val writer = inferWriter(e, valueTpe) 126 | q"$e.map { case (k, v) => (k, $writer.write(v)) }" 127 | 128 | case t if t <:< c.typeOf[None.type] => nil 129 | case t if t <:< c.typeOf[Option[(String, JsValue)]] => e 130 | case t if t <:< c.typeOf[Option[(String, Any)]] => 131 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[(String, Nothing)]] :: Nil))(2) 132 | val writer = inferWriter(e, valueTpe) 133 | q"$e.toIterable.map { case (k, v) => (k, $writer.write(v)) }" 134 | 135 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 136 | } 137 | 138 | def spliceFieldOpt(k: String, e: Tree): Tree = e.tpe match { 139 | case t if t <:< c.typeOf[None.type] => nil 140 | case t if t <:< c.typeOf[Option[JsValue]] => q"$e.toIterable.map(v => ($k, v))" 141 | case t if t <:< c.typeOf[Option[Any]] => 142 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 143 | val writer = inferWriter(e, valueTpe) 144 | q"$e.toIterable.map { v => ($k, $writer.write(v)) }" 145 | 146 | case t => c.abort(e.pos, s"required Option[_] but got $t") 147 | } 148 | 149 | def spliceFieldName(e: Tree): Tree = e.tpe match { 150 | case t if t =:= c.typeOf[String] => e 151 | case t => c.abort(e.pos, s"required String but got $t") 152 | } 153 | 154 | def spliceFieldNameOpt(k: Tree, v: Tree): Tree = { 155 | k.tpe match { 156 | case t if t =:= c.typeOf[String] => 157 | case t => c.abort(k.pos, s"required String but got $t") 158 | } 159 | v.tpe match { 160 | case t if t <:< c.typeOf[None.type] => nil 161 | case t if t <:< c.typeOf[Option[JsValue]] => q"$v.toIterable.map(v => ($k, v))" 162 | case t if t <:< c.typeOf[Option[Any]] => 163 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 164 | val writer = inferWriter(v, valueTpe) 165 | q"$v.toIterable.map { v => ($k, $writer.write(v)) }" 166 | 167 | case t => c.abort(v.pos, s"required Option[_] but got $t") 168 | } 169 | } 170 | 171 | // return a list of type parameters in the given type 172 | // example: List[(String, Int)] => Seq(Tuple2, String, Int) 173 | def typeParams(tpe: Type): Seq[Type] = { 174 | val b = Iterable.newBuilder[Type] 175 | tpe.foreach(b += _) 176 | b.result.drop(2).grouped(2).map(_.head).toIndexedSeq 177 | } 178 | 179 | // locate an implicit Writes[T] for the given type 180 | def inferWriter(e: Tree, t: Type): Tree = { 181 | val writerTpe = appliedType(c.typeOf[JsonWriter[_]], List(t)) 182 | c.inferImplicitValue(writerTpe) match { 183 | case EmptyTree => c.abort(e.pos, s"could not find implicit value of type JsonWriter[$t]") 184 | case tree => tree 185 | } 186 | } 187 | 188 | // Parse the string context parts into a json AST with holes, and then 189 | // typecheck/convert args to the appropriate types and splice them in. 190 | c.prefix.tree match { 191 | case Apply(_, List(Apply(_, partTrees))) => 192 | val parts = partTrees map { case Literal(Constant(const: String)) => const } 193 | val positions = (parts zip partTrees.map(_.pos)).toMap 194 | val js = try { 195 | Parse(parts) 196 | } catch { 197 | case JsonError(msg, Pos(s, ofs)) => 198 | val pos = positions(s) 199 | c.abort(pos.withPoint(pos.point + ofs), msg) 200 | } 201 | c.Expr[JsValue](splice(js)(args.iterator.map(_.tree))) 202 | 203 | case _ => 204 | c.abort(c.enclosingPosition, "invalid") 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /spray/src/test/scala/net/maffoo/jsonquote/spray/Test.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.spray 2 | 3 | import _root_.spray.json._ 4 | import _root_.spray.json.DefaultJsonProtocol._ 5 | import org.scalatest.{FunSuite, Matchers} 6 | 7 | case class Foo(bar: String, baz: String) 8 | 9 | class SprayTest extends FunSuite with Matchers { 10 | 11 | implicit object FooFormat extends JsonWriter[Foo] { 12 | def write(x: Foo) = JsObject(Map("bar" -> JsString(x.bar), "baz" -> JsString(x.baz))) 13 | } 14 | 15 | def check(js: JsValue, s: String): Unit = { js should equal (s.parseJson) } 16 | 17 | //case class Baz(s: String) 18 | //json"""{test: ${Baz("test")}}""" 19 | 20 | test("can parse plain json") { 21 | check(json""" "hello!" """, """"hello!"""") 22 | } 23 | 24 | test("can use bare identifiers for object keys") { 25 | check(json"{ test0: 0 }", """{ "test0": 0 }""") 26 | check(json"{ test: 1 }", """{ "test": 1 }""") 27 | check(json"{ test-2: 2 }", """{ "test-2": 2 }""") 28 | check(json"{ test_3: 3 }", """{ "test_3": 3 }""") 29 | check(json"{ _test-4: 4 }", """{ "_test-4": 4 }""") 30 | } 31 | 32 | test("can inject value with implicit JsonWriter") { 33 | val foo = Foo(bar = "a", baz = "b") 34 | check(json"$foo", """{"bar": "a", "baz": "b"}""") 35 | } 36 | 37 | test("can inject values with implicit JsonWriter") { 38 | val foos = List(Foo("1", "2"), Foo("3", "4")) 39 | check(json"[..$foos]", """[{"bar":"1", "baz":"2"}, {"bar":"3", "baz":"4"}]""") 40 | check(json"[..$Nil]", """[]""") 41 | } 42 | 43 | test("can inject Option values") { 44 | val vOpt = Some(JsNumber(1)) 45 | check(json"[..$vOpt]", """[1]""") 46 | check(json"[..$None]" , """[]""") 47 | } 48 | 49 | test("can inject Option values with implicit JsonWriter") { 50 | val vOpt = Some(1) 51 | check(json"[..$vOpt]", """[1]""") 52 | } 53 | 54 | test("can mix values, Iterables and Options in array") { 55 | val a = List("a") 56 | val b = Some("b") 57 | val c = Nil 58 | val d = None 59 | check(json"""[1, ..$a, 2, ..$b, 3, ..$c, 4, ..$d, 5]""", """[1, "a", 2, "b", 3, 4, 5]""") 60 | } 61 | 62 | test("can inject Tuple2 as object field") { 63 | val kv = "test" -> Foo("1", "2") 64 | check(json"{$kv}", """{"test": {"bar":"1", "baz":"2"}}""") 65 | } 66 | 67 | test("can inject multiple fields") { 68 | val kvs = Seq("a" -> 1, "b" -> 2) 69 | check(json"{..$kvs}", """{"a": 1, "b": 2}""") 70 | check(json"{..$Nil}", """{}""") 71 | } 72 | 73 | test("can inject just a field name") { 74 | val key = "foo" 75 | check(json"{$key: 1}", """{"foo": 1}""") 76 | } 77 | 78 | test("can inject Option fields") { 79 | val kvOpt = Some("a" -> JsNumber(1)) 80 | check(json"{..$kvOpt}", """{"a": 1}""") 81 | check(json"{..$None}", """{}""") 82 | } 83 | 84 | test("can inject Option fields with implicit JsonWriter") { 85 | val kvOpt = Some("a" -> Foo("1", "2")) 86 | check(json"{..$kvOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 87 | } 88 | 89 | test("can inject Option field values") { 90 | val vOpt = Some(JsNumber(1)) 91 | check(json"{a:? $vOpt}", """{"a": 1}""") 92 | check(json"{a:? $None}", """{}""") 93 | 94 | val k = "a" 95 | check(json"{$k:? $vOpt}", """{"a": 1}""") 96 | check(json"{$k:? $None}", """{}""") 97 | } 98 | 99 | test("can inject Option field values with implicit Writes") { 100 | val vOpt = Some(Foo("1", "2")) 101 | check(json"{a:? $vOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 102 | } 103 | 104 | test("can inject object entries without reordering fields") { 105 | // With spray json's default ASTs, these may fail to compile, 106 | // because iteration order is not preserved so the seq gets 107 | // interpolated into the wrong slot. 108 | val x = 1 109 | val xs = Seq(1, 2) 110 | json"{a: [..$xs], b: $x, c: $x, d: $x, e: $x}" 111 | json"{a: [..$xs], b: $x, c: $x, d: $x, e: $x, f: $x}" 112 | json"{a: [..$xs], b: $x, c: $x, d: $x, e: $x, f: $x, g: $x}" 113 | json"{a: [..$xs], b: $x, c: $x, d: $x, e: $x, f: $x, g: $x, h: $x}" 114 | json"{a: [..$xs], b: $x, c: $x, d: $x, e: $x, f: $x, g: $x, h: $x, i: $x}" 115 | } 116 | 117 | test("can mix values, Iterables and Options in object") { 118 | val a = List("a" -> 10) 119 | val b = Some("b" -> 20) 120 | val c = Nil 121 | val d = None 122 | check( 123 | json"""{i1:1, ..$a, i2:2, ..$b, i3:3, ..$c, i4:4, ..$d, i5:5}""", 124 | """{"i1":1, "a":10, "i2":2, "b":20, "i3":3, "i4":4, "i5":5}""" 125 | ) 126 | } 127 | 128 | test("can nest jsonquote templates") { 129 | 130 | // adapted from the play docs: http://www.playframework.com/documentation/2.1.x/ScalaJson 131 | 132 | val jsonObject = JsObject( 133 | Map( 134 | "users" -> JsArray( 135 | JsObject( 136 | Map( 137 | "name" -> JsString("Bob"), 138 | "age" -> JsNumber(31), 139 | "email" -> JsString("bob@gmail.com") 140 | ) 141 | ), 142 | JsObject( 143 | Map( 144 | "name" -> JsString("Kiki"), 145 | "age" -> JsNumber(25), 146 | "email" -> JsNull 147 | ) 148 | ) 149 | ) 150 | ) 151 | ) 152 | 153 | val users = Seq(("Bob", 31, Some("bob@gmail.com")), ("Kiki", 25, None)) 154 | 155 | // TODO: find a way to avoid the need for .toSeq here 156 | val quoteA = json"""{ 157 | users: [..${ 158 | users.map { case (name, age, email) => 159 | json"""{ 160 | name: $name, 161 | age: $age, 162 | email: $email 163 | }""" 164 | }.toSeq 165 | }] 166 | }""" 167 | 168 | // spray already knows how to convert Seq[JsValue] to json array 169 | // still need the .toSeq here 170 | val quoteB = json"""{ 171 | users: ${ 172 | users.map { case (name, age, email) => 173 | json"""{ 174 | name: $name, 175 | age: $age, 176 | email: $email 177 | }""" 178 | }.toSeq 179 | } 180 | }""" 181 | 182 | // types inferred properly here 183 | val mapped = users.map { case (name, age, email) => 184 | json"""{ 185 | name: $name, 186 | age: $age, 187 | email: $email 188 | }""" 189 | } 190 | val quoteC = json"""{ 191 | users: [..$mapped] 192 | }""" 193 | 194 | quoteA should equal (jsonObject) 195 | quoteB should equal (jsonObject) 196 | quoteC should equal (jsonObject) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /upickle/src/main/scala/net/maffoo/jsonquote/upickle/Parse.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.upickle 2 | 3 | import net.maffoo.jsonquote.Parser 4 | import ujson._ 5 | 6 | object Parse extends Parser[Value, (String, Value)] { 7 | def makeObject(fields: Iterable[(String, Value)]): Value = Obj.from(fields) 8 | def makeArray(elements: Iterable[Value]): Value = Arr.from(elements) 9 | def makeNumber(n: BigDecimal): Value = Num(n.toDouble) 10 | def makeNumber(n: Double): Value = Num(n) 11 | def makeString(s: String): Value = Str(s) 12 | def makeBoolean(b: Boolean): Value = Bool(b) 13 | def makeNull(): Value = Null 14 | def makeSpliceValue(): Value = SpliceValue() 15 | def makeSpliceValues(): Value = SpliceValues() 16 | 17 | def makeField(k: String, v: Value): (String, Value) = (k, v) 18 | def makeSpliceField(): (String, Value) = SpliceField() 19 | def makeSpliceFields(): (String, Value) = SpliceFields() 20 | def makeSpliceFieldNameOpt(): (String, Value) = SpliceFieldNameOpt() 21 | def makeSpliceFieldName(v: Value): (String, Value) = SpliceFieldName(v) 22 | def makeSpliceFieldOpt(k: String): (String, Value) = SpliceFieldOpt(k) 23 | } 24 | -------------------------------------------------------------------------------- /upickle/src/main/scala/net/maffoo/jsonquote/upickle/Splice.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.upickle 2 | 3 | import ujson._ 4 | 5 | import scala.util.Random 6 | 7 | // Sentinel values to mark splice points in the json AST. 8 | // Because ujson uses Map to represent json objects, we use unique 9 | // random keys beginning with a known prefix to mark field splice points. 10 | 11 | class Sentinel[A <: AnyRef](inst: A) { 12 | def apply(): A = inst 13 | def unapply(a: A): Boolean = a == inst 14 | } 15 | 16 | object SpliceValue extends Sentinel[Value](Obj("__splice_value__" -> Null)) 17 | object SpliceValues extends Sentinel[Value](Obj("__splice_values__" -> Null)) 18 | 19 | object SpliceField { 20 | def apply(): (String, Value) = ("__splice_field__%016X".format(Random.nextLong), Null) 21 | def unapply(a: (String, Value)): Boolean = a match { 22 | case (f, Null) if f.startsWith("__splice_field__") => true 23 | case _ => false 24 | } 25 | } 26 | 27 | object SpliceFields { 28 | def apply(): (String, Value) = ("__splice_fields__%016X".format(Random.nextLong), Null) 29 | def unapply(a: (String, Value)): Boolean = a match { 30 | case (f, Null) if f.startsWith("__splice_fields__") => true 31 | case _ => false 32 | } 33 | } 34 | 35 | object SpliceFieldNameOpt { 36 | def apply(): (String, Value) = ("__splice_field_name_opt__%016X".format(Random.nextLong), Null) 37 | def unapply(f: (String, Value)): Boolean = f match { 38 | case (f, Null) if f.startsWith("__splice_field_name_opt__") => true 39 | case _ => false 40 | } 41 | } 42 | 43 | object SpliceFieldName { 44 | def apply(x: Value): (String, Value) = ("__splice_field_name__%016X".format(Random.nextLong), x) 45 | def unapply(f: (String, Value)): Option[Value] = f match { 46 | case (f, x) if f.startsWith("__splice_field_name__") => Some(x) 47 | case _ => None 48 | } 49 | } 50 | 51 | object SpliceFieldOpt { 52 | def apply(k: String): (String, Null) = (k, null) 53 | def unapply(f: (String, Value)): Option[String] = f match { 54 | case (k, null) => Some(k) 55 | case _ => None 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /upickle/src/main/scala/net/maffoo/jsonquote/upickle/package.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote 2 | 3 | import net.maffoo.jsonquote.Macros._ 4 | import ujson._ 5 | import _root_.upickle.default.Writer 6 | 7 | import scala.collection.mutable 8 | import scala.language.experimental.macros 9 | 10 | 11 | package object upickle { 12 | /** 13 | * Rendering Value produces valid json literal 14 | */ 15 | implicit class UpickleToLiteralJson(val json: Value) extends AnyVal { 16 | def toLiteral: literal.Json = new literal.Json(json.toString()) 17 | } 18 | 19 | implicit class RichJsonSringContext(val sc: StringContext) extends AnyVal { 20 | def json(args: Any*): Value = macro jsonImpl 21 | } 22 | 23 | def jsonImpl(c: Context)(args: c.Expr[Any]*): c.Expr[Value] = { 24 | import c.universe._ 25 | 26 | // fully-qualified symbols and types (for hygiene) 27 | val nil = q"_root_.scala.collection.immutable.Nil" 28 | val seq = q"_root_.scala.Seq" 29 | val iterable = q"_root_.scala.Iterable" 30 | val jsArray = q"_root_.ujson.Arr" 31 | val jsObject = q"_root_.ujson.Obj" 32 | val writeJs = q"_root_.upickle.default.writeJs" 33 | 34 | // convert the given json AST to a tree with arguments spliced in at the correct locations 35 | def splice(js: Value)(implicit args: Iterator[Tree]): Tree = js match { 36 | case SpliceValue() => spliceValue(args.next) 37 | case SpliceValues() => c.abort(c.enclosingPosition, "cannot splice values at top level") 38 | 39 | case Obj(members) => 40 | val seqMembers = members.collect { 41 | case SpliceFields() => 42 | case SpliceFieldNameOpt() => 43 | case SpliceFieldOpt(k) => 44 | } 45 | if (seqMembers.isEmpty) { 46 | val ms = members.toSeq.map { 47 | case SpliceField() => spliceField(args.next) 48 | case SpliceFieldName(v) => q"(${spliceFieldName(args.next)}, ${splice(v)})" 49 | case (k, v) => q"($k, ${splice(v)})" 50 | } 51 | q"$jsObject(..$ms)" 52 | } else { 53 | val ms = members.toSeq.map { 54 | case SpliceField() => q"$seq(${spliceField(args.next)})" 55 | case SpliceFields() => spliceFields(args.next) 56 | case SpliceFieldNameOpt() => spliceFieldNameOpt(args.next, args.next) 57 | case SpliceFieldName(v) => q"$seq((${spliceFieldName(args.next)}, ${splice(v)}))" 58 | case SpliceFieldOpt(k) => spliceFieldOpt(k, args.next) 59 | case (k, v) => q"$seq(($k, ${splice(v)}))" 60 | } 61 | q"$jsObject.from($iterable(..$ms).flatten)" 62 | } 63 | 64 | case Arr(elements) => 65 | val seqElems = elements.collect { 66 | case SpliceValues() => 67 | } 68 | if (seqElems.isEmpty) { 69 | val es = elements.map { 70 | case SpliceValue() => spliceValue(args.next) 71 | case e => splice(e) 72 | } 73 | q"$jsArray(..$es)" 74 | } else { 75 | val es = elements.map { 76 | case SpliceValue() => q"$seq(${spliceValue(args.next)})" 77 | case SpliceValues() => spliceValues(args.next) 78 | case e => q"$seq(${splice(e)})" 79 | } 80 | q"$jsArray.from($iterable(..$es).flatten)" 81 | } 82 | 83 | case Str(s) => q"_root_.ujson.Str($s)" 84 | case Num(n) => q"_root_.ujson.Num($n)" 85 | case Bool(true) => q"_root_.ujson.Bool(true)" 86 | case Bool(false) => q"_root_.ujson.Bool(false)" 87 | case Null => q"_root_.ujson.Null" 88 | } 89 | 90 | def spliceValue(e: Tree): Tree = e.tpe match { 91 | case t if t <:< c.typeOf[Value] => e 92 | case t => 93 | val writer = inferWriter(e, t) 94 | q"$writeJs($e)($writer)" 95 | } 96 | 97 | def spliceValues(e: Tree): Tree = e.tpe match { 98 | case t if t <:< c.typeOf[Iterable[Value]] => e 99 | case t if t <:< c.typeOf[Iterable[Any]] => 100 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[Nothing]] :: Nil))(0) 101 | val writer = inferWriter(e, valueTpe) 102 | q"$e.map(v => $writeJs(v)($writer))" 103 | 104 | case t if t <:< c.typeOf[None.type] => nil 105 | case t if t <:< c.typeOf[Option[Value]] => e 106 | case t if t <:< c.typeOf[Option[Any]] => 107 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 108 | val writer = inferWriter(e, valueTpe) 109 | q"$e.toIterable.map(v => $writeJs(v)($writer))" 110 | 111 | case t => c.abort(e.pos, s"required Iterable[_] but got $t") 112 | } 113 | 114 | def spliceField(e: Tree): Tree = e.tpe match { 115 | case t if t <:< c.typeOf[(String, Value)] => e 116 | case t if t <:< c.typeOf[(String, Any)] => 117 | val valueTpe = typeParams(lub(t :: c.typeOf[(String, Nothing)] :: Nil))(1) 118 | val writer = inferWriter(e, valueTpe) 119 | q"val (k, v) = $e; (k, $writeJs(v)($writer))" 120 | 121 | case t => c.abort(e.pos, s"required (String, _) but got $t") 122 | } 123 | 124 | def spliceFields(e: Tree): Tree = e.tpe match { 125 | case t if t <:< c.typeOf[Iterable[(String, Value)]] => e 126 | case t if t <:< c.typeOf[Iterable[(String, Any)]] => 127 | val valueTpe = typeParams(lub(t :: c.typeOf[Iterable[(String, Nothing)]] :: Nil))(2) 128 | val writer = inferWriter(e, valueTpe) 129 | q"$e.map { case (k, v) => (k, $writeJs(v)($writer)) }" 130 | 131 | case t if t <:< c.typeOf[None.type] => nil 132 | case t if t <:< c.typeOf[Option[(String, Value)]] => e 133 | case t if t <:< c.typeOf[Option[(String, Any)]] => 134 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[(String, Nothing)]] :: Nil))(2) 135 | val writer = inferWriter(e, valueTpe) 136 | q"$e.toIterable.map { case (k, v) => (k, $writeJs(v)($writer)) }" 137 | 138 | case t => c.abort(e.pos, s"required Iterable[(String, _)] but got $t") 139 | } 140 | 141 | def spliceFieldOpt(k: String, e: Tree): Tree = e.tpe match { 142 | case t if t <:< c.typeOf[None.type] => nil 143 | case t if t <:< c.typeOf[Option[Value]] => q"$e.toIterable.map(v => ($k, v))" 144 | case t if t <:< c.typeOf[Option[Any]] => 145 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 146 | val writer = inferWriter(e, valueTpe) 147 | q"$e.toIterable.map { v => ($k, $writeJs(v)($writer)) }" 148 | 149 | case t => c.abort(e.pos, s"required Option[_] but got $t") 150 | } 151 | 152 | def spliceFieldName(e: Tree): Tree = e.tpe match { 153 | case t if t =:= c.typeOf[String] => e 154 | case t => c.abort(e.pos, s"required String but got $t") 155 | } 156 | 157 | def spliceFieldNameOpt(k: Tree, v: Tree): Tree = { 158 | k.tpe match { 159 | case t if t =:= c.typeOf[String] => 160 | case t => c.abort(k.pos, s"required String but got $t") 161 | } 162 | v.tpe match { 163 | case t if t <:< c.typeOf[None.type] => nil 164 | case t if t <:< c.typeOf[Option[Value]] => q"$v.toIterable.map(v => ($k, v))" 165 | case t if t <:< c.typeOf[Option[Any]] => 166 | val valueTpe = typeParams(lub(t :: c.typeOf[Option[Nothing]] :: Nil))(0) 167 | val writer = inferWriter(v, valueTpe) 168 | q"$v.toIterable.map { v => ($k, $writeJs(v)($writer)) }" 169 | 170 | case t => c.abort(v.pos, s"required Option[_] but got $t") 171 | } 172 | } 173 | 174 | // return a list of type parameters in the given type 175 | // example: List[(String, Int)] => Seq(Tuple2, String, Int) 176 | def typeParams(tpe: Type): Seq[Type] = { 177 | val b = Iterable.newBuilder[Type] 178 | tpe.foreach(b += _) 179 | b.result.drop(2).grouped(2).map(_.head).toIndexedSeq 180 | } 181 | 182 | // locate an implicit Writes[T] for the given type 183 | def inferWriter(e: Tree, t: Type): Tree = { 184 | val writerTpe = appliedType(c.typeOf[Writer[_]], List(t)) 185 | c.inferImplicitValue(writerTpe) match { 186 | case EmptyTree => c.abort(e.pos, s"could not find implicit value of type JsonWriter[$t]") 187 | case tree => tree 188 | } 189 | } 190 | 191 | // Parse the string context parts into a json AST with holes, and then 192 | // typecheck/convert args to the appropriate types and splice them in. 193 | c.prefix.tree match { 194 | case Apply(_, List(Apply(_, partTrees))) => 195 | val parts = partTrees map { case Literal(Constant(const: String)) => const } 196 | val positions = (parts zip partTrees.map(_.pos)).toMap 197 | val js = try { 198 | Parse(parts) 199 | } catch { 200 | case JsonError(msg, Pos(s, ofs)) => 201 | val pos = positions(s) 202 | c.abort(pos.withPoint(pos.point + ofs), msg) 203 | } 204 | c.Expr[Value](splice(js)(args.iterator.map(_.tree))) 205 | 206 | case _ => 207 | c.abort(c.enclosingPosition, "invalid") 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /upickle/src/test/scala/net/maffoo/jsonquote/upickle/Test.scala: -------------------------------------------------------------------------------- 1 | package net.maffoo.jsonquote.upickle 2 | 3 | import org.scalatest.{FunSuite, Matchers} 4 | import ujson.{Arr, Null, Num, Obj, Str, Value} 5 | import upickle.default 6 | import upickle.default._ 7 | import upickle.default.macroRW 8 | 9 | case class Foo(bar: String, baz: String) 10 | 11 | class UpickleTest extends FunSuite with Matchers { 12 | 13 | implicit val FooRW: default.ReadWriter[Foo] = macroRW 14 | 15 | def check(js: Value, s: String): Unit = { js should equal (read[Value](s)) } 16 | 17 | test("can parse plain json") { 18 | check(json""" "hello!" """, """"hello!"""") 19 | } 20 | 21 | test("can use bare identifiers for object keys") { 22 | check(json"{ test0: 0 }", """{ "test0": 0 }""") 23 | check(json"{ test: 1 }", """{ "test": 1 }""") 24 | check(json"{ test-2: 2 }", """{ "test-2": 2 }""") 25 | check(json"{ test_3: 3 }", """{ "test_3": 3 }""") 26 | check(json"{ _test-4: 4 }", """{ "_test-4": 4 }""") 27 | } 28 | 29 | test("can inject value with implicit Writes") { 30 | val foo = Foo(bar = "a", baz = "b") 31 | check(json"$foo", """{"bar": "a", "baz": "b"}""") 32 | } 33 | 34 | test("can inject values with implicit Writes") { 35 | val foos = List(Foo("1", "2"), Foo("3", "4")) 36 | check(json"[..$foos]", """[{"bar":"1", "baz":"2"}, {"bar":"3", "baz":"4"}]""") 37 | check(json"[..$Nil]", """[]""") 38 | } 39 | 40 | test("can inject Option values") { 41 | val vOpt = Some(Num(1)) 42 | 43 | check(json"[..$vOpt]", """[1]""") 44 | check(json"[..$None]" , """[]""") 45 | } 46 | 47 | test("can inject Option values with implicit Writes") { 48 | val vOpt = Some(1) 49 | check(json"[..$vOpt]", """[1]""") 50 | } 51 | 52 | test("can mix values, Iterables and Options in array") { 53 | val a = List("a") 54 | val b = Some("b") 55 | val c = Nil 56 | val d = None 57 | check(json"""[1, ..$a, 2, ..$b, 3, ..$c, 4, ..$d, 5]""", """[1, "a", 2, "b", 3, 4, 5]""") 58 | } 59 | 60 | test("can inject Tuple2 as object field") { 61 | val kv = "test" -> Foo("1", "2") 62 | check(json"{$kv}", """{"test": {"bar":"1", "baz":"2"}}""") 63 | } 64 | 65 | //First one with error 66 | test("can inject multiple fields") { 67 | val kvs = Seq("a" -> 1, "b" -> 2) 68 | val kvs2 = Nil 69 | check(json"{..$kvs}", """{"a": 1, "b": 2}""") 70 | check(json"{..$kvs2}", """{}""") 71 | } 72 | 73 | test("can inject just a field name") { 74 | val key = "foo" 75 | check(json"{$key: 1}", """{"foo": 1}""") 76 | } 77 | 78 | test("can inject Option fields") { 79 | val kvOpt = Some("a" -> 1) 80 | val kvOpt2 = None 81 | check(json"{..$kvOpt}", """{"a": 1}""") 82 | check(json"{..$kvOpt2}", """{}""") 83 | } 84 | 85 | test("can inject Option fields with implicit Writes") { 86 | val kvOpt = Some("a" -> Foo("1", "2")) 87 | check(json"{..$kvOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 88 | } 89 | 90 | test("can inject Option field values") { 91 | val vOpt = Some(1) 92 | val vOpt2 = None 93 | check(json"{a:? $vOpt}", """{"a": 1}""") 94 | check(json"{a:? $vOpt2}", """{}""") 95 | 96 | val k = "a" 97 | check(json"{$k:? $vOpt}", """{"a": 1}""") 98 | check(json"{$k:? $vOpt2}", """{}""") 99 | } 100 | 101 | test("can inject Option field values with implicit Writes") { 102 | val vOpt = Some(Foo("1", "2")) 103 | check(json"{a:? $vOpt}", """{"a": {"bar": "1", "baz": "2"}}""") 104 | } 105 | 106 | test("can mix values, Iterables and Options in object") { 107 | val a = List("a" -> 10) 108 | val b = Some("b" -> 20) 109 | val c = Nil 110 | val d = None 111 | check( 112 | json"""{i:1, ..$a, j:2, ..$b, k:3, ..$c, l:4, ..$d, m:5}""", 113 | """{"i":1, "a":10, "j":2, "b":20, "k":3, "l":4, "m":5}""" 114 | ) 115 | } 116 | 117 | test("can nest jsonquote templates") { 118 | 119 | val jsonObject = Obj( 120 | "users" -> Seq( 121 | Obj( 122 | "name" -> Str("Bob"), 123 | "age" -> Num(31), 124 | "email" -> Str("bob@gmail.com") 125 | ), 126 | Obj( 127 | "name" -> Str("Kiki"), 128 | "age" -> Num(25) 129 | ) 130 | ) 131 | ) 132 | 133 | val users = Seq(("Bob", 31, Some("bob@gmail.com")), ("Kiki", 25, None)) 134 | 135 | // TODO: find a way to avoid the need for .toSeq here 136 | val quoteA = json"""{ 137 | users: [..${ 138 | users.map { case (name, age, email) => 139 | json"""{ 140 | name: $name, 141 | age: $age, 142 | email:? $email 143 | }""" 144 | }.toSeq 145 | }] 146 | }""" 147 | 148 | // upickle already knows how to convert Seq[Value] to json array 149 | // still need the .toSeq here 150 | val quoteB = json"""{ 151 | users: ${ 152 | users.map { case (name, age, email) => 153 | json"""{ 154 | name: $name, 155 | age: $age, 156 | email:? $email 157 | }""" 158 | }.toSeq 159 | } 160 | }""" 161 | 162 | // types inferred properly here 163 | val mapped = users.map { case (name, age, email) => 164 | json"""{ 165 | name: $name, 166 | age: $age, 167 | email:? $email 168 | }""" 169 | } 170 | val quoteC = json"""{ 171 | users: [..$mapped] 172 | }""" 173 | 174 | quoteA should equal (jsonObject) 175 | quoteB should equal (jsonObject) 176 | quoteC should equal (jsonObject) 177 | } 178 | } 179 | --------------------------------------------------------------------------------