summaryrefslogtreecommitdiff
path: root/jvm/src/main
diff options
context:
space:
mode:
authorJakob Odersky <jakob@odersky.com>2018-03-07 14:07:25 -0800
committerJakob Odersky <jakob@odersky.com>2019-04-03 20:08:15 -0400
commitf5cbf3b48d1f94a5007d815cc42d5f3e94b625ec (patch)
tree46921ce46d8459040cc06cbedc8f47abfbd58c4b /jvm/src/main
parent0b893f26ed2fd71649389e7eccc7bba6f1507946 (diff)
downloadspray-json-f5cbf3b48d1f94a5007d815cc42d5f3e94b625ec.tar.gz
spray-json-f5cbf3b48d1f94a5007d815cc42d5f3e94b625ec.tar.bz2
spray-json-f5cbf3b48d1f94a5007d815cc42d5f3e94b625ec.zip
Add support for ScalaJS and Scala Native
Binary compatibility with previous versions is maintained.
Diffstat (limited to 'jvm/src/main')
-rw-r--r--jvm/src/main/boilerplate/spray/json/ProductFormatsInstances.scala.template45
-rw-r--r--jvm/src/main/scala/spray/json/ProductFormats.scala155
2 files changed, 200 insertions, 0 deletions
diff --git a/jvm/src/main/boilerplate/spray/json/ProductFormatsInstances.scala.template b/jvm/src/main/boilerplate/spray/json/ProductFormatsInstances.scala.template
new file mode 100644
index 0000000..fa0d875
--- /dev/null
+++ b/jvm/src/main/boilerplate/spray/json/ProductFormatsInstances.scala.template
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2011,2012 Mathias Doenitz, Johannes Rudolph
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package spray.json
+
+import scala.reflect.{ classTag, ClassTag }
+
+trait ProductFormatsInstances { self: ProductFormats with StandardFormats =>
+[# // Case classes with 1 parameters
+
+ def jsonFormat1[[#P1 :JF#], T <: Product :ClassTag](construct: ([#P1#]) => T): RootJsonFormat[T] = {
+ val Array([#p1#]) = extractFieldNames(classTag[T])
+ jsonFormat(construct, [#p1#])
+ }
+ def jsonFormat[[#P1 :JF#], T <: Product](construct: ([#P1#]) => T, [#fieldName1: String#]): RootJsonFormat[T] = new RootJsonFormat[T]{
+ def write(p: T) = {
+ val fields = new collection.mutable.ListBuffer[(String, JsValue)]
+ fields.sizeHint(1 * 2)
+ [#fields ++= productElement##2Field[P1](fieldName1, p, 0)#
+ ]
+ JsObject(fields.toSeq: _*)
+ }
+ def read(value: JsValue) = {
+ [#val p1V = fromField[P1](value, fieldName1)#
+ ]
+ construct([#p1V#])
+ }
+ }#
+
+
+]
+}
diff --git a/jvm/src/main/scala/spray/json/ProductFormats.scala b/jvm/src/main/scala/spray/json/ProductFormats.scala
new file mode 100644
index 0000000..81a48af
--- /dev/null
+++ b/jvm/src/main/scala/spray/json/ProductFormats.scala
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2011 Mathias Doenitz
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package spray.json
+
+import java.lang.reflect.Modifier
+import scala.annotation.tailrec
+import scala.util.control.NonFatal
+import scala.reflect.ClassTag
+
+/**
+ * Provides the helpers for constructing custom JsonFormat implementations for types implementing the Product trait
+ * (especially case classes)
+ */
+trait ProductFormats extends ProductFormatsInstances {
+ this: StandardFormats =>
+
+ def jsonFormat0[T](construct: () => T): RootJsonFormat[T] =
+ new RootJsonFormat[T] {
+ def write(p: T) = JsObject()
+ def read(value: JsValue) = value match {
+ case JsObject(_) => construct()
+ case _ => throw new DeserializationException("Object expected")
+ }
+ }
+
+ // helpers
+
+ protected def productElement2Field[T](fieldName: String, p: Product, ix: Int, rest: List[JsField] = Nil)
+ (implicit writer: JsonWriter[T]): List[JsField] = {
+ val value = p.productElement(ix).asInstanceOf[T]
+ writer match {
+ case _: OptionFormat[_] if (value == None) => rest
+ case _ => (fieldName, writer.write(value)) :: rest
+ }
+ }
+
+ protected def fromField[T](value: JsValue, fieldName: String)
+ (implicit reader: JsonReader[T]) = value match {
+ case x: JsObject if
+ (reader.isInstanceOf[OptionFormat[_]] &
+ !x.fields.contains(fieldName)) =>
+ None.asInstanceOf[T]
+ case x: JsObject =>
+ try reader.read(x.fields(fieldName))
+ catch {
+ case e: NoSuchElementException =>
+ deserializationError("Object is missing required member '" + fieldName + "'", e, fieldName :: Nil)
+ case DeserializationException(msg, cause, fieldNames) =>
+ deserializationError(msg, cause, fieldName :: fieldNames)
+ }
+ case _ => deserializationError("Object expected in field '" + fieldName + "'", fieldNames = fieldName :: Nil)
+ }
+
+ protected def extractFieldNames(tag: ClassTag[_]): Array[String] = {
+ val clazz = tag.runtimeClass
+ try {
+ // copy methods have the form copy$default$N(), we need to sort them in order, but must account for the fact
+ // that lexical sorting of ...8(), ...9(), ...10() is not correct, so we extract N and sort by N.toInt
+ val copyDefaultMethods = clazz.getMethods.filter(_.getName.startsWith("copy$default$")).sortBy(
+ _.getName.drop("copy$default$".length).takeWhile(_ != '(').toInt)
+ val fields = clazz.getDeclaredFields.filterNot { f =>
+ import Modifier._
+ (f.getModifiers & (TRANSIENT | STATIC | 0x1000 /* SYNTHETIC*/)) > 0
+ }
+ if (copyDefaultMethods.length != fields.length)
+ sys.error("Case class " + clazz.getName + " declares additional fields")
+ if (fields.zip(copyDefaultMethods).exists { case (f, m) => f.getType != m.getReturnType })
+ sys.error("Cannot determine field order of case class " + clazz.getName)
+ fields.map(f => ProductFormats.unmangle(f.getName))
+ } catch {
+ case NonFatal(ex) => throw new RuntimeException("Cannot automatically determine case class field names and order " +
+ "for '" + clazz.getName + "', please use the 'jsonFormat' overload with explicit field name specification", ex)
+ }
+ }
+
+}
+
+object ProductFormats {
+
+ private def unmangle(name: String) = {
+ import java.lang.{StringBuilder => JStringBuilder}
+ @tailrec def rec(ix: Int, builder: JStringBuilder): String = {
+ val rem = name.length - ix
+ if (rem > 0) {
+ var ch = name.charAt(ix)
+ var ni = ix + 1
+ val sb = if (ch == '$' && rem > 1) {
+ def c(offset: Int, ch: Char) = name.charAt(ix + offset) == ch
+ ni = name.charAt(ix + 1) match {
+ case 'a' if rem > 3 && c(2, 'm') && c(3, 'p') => { ch = '&'; ix + 4 }
+ case 'a' if rem > 2 && c(2, 't') => { ch = '@'; ix + 3 }
+ case 'b' if rem > 4 && c(2, 'a') && c(3, 'n') && c(4, 'g') => { ch = '!'; ix + 5 }
+ case 'b' if rem > 3 && c(2, 'a') && c(3, 'r') => { ch = '|'; ix + 4 }
+ case 'd' if rem > 3 && c(2, 'i') && c(3, 'v') => { ch = '/'; ix + 4 }
+ case 'e' if rem > 2 && c(2, 'q') => { ch = '='; ix + 3 }
+ case 'g' if rem > 7 && c(2, 'r') && c(3, 'e') && c(4, 'a') && c(5, 't') && c(6, 'e') && c(7, 'r') => { ch = '>'; ix + 8 }
+ case 'h' if rem > 4 && c(2, 'a') && c(3, 's') && c(4, 'h') => { ch = '#'; ix + 5 }
+ case 'l' if rem > 4 && c(2, 'e') && c(3, 's') && c(4, 's') => { ch = '<'; ix + 5 }
+ case 'm' if rem > 5 && c(2, 'i') && c(3, 'n') && c(4, 'u') && c(5, 's') => { ch = '-'; ix + 6 }
+ case 'p' if rem > 7 && c(2, 'e') && c(3, 'r') && c(4, 'c') && c(5, 'e') && c(6, 'n') && c(7, 't') => { ch = '%'; ix + 8 }
+ case 'p' if rem > 4 && c(2, 'l') && c(3, 'u') && c(4, 's') => { ch = '+'; ix + 5 }
+ case 'q' if rem > 5 && c(2, 'm') && c(3, 'a') && c(4, 'r') && c(5, 'k') => { ch = '?'; ix + 6 }
+ case 't' if rem > 5 && c(2, 'i') && c(3, 'l') && c(4, 'd') && c(5, 'e') => { ch = '~'; ix + 6 }
+ case 't' if rem > 5 && c(2, 'i') && c(3, 'm') && c(4, 'e') && c(5, 's') => { ch = '*'; ix + 6 }
+ case 'u' if rem > 2 && c(2, 'p') => { ch = '^'; ix + 3 }
+ case 'u' if rem > 5 =>
+ def hexValue(offset: Int): Int = {
+ val c = name.charAt(ix + offset)
+ if ('0' <= c && c <= '9') c - '0'
+ else if ('a' <= c && c <= 'f') c - 87
+ else if ('A' <= c && c <= 'F') c - 55 else -0xFFFF
+ }
+ val ci = (hexValue(2) << 12) + (hexValue(3) << 8) + (hexValue(4) << 4) + hexValue(5)
+ if (ci >= 0) { ch = ci.toChar; ix + 6 } else ni
+ case _ => ni
+ }
+ if (ni > ix + 1 && builder == null) new JStringBuilder(name.substring(0, ix)) else builder
+ } else builder
+ rec(ni, if (sb != null) sb.append(ch) else null)
+ } else if (builder != null) builder.toString else name
+ }
+ rec(0, null)
+ }
+}
+
+/**
+ * This trait supplies an alternative rendering mode for optional case class members.
+ * Normally optional members that are undefined (`None`) are not rendered at all.
+ * By mixing in this trait into your custom JsonProtocol you can enforce the rendering of undefined members as `null`.
+ * (Note that this only affect JSON writing, spray-json will always read missing optional members as well as `null`
+ * optional members as `None`.)
+ */
+trait NullOptions extends ProductFormats {
+ this: StandardFormats =>
+
+ override protected def productElement2Field[T](fieldName: String, p: Product, ix: Int, rest: List[JsField])
+ (implicit writer: JsonWriter[T]) = {
+ val value = p.productElement(ix).asInstanceOf[T]
+ (fieldName, writer.write(value)) :: rest
+ }
+}