diff options
Diffstat (limited to 'shared/src/main/scala')
-rw-r--r-- | shared/src/main/scala/spray/json/AdditionalFormats.scala | 117 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/BasicFormats.scala | 139 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/CollectionFormats.scala | 95 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/CompactPrinter.scala | 51 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/DefaultJsonProtocol.scala | 30 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/JsValue.scala | 125 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/JsonFormat.scala | 71 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/JsonParser.scala | 379 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/JsonParserSettings.scala | 56 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/JsonPrinter.scala | 106 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/PrettyPrinter.scala | 71 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/SortedPrinter.scala | 25 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/StandardFormats.scala | 120 | ||||
-rw-r--r-- | shared/src/main/scala/spray/json/package.scala | 66 |
14 files changed, 1451 insertions, 0 deletions
diff --git a/shared/src/main/scala/spray/json/AdditionalFormats.scala b/shared/src/main/scala/spray/json/AdditionalFormats.scala new file mode 100644 index 0000000..fbabb0b --- /dev/null +++ b/shared/src/main/scala/spray/json/AdditionalFormats.scala @@ -0,0 +1,117 @@ +/* + * Original implementation (C) 2009-2011 Debasish Ghosh + * Adapted and extended in 2011 by 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 + +/** + * Provides additional JsonFormats and helpers + */ +trait AdditionalFormats { + + implicit object JsValueFormat extends JsonFormat[JsValue] { + def write(value: JsValue) = value + def read(value: JsValue) = value + } + + implicit object RootJsObjectFormat extends RootJsonFormat[JsObject] { + def write(value: JsObject) = value + def read(value: JsValue) = value.asJsObject + } + + implicit object RootJsArrayFormat extends RootJsonFormat[JsArray] { + def write(value: JsArray) = value + def read(value: JsValue) = value match { + case x: JsArray => x + case _ => deserializationError("JSON array expected") + } + } + + /** + * Constructs a JsonFormat from its two parts, JsonReader and JsonWriter. + */ + def jsonFormat[T](reader: JsonReader[T], writer: JsonWriter[T]) = new JsonFormat[T] { + def write(obj: T) = writer.write(obj) + def read(json: JsValue) = reader.read(json) + } + + /** + * Constructs a RootJsonFormat from its two parts, RootJsonReader and RootJsonWriter. + */ + def rootJsonFormat[T](reader: RootJsonReader[T], writer: RootJsonWriter[T]) = + rootFormat(jsonFormat(reader, writer)) + + /** + * Turns a JsonWriter into a JsonFormat that throws an UnsupportedOperationException for reads. + */ + def lift[T](writer :JsonWriter[T]) = new JsonFormat[T] { + def write(obj: T): JsValue = writer.write(obj) + def read(value: JsValue) = + throw new UnsupportedOperationException("JsonReader implementation missing") + } + + /** + * Turns a RootJsonWriter into a RootJsonFormat that throws an UnsupportedOperationException for reads. + */ + def lift[T](writer :RootJsonWriter[T]): RootJsonFormat[T] = + rootFormat(lift(writer :JsonWriter[T])) + + /** + * Turns a JsonReader into a JsonFormat that throws an UnsupportedOperationException for writes. + */ + def lift[T <: AnyRef](reader :JsonReader[T]) = new JsonFormat[T] { + def write(obj: T): JsValue = + throw new UnsupportedOperationException("No JsonWriter[" + obj.getClass + "] available") + def read(value: JsValue) = reader.read(value) + } + + /** + * Turns a RootJsonReader into a RootJsonFormat that throws an UnsupportedOperationException for writes. + */ + def lift[T <: AnyRef](reader :RootJsonReader[T]): RootJsonFormat[T] = + rootFormat(lift(reader :JsonReader[T])) + + /** + * Lazy wrapper around serialization. Useful when you want to serialize (mutually) recursive structures. + */ + def lazyFormat[T](format: => JsonFormat[T]) = new JsonFormat[T] { + lazy val delegate = format; + def write(x: T) = delegate.write(x); + def read(value: JsValue) = delegate.read(value); + } + + /** + * Explicitly turns a JsonFormat into a RootJsonFormat. + */ + def rootFormat[T](format: JsonFormat[T]) = new RootJsonFormat[T] { + def write(obj: T) = format.write(obj) + def read(json: JsValue) = format.read(json) + } + + /** + * Wraps an existing JsonReader with Exception protection. + */ + def safeReader[A :JsonReader] = new JsonReader[Either[Exception, A]] { + def read(json: JsValue) = { + try { + Right(json.convertTo[A]) + } catch { + case e: Exception => Left(e) + } + } + } + +}
\ No newline at end of file diff --git a/shared/src/main/scala/spray/json/BasicFormats.scala b/shared/src/main/scala/spray/json/BasicFormats.scala new file mode 100644 index 0000000..2e6342f --- /dev/null +++ b/shared/src/main/scala/spray/json/BasicFormats.scala @@ -0,0 +1,139 @@ +/* + * Original implementation (C) 2009-2011 Debasish Ghosh + * Adapted and extended in 2011 by 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 + +/** + * Provides the JsonFormats for the most important Scala types. + */ +trait BasicFormats { + + implicit object IntJsonFormat extends JsonFormat[Int] { + def write(x: Int) = JsNumber(x) + def read(value: JsValue) = value match { + case JsNumber(x) => x.intValue + case x => deserializationError("Expected Int as JsNumber, but got " + x) + } + } + + implicit object LongJsonFormat extends JsonFormat[Long] { + def write(x: Long) = JsNumber(x) + def read(value: JsValue) = value match { + case JsNumber(x) => x.longValue + case x => deserializationError("Expected Long as JsNumber, but got " + x) + } + } + + implicit object FloatJsonFormat extends JsonFormat[Float] { + def write(x: Float) = JsNumber(x) + def read(value: JsValue) = value match { + case JsNumber(x) => x.floatValue + case JsNull => Float.NaN + case x => deserializationError("Expected Float as JsNumber, but got " + x) + } + } + + implicit object DoubleJsonFormat extends JsonFormat[Double] { + def write(x: Double) = JsNumber(x) + def read(value: JsValue) = value match { + case JsNumber(x) => x.doubleValue + case JsNull => Double.NaN + case x => deserializationError("Expected Double as JsNumber, but got " + x) + } + } + + implicit object ByteJsonFormat extends JsonFormat[Byte] { + def write(x: Byte) = JsNumber(x) + def read(value: JsValue) = value match { + case JsNumber(x) => x.byteValue + case x => deserializationError("Expected Byte as JsNumber, but got " + x) + } + } + + implicit object ShortJsonFormat extends JsonFormat[Short] { + def write(x: Short) = JsNumber(x) + def read(value: JsValue) = value match { + case JsNumber(x) => x.shortValue + case x => deserializationError("Expected Short as JsNumber, but got " + x) + } + } + + implicit object BigDecimalJsonFormat extends JsonFormat[BigDecimal] { + def write(x: BigDecimal) = { + require(x ne null) + JsNumber(x) + } + def read(value: JsValue) = value match { + case JsNumber(x) => x + case JsString(x) => BigDecimal(x) + case x => deserializationError("Expected BigDecimal as JsNumber, but got " + x) + } + } + + implicit object BigIntJsonFormat extends JsonFormat[BigInt] { + def write(x: BigInt) = { + require(x ne null) + JsNumber(x) + } + def read(value: JsValue) = value match { + case JsNumber(x) => x.toBigInt + case JsString(x) => BigInt(x) + case x => deserializationError("Expected BigInt as JsNumber, but got " + x) + } + } + + implicit object UnitJsonFormat extends JsonFormat[Unit] { + def write(x: Unit) = JsNumber(1) + def read(value: JsValue): Unit = {} + } + + implicit object BooleanJsonFormat extends JsonFormat[Boolean] { + def write(x: Boolean) = JsBoolean(x) + def read(value: JsValue) = value match { + case JsTrue => true + case JsFalse => false + case x => deserializationError("Expected JsBoolean, but got " + x) + } + } + + implicit object CharJsonFormat extends JsonFormat[Char] { + def write(x: Char) = JsString(String.valueOf(x)) + def read(value: JsValue) = value match { + case JsString(x) if x.length == 1 => x.charAt(0) + case x => deserializationError("Expected Char as single-character JsString, but got " + x) + } + } + + implicit object StringJsonFormat extends JsonFormat[String] { + def write(x: String) = { + require(x ne null) + JsString(x) + } + def read(value: JsValue) = value match { + case JsString(x) => x + case x => deserializationError("Expected String as JsString, but got " + x) + } + } + + implicit object SymbolJsonFormat extends JsonFormat[Symbol] { + def write(x: Symbol) = JsString(x.name) + def read(value: JsValue) = value match { + case JsString(x) => Symbol(x) + case x => deserializationError("Expected Symbol as JsString, but got " + x) + } + } +} diff --git a/shared/src/main/scala/spray/json/CollectionFormats.scala b/shared/src/main/scala/spray/json/CollectionFormats.scala new file mode 100644 index 0000000..ef94297 --- /dev/null +++ b/shared/src/main/scala/spray/json/CollectionFormats.scala @@ -0,0 +1,95 @@ +/* + * Original implementation (C) 2009-2011 Debasish Ghosh + * Adapted and extended in 2011 by 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 scala.reflect.ClassTag + +trait CollectionFormats { + + /** + * Supplies the JsonFormat for Lists. + */ + implicit def listFormat[T :JsonFormat] = new RootJsonFormat[List[T]] { + def write(list: List[T]) = JsArray(list.map(_.toJson).toVector) + def read(value: JsValue): List[T] = value match { + case JsArray(elements) => elements.toIterator.map(_.convertTo[T]).toList + case x => deserializationError("Expected List as JsArray, but got " + x) + } + } + + /** + * Supplies the JsonFormat for Arrays. + */ + implicit def arrayFormat[T :JsonFormat :ClassTag] = new RootJsonFormat[Array[T]] { + def write(array: Array[T]) = JsArray(array.map(_.toJson).toVector) + def read(value: JsValue) = value match { + case JsArray(elements) => elements.map(_.convertTo[T]).toArray[T] + case x => deserializationError("Expected Array as JsArray, but got " + x) + } + } + + /** + * Supplies the JsonFormat for Maps. The implicitly available JsonFormat for the key type K must + * always write JsStrings, otherwise a [[spray.json.SerializationException]] will be thrown. + */ + implicit def mapFormat[K :JsonFormat, V :JsonFormat] = new RootJsonFormat[Map[K, V]] { + def write(m: Map[K, V]) = JsObject { + m.map { field => + field._1.toJson match { + case JsString(x) => x -> field._2.toJson + case x => throw new SerializationException("Map key must be formatted as JsString, not '" + x + "'") + } + } + } + def read(value: JsValue) = value match { + case x: JsObject => x.fields.map { field => + (JsString(field._1).convertTo[K], field._2.convertTo[V]) + } + case x => deserializationError("Expected Map as JsObject, but got " + x) + } + } + + import collection.{immutable => imm} + + implicit def immIterableFormat[T :JsonFormat] = viaSeq[imm.Iterable[T], T](seq => imm.Iterable(seq :_*)) + implicit def immSeqFormat[T :JsonFormat] = viaSeq[imm.Seq[T], T](seq => imm.Seq(seq :_*)) + implicit def immIndexedSeqFormat[T :JsonFormat] = viaSeq[imm.IndexedSeq[T], T](seq => imm.IndexedSeq(seq :_*)) + implicit def immLinearSeqFormat[T :JsonFormat] = viaSeq[imm.LinearSeq[T], T](seq => imm.LinearSeq(seq :_*)) + implicit def immSetFormat[T :JsonFormat] = viaSeq[imm.Set[T], T](seq => imm.Set(seq :_*)) + implicit def vectorFormat[T :JsonFormat] = viaSeq[Vector[T], T](seq => Vector(seq :_*)) + + import collection._ + + implicit def iterableFormat[T :JsonFormat] = viaSeq[Iterable[T], T](seq => Iterable(seq :_*)) + implicit def seqFormat[T :JsonFormat] = viaSeq[Seq[T], T](seq => Seq(seq :_*)) + implicit def indexedSeqFormat[T :JsonFormat] = viaSeq[IndexedSeq[T], T](seq => IndexedSeq(seq :_*)) + implicit def linearSeqFormat[T :JsonFormat] = viaSeq[LinearSeq[T], T](seq => LinearSeq(seq :_*)) + implicit def setFormat[T :JsonFormat] = viaSeq[Set[T], T](seq => Set(seq :_*)) + + /** + * A JsonFormat construction helper that creates a JsonFormat for an Iterable type I from a builder function + * List => I. + */ + def viaSeq[I <: Iterable[T], T :JsonFormat](f: imm.Seq[T] => I): RootJsonFormat[I] = new RootJsonFormat[I] { + def write(iterable: I) = JsArray(iterable.map(_.toJson).toVector) + def read(value: JsValue) = value match { + case JsArray(elements) => f(elements.map(_.convertTo[T])) + case x => deserializationError("Expected Collection as JsArray, but got " + x) + } + } +} diff --git a/shared/src/main/scala/spray/json/CompactPrinter.scala b/shared/src/main/scala/spray/json/CompactPrinter.scala new file mode 100644 index 0000000..7260c2f --- /dev/null +++ b/shared/src/main/scala/spray/json/CompactPrinter.scala @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2009-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.StringBuilder + +/** + * A JsonPrinter that produces compact JSON source without any superfluous whitespace. + */ +trait CompactPrinter extends JsonPrinter { + + def print(x: JsValue, sb: StringBuilder): Unit = { + x match { + case JsObject(x) => printObject(x, sb) + case JsArray(x) => printArray(x, sb) + case _ => printLeaf(x, sb) + } + } + + protected def printObject(members: Map[String, JsValue], sb: StringBuilder): Unit = { + sb.append('{') + printSeq(members, sb.append(',')) { m => + printString(m._1, sb) + sb.append(':') + print(m._2, sb) + } + sb.append('}') + } + + protected def printArray(elements: Seq[JsValue], sb: StringBuilder): Unit = { + sb.append('[') + printSeq(elements, sb.append(','))(print(_, sb)) + sb.append(']') + } +} + +object CompactPrinter extends CompactPrinter
\ No newline at end of file diff --git a/shared/src/main/scala/spray/json/DefaultJsonProtocol.scala b/shared/src/main/scala/spray/json/DefaultJsonProtocol.scala new file mode 100644 index 0000000..4c93184 --- /dev/null +++ b/shared/src/main/scala/spray/json/DefaultJsonProtocol.scala @@ -0,0 +1,30 @@ +/* + * Original implementation (C) 2009-2011 Debasish Ghosh + * Adapted and extended in 2011 by 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 + +/** + * Provides all the predefined JsonFormats. + */ +trait DefaultJsonProtocol + extends BasicFormats + with StandardFormats + with CollectionFormats + with ProductFormats + with AdditionalFormats + +object DefaultJsonProtocol extends DefaultJsonProtocol diff --git a/shared/src/main/scala/spray/json/JsValue.scala b/shared/src/main/scala/spray/json/JsValue.scala new file mode 100644 index 0000000..9ed94da --- /dev/null +++ b/shared/src/main/scala/spray/json/JsValue.scala @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2009-2011 Mathias Doenitz + * Inspired by a similar implementation by Nathan Hamblen + * (https://github.com/dispatch/classic) + * + * 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 collection.immutable +import scala.collection.immutable.TreeMap + +/** + * The general type of a JSON AST node. + */ +sealed abstract class JsValue { + override def toString = compactPrint + def toString(printer: (JsValue => String)) = printer(this) + def compactPrint = CompactPrinter(this) + def prettyPrint = PrettyPrinter(this) + def sortedPrint = SortedPrinter(this) + def convertTo[T :JsonReader]: T = jsonReader[T].read(this) + + /** + * Returns `this` if this JsValue is a JsObject, otherwise throws a DeserializationException with the given error msg. + */ + def asJsObject(errorMsg: String = "JSON object expected"): JsObject = deserializationError(errorMsg) + + /** + * Returns `this` if this JsValue is a JsObject, otherwise throws a DeserializationException. + */ + def asJsObject: JsObject = asJsObject() + + @deprecated("Superceded by 'convertTo'", "1.1.0") + def fromJson[T :JsonReader]: T = convertTo +} + +/** + * A JSON object. + */ +case class JsObject(fields: Map[String, JsValue]) extends JsValue { + override def asJsObject(errorMsg: String) = this + def getFields(fieldNames: String*): immutable.Seq[JsValue] = fieldNames.toIterator.flatMap(fields.get).toList +} +object JsObject { + val empty = JsObject(TreeMap.empty[String, JsValue]) + def apply(members: JsField*): JsObject = new JsObject(TreeMap(members: _*)) + @deprecated("Use JsObject(JsValue*) instead", "1.3.0") + def apply(members: List[JsField]): JsObject = apply(members: _*) +} + +/** + * A JSON array. + */ +case class JsArray(elements: Vector[JsValue]) extends JsValue { + @deprecated("Use JsArray(Vector[JsValue]) instead", "1.3.0") + def this(elements: List[JsValue]) = this(elements.toVector) +} +object JsArray { + val empty = JsArray(Vector.empty) + def apply(elements: JsValue*) = new JsArray(elements.toVector) + @deprecated("Use JsArray(Vector[JsValue]) instead", "1.3.0") + def apply(elements: List[JsValue]) = new JsArray(elements.toVector) +} + +/** + * A JSON string. + */ +case class JsString(value: String) extends JsValue + +object JsString { + val empty = JsString("") + def apply(value: Symbol) = new JsString(value.name) +} + +/** + * A JSON number. + */ +case class JsNumber(value: BigDecimal) extends JsValue +object JsNumber { + val zero: JsNumber = apply(0) + def apply(n: Int) = new JsNumber(BigDecimal(n)) + def apply(n: Long) = new JsNumber(BigDecimal(n)) + def apply(n: Double) = n match { + case n if n.isNaN => JsNull + case n if n.isInfinity => JsNull + case _ => new JsNumber(BigDecimal(n)) + } + def apply(n: BigInt) = new JsNumber(BigDecimal(n)) + def apply(n: String) = new JsNumber(BigDecimal(n)) + def apply(n: Array[Char]) = new JsNumber(BigDecimal(n)) +} + +/** + * JSON Booleans. + */ +sealed abstract class JsBoolean extends JsValue { + def value: Boolean +} +object JsBoolean { + def apply(x: Boolean): JsBoolean = if (x) JsTrue else JsFalse + def unapply(x: JsBoolean): Option[Boolean] = Some(x.value) +} +case object JsTrue extends JsBoolean { + def value = true +} +case object JsFalse extends JsBoolean { + def value = false +} + +/** + * The representation for JSON null. + */ +case object JsNull extends JsValue diff --git a/shared/src/main/scala/spray/json/JsonFormat.scala b/shared/src/main/scala/spray/json/JsonFormat.scala new file mode 100644 index 0000000..c4915cc --- /dev/null +++ b/shared/src/main/scala/spray/json/JsonFormat.scala @@ -0,0 +1,71 @@ +/* + * Original implementation (C) 2009-2011 Debasish Ghosh + * Adapted and extended in 2011 by 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 annotation.implicitNotFound + +/** + * Provides the JSON deserialization for type T. + */ +@implicitNotFound(msg = "Cannot find JsonReader or JsonFormat type class for ${T}") +trait JsonReader[T] { + def read(json: JsValue): T +} + +object JsonReader { + implicit def func2Reader[T](f: JsValue => T): JsonReader[T] = new JsonReader[T] { + def read(json: JsValue) = f(json) + } +} + +/** + * Provides the JSON serialization for type T. + */ +@implicitNotFound(msg = "Cannot find JsonWriter or JsonFormat type class for ${T}") +trait JsonWriter[T] { + def write(obj: T): JsValue +} + +object JsonWriter { + implicit def func2Writer[T](f: T => JsValue): JsonWriter[T] = new JsonWriter[T] { + def write(obj: T) = f(obj) + } +} + +/** + * Provides the JSON deserialization and serialization for type T. + */ +trait JsonFormat[T] extends JsonReader[T] with JsonWriter[T] + +/** + * A special JsonReader capable of reading a legal JSON root object, i.e. either a JSON array or a JSON object. + */ +@implicitNotFound(msg = "Cannot find RootJsonReader or RootJsonFormat type class for ${T}") +trait RootJsonReader[T] extends JsonReader[T] + +/** + * A special JsonWriter capable of writing a legal JSON root object, i.e. either a JSON array or a JSON object. + */ +@implicitNotFound(msg = "Cannot find RootJsonWriter or RootJsonFormat type class for ${T}") +trait RootJsonWriter[T] extends JsonWriter[T] + +/** + * A special JsonFormat signaling that the format produces a legal JSON root object, i.e. either a JSON array + * or a JSON object. + */ +trait RootJsonFormat[T] extends JsonFormat[T] with RootJsonReader[T] with RootJsonWriter[T]
\ No newline at end of file diff --git a/shared/src/main/scala/spray/json/JsonParser.scala b/shared/src/main/scala/spray/json/JsonParser.scala new file mode 100644 index 0000000..7dfae0a --- /dev/null +++ b/shared/src/main/scala/spray/json/JsonParser.scala @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2014 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 scala.annotation.{switch, tailrec} +import java.lang.{StringBuilder => JStringBuilder} +import java.nio.{ByteBuffer, CharBuffer} +import java.nio.charset.Charset + +import scala.collection.immutable.TreeMap + +/** + * Fast, no-dependency parser for JSON as defined by http://tools.ietf.org/html/rfc4627. + */ +object JsonParser { + def apply(input: ParserInput): JsValue = new JsonParser(input).parseJsValue() + def apply(input: ParserInput, settings: JsonParserSettings): JsValue = new JsonParser(input, settings).parseJsValue() + + class ParsingException(val summary: String, val detail: String = "") + extends RuntimeException(if (summary.isEmpty) detail else if (detail.isEmpty) summary else summary + ":" + detail) +} + +class JsonParser(input: ParserInput, settings: JsonParserSettings = JsonParserSettings.default) { + def this(input: ParserInput) = this(input, JsonParserSettings.default) + + import JsonParser.ParsingException + + private[this] val sb = new JStringBuilder + private[this] var cursorChar: Char = input.nextChar() + private[this] var jsValue: JsValue = _ + + def parseJsValue(): JsValue = + parseJsValue(false) + + def parseJsValue(allowTrailingInput: Boolean): JsValue = { + ws() + `value`(settings.maxDepth) + if (!allowTrailingInput) + require(EOI) + jsValue + } + + ////////////////////// GRAMMAR //////////////////////// + + private final val EOI = '\uFFFF' // compile-time constant + + // http://tools.ietf.org/html/rfc4627#section-2.1 + private def `value`(remainingNesting: Int): Unit = + if (remainingNesting == 0) + throw new ParsingException( + "JSON input nested too deeply", + s"JSON input was nested more deeply than the configured limit of maxNesting = ${settings.maxDepth}" + ) + else { + val mark = input.cursor + def simpleValue(matched: Boolean, value: JsValue) = if (matched) jsValue = value else fail("JSON Value", mark) + (cursorChar: @switch) match { + case 'f' => simpleValue(`false`(), JsFalse) + case 'n' => simpleValue(`null`(), JsNull) + case 't' => simpleValue(`true`(), JsTrue) + case '{' => advance(); `object`(remainingNesting) + case '[' => advance(); `array`(remainingNesting) + case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '-' => `number`() + case '"' => `string`(); jsValue = if (sb.length == 0) JsString.empty else JsString(sb.toString) + case _ => fail("JSON Value") + } + } + + private def `false`() = advance() && ch('a') && ch('l') && ch('s') && ws('e') + private def `null`() = advance() && ch('u') && ch('l') && ws('l') + private def `true`() = advance() && ch('r') && ch('u') && ws('e') + + // http://tools.ietf.org/html/rfc4627#section-2.2 + private def `object`(remainingNesting: Int): Unit = { + ws() + jsValue = if (cursorChar != '}') { + @tailrec def members(map: Map[String, JsValue]): Map[String, JsValue] = { + `string`() + require(':') + ws() + val key = sb.toString + `value`(remainingNesting - 1) + val nextMap = map.updated(key, jsValue) + if (ws(',')) members(nextMap) else nextMap + } + val map = members(TreeMap.empty[String, JsValue]) + require('}') + JsObject(map) + } else { + advance() + JsObject.empty + } + ws() + } + + // http://tools.ietf.org/html/rfc4627#section-2.3 + private def `array`(remainingNesting: Int): Unit = { + ws() + jsValue = if (cursorChar != ']') { + val list = Vector.newBuilder[JsValue] + @tailrec def values(): Unit = { + `value`(remainingNesting - 1) + list += jsValue + if (ws(',')) values() + } + values() + require(']') + JsArray(list.result()) + } else { + advance() + JsArray.empty + } + ws() + } + + // http://tools.ietf.org/html/rfc4627#section-2.4 + private def `number`() = { + val start = input.cursor + val startChar = cursorChar + ch('-') + `int`() + `frac`() + `exp`() + val numberLength = input.cursor - start + + jsValue = + if (startChar == '0' && input.cursor - start == 1) JsNumber.zero + else if (numberLength <= settings.maxNumberCharacters) JsNumber(input.sliceCharArray(start, input.cursor)) + else { + val numberSnippet = new String(input.sliceCharArray(start, math.min(input.cursor, start + 20))) + throw new ParsingException("Number too long", + s"The number starting with '$numberSnippet' had " + + s"$numberLength characters which is more than the allowed limit maxNumberCharacters = ${settings.maxNumberCharacters}. If this is legit input " + + s"consider increasing the limit." + ) + } + ws() + } + + private def `int`(): Unit = if (!ch('0')) oneOrMoreDigits() + private def `frac`(): Unit = if (ch('.')) oneOrMoreDigits() + private def `exp`(): Unit = if (ch('e') || ch('E')) { ch('-') || ch('+'); oneOrMoreDigits() } + + private def oneOrMoreDigits(): Unit = if (DIGIT()) zeroOrMoreDigits() else fail("DIGIT") + @tailrec private def zeroOrMoreDigits(): Unit = if (DIGIT()) zeroOrMoreDigits() + + private def DIGIT(): Boolean = cursorChar >= '0' && cursorChar <= '9' && advance() + + // http://tools.ietf.org/html/rfc4627#section-2.5 + private def `string`(): Unit = { + if (cursorChar == '"') cursorChar = input.nextUtf8Char() else fail("'\"'") + sb.setLength(0) + while (`char`()) cursorChar = input.nextUtf8Char() + require('"') + ws() + } + + private def `char`() = + // simple bloom-filter that quick-matches the most frequent case of characters that are ok to append + // (it doesn't match control chars, EOI, '"', '?', '\', 'b' and certain higher, non-ASCII chars) + if (((1L << cursorChar) & ((31 - cursorChar) >> 31) & 0x7ffffffbefffffffL) != 0L) appendSB(cursorChar) + else cursorChar match { + case '"' | EOI => false + case '\\' => advance(); `escaped`() + case c => (c >= ' ') && appendSB(c) + } + + private def `escaped`() = { + def hexValue(c: Char): Int = + if ('0' <= c && c <= '9') c - '0' + else if ('a' <= c && c <= 'f') c - 87 + else if ('A' <= c && c <= 'F') c - 55 + else fail("hex digit") + def unicode() = { + var value = hexValue(cursorChar) + advance() + value = (value << 4) + hexValue(cursorChar) + advance() + value = (value << 4) + hexValue(cursorChar) + advance() + value = (value << 4) + hexValue(cursorChar) + appendSB(value.toChar) + } + (cursorChar: @switch) match { + case '"' | '/' | '\\' => appendSB(cursorChar) + case 'b' => appendSB('\b') + case 'f' => appendSB('\f') + case 'n' => appendSB('\n') + case 'r' => appendSB('\r') + case 't' => appendSB('\t') + case 'u' => advance(); unicode() + case _ => fail("JSON escape sequence") + } + } + + @tailrec private def ws(): Unit = + // fast test whether cursorChar is one of " \n\r\t" + if (((1L << cursorChar) & ((cursorChar - 64) >> 31) & 0x100002600L) != 0L) { advance(); ws() } + + ////////////////////////// HELPERS ////////////////////////// + + private def ch(c: Char): Boolean = if (cursorChar == c) { advance(); true } else false + private def ws(c: Char): Boolean = if (ch(c)) { ws(); true } else false + private def advance(): Boolean = { cursorChar = input.nextChar(); true } + private def appendSB(c: Char): Boolean = { sb.append(c); true } + private def require(c: Char): Unit = if (!ch(c)) fail(s"'$c'") + + private def fail(target: String, cursor: Int = input.cursor, errorChar: Char = cursorChar): Nothing = { + val ParserInput.Line(lineNr, col, text) = input.getLine(cursor) + val summary = { + val unexpected = + if (errorChar != EOI) { + val c = if (Character.isISOControl(errorChar)) "\\u%04x" format errorChar.toInt else errorChar.toString + s"character '$c'" + } else "end-of-input" + val expected = if (target != "'\uFFFF'") target else "end-of-input" + s"Unexpected $unexpected at input index $cursor (line $lineNr, position $col), expected $expected" + } + val detail = { + val sanitizedText = text.map(c => if (Character.isISOControl(c)) '?' else c) + s"\n$sanitizedText\n${" " * (col-1)}^\n" + } + throw new ParsingException(summary, detail) + } +} + +trait ParserInput { + /** + * Advance the cursor and get the next char. + * Since the char is required to be a 7-Bit ASCII char no decoding is required. + */ + def nextChar(): Char + + /** + * Advance the cursor and get the next char, which could potentially be outside + * of the 7-Bit ASCII range. Therefore decoding might be required. + */ + def nextUtf8Char(): Char + + def cursor: Int + def length: Int + def sliceString(start: Int, end: Int): String + def sliceCharArray(start: Int, end: Int): Array[Char] + def getLine(index: Int): ParserInput.Line +} + +object ParserInput { + private final val EOI = '\uFFFF' // compile-time constant + private final val ErrorChar = '\uFFFD' // compile-time constant, universal UTF-8 replacement character '�' + + implicit def apply(string: String): StringBasedParserInput = new StringBasedParserInput(string) + implicit def apply(chars: Array[Char]): CharArrayBasedParserInput = new CharArrayBasedParserInput(chars) + implicit def apply(bytes: Array[Byte]): ByteArrayBasedParserInput = new ByteArrayBasedParserInput(bytes) + + case class Line(lineNr: Int, column: Int, text: String) + + abstract class DefaultParserInput extends ParserInput { + protected var _cursor: Int = -1 + def cursor = _cursor + def getLine(index: Int): Line = { + val sb = new java.lang.StringBuilder + @tailrec def rec(ix: Int, lineStartIx: Int, lineNr: Int): Line = + nextUtf8Char() match { + case '\n' if index > ix => sb.setLength(0); rec(ix + 1, ix + 1, lineNr + 1) + case '\n' | EOI => Line(lineNr, index - lineStartIx + 1, sb.toString) + case c => sb.append(c); rec(ix + 1, lineStartIx, lineNr) + } + val savedCursor = _cursor + _cursor = -1 + val line = rec(ix = 0, lineStartIx = 0, lineNr = 1) + _cursor = savedCursor + line + } + } + + private val UTF8 = Charset.forName("UTF-8") + + /** + * ParserInput that allows to create a ParserInput from any randomly accessible indexed byte storage. + */ + abstract class IndexedBytesParserInput extends DefaultParserInput { + def length: Int + protected def byteAt(offset: Int): Byte + + private val byteBuffer = ByteBuffer.allocate(4) + private val charBuffer = CharBuffer.allocate(2) + private val decoder = UTF8.newDecoder() + def nextChar() = { + _cursor += 1 + if (_cursor < length) (byteAt(_cursor) & 0xFF).toChar else EOI + } + def nextUtf8Char() = { + @tailrec def decode(byte: Byte, remainingBytes: Int): Char = { + byteBuffer.put(byte) + if (remainingBytes > 0) { + _cursor += 1 + if (_cursor < length) decode(byteAt(_cursor), remainingBytes - 1) else ErrorChar + } else { + byteBuffer.flip() + val coderResult = decoder.decode(byteBuffer, charBuffer, false) + charBuffer.flip() + val result = if (coderResult.isUnderflow & charBuffer.hasRemaining) charBuffer.get() else ErrorChar + byteBuffer.clear() + if (!charBuffer.hasRemaining) charBuffer.clear() + result + } + } + + if (charBuffer.position() > 0) { + val result = charBuffer.get() + charBuffer.clear() + result + } else { + _cursor += 1 + if (_cursor < length) { + val byte = byteAt(_cursor) + if (byte >= 0) byte.toChar // 7-Bit ASCII + else if ((byte & 0xE0) == 0xC0) decode(byte, 1) // 2-byte UTF-8 sequence + else if ((byte & 0xF0) == 0xE0) decode(byte, 2) // 3-byte UTF-8 sequence + else if ((byte & 0xF8) == 0xF0) decode(byte, 3) // 4-byte UTF-8 sequence + else ErrorChar + } else EOI + } + } + } + + /** + * ParserInput reading directly off a byte array which is assumed to contain the UTF-8 encoded representation + * of the JSON input, without requiring a separate decoding step. + */ + class ByteArrayBasedParserInput(bytes: Array[Byte]) extends IndexedBytesParserInput { + protected def byteAt(offset: Int): Byte = bytes(offset) + def length: Int = bytes.length + + def sliceString(start: Int, end: Int) = new String(bytes, start, end - start, UTF8) + def sliceCharArray(start: Int, end: Int) = + UTF8.decode(ByteBuffer.wrap(java.util.Arrays.copyOfRange(bytes, start, end))).array() + } + + class StringBasedParserInput(string: String) extends DefaultParserInput { + def nextChar(): Char = { + _cursor += 1 + if (_cursor < string.length) string.charAt(_cursor) else EOI + } + def nextUtf8Char() = nextChar() + def length = string.length + def sliceString(start: Int, end: Int) = string.substring(start, end) + def sliceCharArray(start: Int, end: Int) = { + val chars = new Array[Char](end - start) + string.getChars(start, end, chars, 0) + chars + } + } + + class CharArrayBasedParserInput(chars: Array[Char]) extends DefaultParserInput { + def nextChar(): Char = { + _cursor += 1 + if (_cursor < chars.length) chars(_cursor) else EOI + } + def nextUtf8Char() = nextChar() + def length = chars.length + def sliceString(start: Int, end: Int) = new String(chars, start, end - start) + def sliceCharArray(start: Int, end: Int) = java.util.Arrays.copyOfRange(chars, start, end) + } +}
\ No newline at end of file diff --git a/shared/src/main/scala/spray/json/JsonParserSettings.scala b/shared/src/main/scala/spray/json/JsonParserSettings.scala new file mode 100644 index 0000000..8c76b0a --- /dev/null +++ b/shared/src/main/scala/spray/json/JsonParserSettings.scala @@ -0,0 +1,56 @@ +package spray.json + +/** + * Allows to customize settings for the JSON parser. + * + * Use it like this: + * + * ``` + * val customSettings = + * JsonParserSettings.default + * .withMaxDepth(100) + * .withMaxNumberCharacters(20) + * + * JsonParser(jsonString, customSettings) + * // or + * jsonString.parseJson(customSettings) + * ``` + */ +sealed trait JsonParserSettings { + /** + * The JsonParser uses recursive decent parsing that keeps intermediate values on the stack. To prevent + * StackOverflowExceptions a limit is enforced on the depth of the parsed JSON structure. + * + * As a guideline we tested that one level of depth needs about 300 bytes of stack space. + * + * The default is a depth of 1000. + */ + def maxDepth: Int + + /** + * Returns a copy of this settings object with the `maxDepth` setting changed to the new value. + */ + def withMaxDepth(newValue: Int): JsonParserSettings + + /** + * The maximum number of characters the parser should support for numbers. This is restricted because creating + * `BigDecimal`s with high precision can be very slow (approx. quadratic runtime per amount of characters). + */ + def maxNumberCharacters: Int + + /** + * Returns a copy of this settings object with the `maxNumberCharacters` setting changed to the new value. + */ + def withMaxNumberCharacters(newValue: Int): JsonParserSettings +} +object JsonParserSettings { + val default: JsonParserSettings = SettingsImpl() + + private case class SettingsImpl( + maxDepth: Int = 1000, + maxNumberCharacters: Int = 100 + ) extends JsonParserSettings { + override def withMaxDepth(newValue: Int): JsonParserSettings = copy(maxDepth = newValue) + override def withMaxNumberCharacters(newValue: Int): JsonParserSettings = copy(maxNumberCharacters = newValue) + } +}
\ No newline at end of file diff --git a/shared/src/main/scala/spray/json/JsonPrinter.scala b/shared/src/main/scala/spray/json/JsonPrinter.scala new file mode 100644 index 0000000..42cbab4 --- /dev/null +++ b/shared/src/main/scala/spray/json/JsonPrinter.scala @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2009-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 annotation.tailrec +import java.lang.{StringBuilder => JStringBuilder} + +/** + * A JsonPrinter serializes a JSON AST to a String. + */ +trait JsonPrinter extends (JsValue => String) { + + def apply(x: JsValue): String = apply(x, None) + + def apply(x: JsValue, + jsonpCallback: Option[String] = None, + sb: JStringBuilder = new JStringBuilder(256)): String = { + jsonpCallback match { + case Some(callback) => + sb.append(callback).append('(') + print(x, sb) + sb.append(')') + case None => print(x, sb) + } + sb.toString + } + + def print(x: JsValue, sb: JStringBuilder): Unit + + protected def printLeaf(x: JsValue, sb: JStringBuilder): Unit = { + x match { + case JsNull => sb.append("null") + case JsTrue => sb.append("true") + case JsFalse => sb.append("false") + case JsNumber(x) => sb.append(x) + case JsString(x) => printString(x, sb) + case _ => throw new IllegalStateException + } + } + + protected def printString(s: String, sb: JStringBuilder): Unit = { + import JsonPrinter._ + @tailrec def firstToBeEncoded(ix: Int = 0): Int = + if (ix == s.length) -1 else if (requiresEncoding(s.charAt(ix))) ix else firstToBeEncoded(ix + 1) + + sb.append('"') + firstToBeEncoded() match { + case -1 => sb.append(s) + case first => + sb.append(s, 0, first) + @tailrec def append(ix: Int): Unit = + if (ix < s.length) { + s.charAt(ix) match { + case c if !requiresEncoding(c) => sb.append(c) + case '"' => sb.append("\\\"") + case '\\' => sb.append("\\\\") + case '\b' => sb.append("\\b") + case '\f' => sb.append("\\f") + case '\n' => sb.append("\\n") + case '\r' => sb.append("\\r") + case '\t' => sb.append("\\t") + case x if x <= 0xF => sb.append("\\u000").append(Integer.toHexString(x)) + case x if x <= 0xFF => sb.append("\\u00").append(Integer.toHexString(x)) + case x if x <= 0xFFF => sb.append("\\u0").append(Integer.toHexString(x)) + case x => sb.append("\\u").append(Integer.toHexString(x)) + } + append(ix + 1) + } + append(first) + } + sb.append('"') + } + + protected def printSeq[A](iterable: Iterable[A], printSeparator: => Unit)(f: A => Unit): Unit = { + var first = true + iterable.foreach { a => + if (first) first = false else printSeparator + f(a) + } + } +} + +object JsonPrinter { + def requiresEncoding(c: Char): Boolean = + // from RFC 4627 + // unescaped = %x20-21 / %x23-5B / %x5D-10FFFF + c match { + case '"' => true + case '\\' => true + case c => c < 0x20 + } +}
\ No newline at end of file diff --git a/shared/src/main/scala/spray/json/PrettyPrinter.scala b/shared/src/main/scala/spray/json/PrettyPrinter.scala new file mode 100644 index 0000000..7526dab --- /dev/null +++ b/shared/src/main/scala/spray/json/PrettyPrinter.scala @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2009-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.StringBuilder +import annotation.tailrec + +/** + * A JsonPrinter that produces a nicely readable JSON source. + */ +trait PrettyPrinter extends JsonPrinter { + val Indent = 2 + + def print(x: JsValue, sb: StringBuilder): Unit = { + print(x, sb, 0) + } + + protected def print(x: JsValue, sb: StringBuilder, indent: Int): Unit = { + x match { + case JsObject(x) => printObject(x, sb, indent) + case JsArray(x) => printArray(x, sb, indent) + case _ => printLeaf(x, sb) + } + } + + protected def organiseMembers(members: Map[String, JsValue]): Seq[(String, JsValue)] = members.toSeq + + protected def printObject(members: Map[String, JsValue], sb: StringBuilder, indent: Int): Unit = { + sb.append("{\n") + printSeq(organiseMembers(members), sb.append(",\n")) { m => + printIndent(sb, indent + Indent) + printString(m._1, sb) + sb.append(": ") + print(m._2, sb, indent + Indent) + } + sb.append('\n') + printIndent(sb, indent) + sb.append("}") + } + + protected def printArray(elements: Seq[JsValue], sb: StringBuilder, indent: Int): Unit = { + sb.append('[') + printSeq(elements, sb.append(", "))(print(_, sb, indent)) + sb.append(']') + } + + protected def printIndent(sb: StringBuilder, indent: Int): Unit = { + @tailrec def rec(indent: Int): Unit = + if (indent > 0) { + sb.append(' ') + rec(indent - 1) + } + rec(indent) + } +} + +object PrettyPrinter extends PrettyPrinter
\ No newline at end of file diff --git a/shared/src/main/scala/spray/json/SortedPrinter.scala b/shared/src/main/scala/spray/json/SortedPrinter.scala new file mode 100644 index 0000000..28db225 --- /dev/null +++ b/shared/src/main/scala/spray/json/SortedPrinter.scala @@ -0,0 +1,25 @@ +/* + * 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 + +trait SortedPrinter extends PrettyPrinter { + + override protected def organiseMembers(members: Map[String, JsValue]): Seq[(String, JsValue)] = + members.toSeq.sortBy(_._1) +} + +object SortedPrinter extends SortedPrinter diff --git a/shared/src/main/scala/spray/json/StandardFormats.scala b/shared/src/main/scala/spray/json/StandardFormats.scala new file mode 100644 index 0000000..e59de64 --- /dev/null +++ b/shared/src/main/scala/spray/json/StandardFormats.scala @@ -0,0 +1,120 @@ +/* + * Original implementation (C) 2009-2011 Debasish Ghosh + * Adapted and extended in 2011 by 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 scala.{Left, Right} + +/** + * Provides the JsonFormats for the non-collection standard types. + */ +trait StandardFormats { + this: AdditionalFormats => + + private[json] type JF[T] = JsonFormat[T] // simple alias for reduced verbosity + + implicit def optionFormat[T :JF]: JF[Option[T]] = new OptionFormat[T] + + class OptionFormat[T :JF] extends JF[Option[T]] { + def write(option: Option[T]) = option match { + case Some(x) => x.toJson + case None => JsNull + } + def read(value: JsValue) = value match { + case JsNull => None + case x => Some(x.convertTo[T]) + } + // allows reading the JSON as a Some (useful in container formats) + def readSome(value: JsValue) = Some(value.convertTo[T]) + } + + implicit def eitherFormat[A :JF, B :JF] = new JF[Either[A, B]] { + def write(either: Either[A, B]) = either match { + case Right(a) => a.toJson + case Left(b) => b.toJson + } + def read(value: JsValue) = (value.convertTo(safeReader[A]), value.convertTo(safeReader[B])) match { + case (Right(a), _: Left[_, _]) => Left(a) + case (_: Left[_, _], Right(b)) => Right(b) + case (_: Right[_, _], _: Right[_, _]) => deserializationError("Ambiguous Either value: can be read as both, Left and Right, values") + case (Left(ea), Left(eb)) => deserializationError("Could not read Either value:\n" + ea + "---------- and ----------\n" + eb) + } + } + + implicit def tuple1Format[A :JF] = new JF[Tuple1[A]] { + def write(t: Tuple1[A]) = t._1.toJson + def read(value: JsValue) = Tuple1(value.convertTo[A]) + } + + implicit def tuple2Format[A :JF, B :JF] = new RootJsonFormat[(A, B)] { + def write(t: (A, B)) = JsArray(t._1.toJson, t._2.toJson) + def read(value: JsValue) = value match { + case JsArray(Seq(a, b)) => (a.convertTo[A], b.convertTo[B]) + case x => deserializationError("Expected Tuple2 as JsArray, but got " + x) + } + } + + implicit def tuple3Format[A :JF, B :JF, C :JF] = new RootJsonFormat[(A, B, C)] { + def write(t: (A, B, C)) = JsArray(t._1.toJson, t._2.toJson, t._3.toJson) + def read(value: JsValue) = value match { + case JsArray(Seq(a, b, c)) => (a.convertTo[A], b.convertTo[B], c.convertTo[C]) + case x => deserializationError("Expected Tuple3 as JsArray, but got " + x) + } + } + + implicit def tuple4Format[A :JF, B :JF, C :JF, D :JF] = new RootJsonFormat[(A, B, C, D)] { + def write(t: (A, B, C, D)) = JsArray(t._1.toJson, t._2.toJson, t._3.toJson, t._4.toJson) + def read(value: JsValue) = value match { + case JsArray(Seq(a, b, c, d)) => (a.convertTo[A], b.convertTo[B], c.convertTo[C], d.convertTo[D]) + case x => deserializationError("Expected Tuple4 as JsArray, but got " + x) + } + } + + implicit def tuple5Format[A :JF, B :JF, C :JF, D :JF, E :JF] = { + new RootJsonFormat[(A, B, C, D, E)] { + def write(t: (A, B, C, D, E)) = JsArray(t._1.toJson, t._2.toJson, t._3.toJson, t._4.toJson, t._5.toJson) + def read(value: JsValue) = value match { + case JsArray(Seq(a, b, c, d, e)) => + (a.convertTo[A], b.convertTo[B], c.convertTo[C], d.convertTo[D], e.convertTo[E]) + case x => deserializationError("Expected Tuple5 as JsArray, but got " + x) + } + } + } + + implicit def tuple6Format[A :JF, B :JF, C :JF, D :JF, E :JF, F: JF] = { + new RootJsonFormat[(A, B, C, D, E, F)] { + def write(t: (A, B, C, D, E, F)) = JsArray(t._1.toJson, t._2.toJson, t._3.toJson, t._4.toJson, t._5.toJson, t._6.toJson) + def read(value: JsValue) = value match { + case JsArray(Seq(a, b, c, d, e, f)) => + (a.convertTo[A], b.convertTo[B], c.convertTo[C], d.convertTo[D], e.convertTo[E], f.convertTo[F]) + case x => deserializationError("Expected Tuple6 as JsArray, but got " + x) + } + } + } + + implicit def tuple7Format[A :JF, B :JF, C :JF, D :JF, E :JF, F: JF, G: JF] = { + new RootJsonFormat[(A, B, C, D, E, F, G)] { + def write(t: (A, B, C, D, E, F, G)) = JsArray(t._1.toJson, t._2.toJson, t._3.toJson, t._4.toJson, t._5.toJson, t._6.toJson, t._7.toJson) + def read(value: JsValue) = value match { + case JsArray(Seq(a, b, c, d, e, f, g)) => + (a.convertTo[A], b.convertTo[B], c.convertTo[C], d.convertTo[D], e.convertTo[E], f.convertTo[F], g.convertTo[G]) + case x => deserializationError("Expected Tuple7 as JsArray, but got " + x) + } + } + } + +} diff --git a/shared/src/main/scala/spray/json/package.scala b/shared/src/main/scala/spray/json/package.scala new file mode 100644 index 0000000..a8a42f0 --- /dev/null +++ b/shared/src/main/scala/spray/json/package.scala @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2009-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 + +package object json { + + type JsField = (String, JsValue) + + def deserializationError(msg: String, cause: Throwable = null, fieldNames: List[String] = Nil) = throw new DeserializationException(msg, cause, fieldNames) + def serializationError(msg: String) = throw new SerializationException(msg) + + def jsonReader[T](implicit reader: JsonReader[T]) = reader + def jsonWriter[T](implicit writer: JsonWriter[T]) = writer + + implicit def enrichAny[T](any: T) = new RichAny(any) + implicit def enrichString(string: String) = new RichString(string) + + @deprecated("use enrichAny", "1.3.4") + def pimpAny[T](any: T) = new PimpedAny(any) + @deprecated("use enrichString", "1.3.4") + def pimpString(string: String) = new PimpedString(string) +} + +package json { + + case class DeserializationException(msg: String, cause: Throwable = null, fieldNames: List[String] = Nil) extends RuntimeException(msg, cause) + class SerializationException(msg: String) extends RuntimeException(msg) + + private[json] class RichAny[T](any: T) { + def toJson(implicit writer: JsonWriter[T]): JsValue = writer.write(any) + } + + private[json] class RichString(string: String) { + @deprecated("deprecated in favor of parseJson", "1.2.6") + def asJson: JsValue = parseJson + def parseJson: JsValue = JsonParser(string) + def parseJson(settings: JsonParserSettings): JsValue = JsonParser(string, settings) + } + + @deprecated("use RichAny", "1.3.4") + private[json] class PimpedAny[T](any: T) { + def toJson(implicit writer: JsonWriter[T]): JsValue = writer.write(any) + } + + @deprecated("use RichString", "1.3.4") + private[json] class PimpedString(string: String) { + @deprecated("deprecated in favor of parseJson", "1.2.6") + def asJson: JsValue = parseJson + def parseJson: JsValue = JsonParser(string) + } + +} |