diff options
35 files changed, 803 insertions, 455 deletions
@@ -1,6 +1,4 @@ /.idea/ -/project/boot/ -/project/plugins/project target/ lib_managed/ src_managed/ @@ -1,3 +1,16 @@ +Version 1.1 (2012-02-01) +------------------------ +- Added automatic case class field name extraction via new jsonFormatX overloads +- Added 'asJson' pimp to Strings +- Added RootJsonFormat (JsonFormat for types corresponding to JSON document roots) +- Fixed problem of JSON object deserialization not being member-order independent + (removed JsField, turned JsObject(List[JsField]) into JsObject(Map[String, JsValue])) +- Fixed issue #8 (Allow (de)serialization of NaN (Double)), thx to @stefritz +- Fixed #6 (rename JsValue:fromJson to 'convertTo', add .prettyPrint and .compactPrint) +- Improved deserialization error messages +- Upgraded to SBT 0.11.2 + + Version 1.0.1 (2011-10-04) -------------------------- - Upgraded to Scala 2.9.1 and parboiled 1.0.2 @@ -7,6 +20,7 @@ Version 1.0.1 (2011-10-04) - Improved documentation - Smaller fixes and additions + Version 1.0.0 (2011-07-18) -------------------------- first public release diff --git a/README.markdown b/README.markdown index fcf4ba9..38c0739 100644 --- a/README.markdown +++ b/README.markdown @@ -3,22 +3,23 @@ _spray-json_ is a lightweight, clean and efficient [JSON] implementation in Scal It sports the following features: * Simple immutable model of the JSON language elements -* An efficient JSON PEG parser (implemented with [parboiled]) +* An efficient JSON PEG parser (implemented with [parboiled][]) * Choice of either compact or pretty JSON-to-string printing * Type-class based (de)serialization of custom objects (no reflection, no intrusion) ### Installation -_spray-json_ is available from the [scala-tools.org] repositories. -The latest release is `1.0.1` and is built against Scala 2.9.1. +_spray-json_ is available from the [repo.spray.cc] repository. +The latest release is `1.1.0` and is built against Scala 2.9.1. -If you use SBT (0.7.x) you can include _spray-json_ in your project with +If you use SBT you can include _spray-json_ in your project with - val sprayJson = "cc.spray.json" %% "spray-json" % "1.0.1" % "compile" withSources() + "cc.spray" %% "spray-json" % "1.1.0" -_spray-json_ has only one dependency: the parsing library [parboiled] -(which is also a dependency of _spray-server_ and _spray-client_, so if you use _spray-json_ with either of them you are not incurring any additional dependency). +_spray-json_ has only one dependency: the parsing library [parboiled][] +(which is also a dependency of _spray-server_ and _spray-client_, so if you use _spray-json_ with either of them you +are not incurring any additional dependency). ### Usage @@ -30,22 +31,22 @@ Just bring all relevant elements in scope with and do one or more of the following: -1. Parse a JSON string into it's Abstract Syntax Tree (AST) representation +1. Parse a JSON string into its Abstract Syntax Tree (AST) representation - val json = """{ "some": "JSON source" }""" - val jsonAst = JsonParser(json) + val source = """{ "some": "JSON source" }""" + val jsonAst = source.asJson // or JsonParser(source) 2. Print a JSON AST back to a String using either the `CompactPrinter` or the `PrettyPrinter` - val json = PrettyPrinter(jsonAst) + val json = jsonAst.prettyPrint // or .compactPrint 3. Convert any Scala object to a JSON AST using the pimped `toJson` method val jsonAst = List(1, 2, 3).toJson -4. Convert a JSON AST to a Scala object with the `fromJson` method +4. Convert a JSON AST to a Scala object with the `convertTo` method - val myObject = jsonAst.fromJson[MyObjectType] + val myObject = jsonAst.convertTo[MyObjectType] In order to make steps 3 and 4 work for an object of type `T` you need to bring implicit values in scope that provide `JsonFormat[T]` instances for `T` and all types used by `T` (directly or indirectly). @@ -54,16 +55,24 @@ The way you normally do this is via a "JsonProtocol". ### JsonProtocol -_spray-json_ uses [SJSON]s Scala-idiomatic type-class-based approach to connect an existing type `T` with the logic how to (de)serialize its instances to and from JSON. (In fact _spray-json_ even reuses some of [SJSON]s code, see the 'Credits' section below). +_spray-json_ uses [SJSON]s Scala-idiomatic type-class-based approach to connect an existing type `T` with the logic how +to (de)serialize its instances to and from JSON. (In fact _spray-json_ even reuses some of [SJSON]s code, see the +'Credits' section below). This approach has the advantage of not requiring any change (or even access) to `T`s source code. All (de)serialization -logic is attached 'from the outside'. There is no reflection involved, so the resulting conversions are fast. Scalas excellent type inference reduces verbosity and boilerplate to a minimum, while the Scala compiler will make sure at compile time that you provided all required (de)serialization logic. +logic is attached 'from the outside'. There is no reflection involved, so the resulting conversions are fast. Scalas +excellent type inference reduces verbosity and boilerplate to a minimum, while the Scala compiler will make sure at +compile time that you provided all required (de)serialization logic. -In _spray-jsons_ terminology a 'JsonProtocol' is nothing but a bunch of implicit values of type `JsonFormat[T]`, whereby each `JsonFormat[T]` contains the logic of how to convert instance of `T` to and from JSON. All `JsonFormat[T]`s of a protocol need to be "mece" (mutually exclusive, collectively exhaustive), i.e. they are not allowed to overlap and together need to span all types required by the application. +In _spray-jsons_ terminology a 'JsonProtocol' is nothing but a bunch of implicit values of type `JsonFormat[T]`, whereby +each `JsonFormat[T]` contains the logic of how to convert instance of `T` to and from JSON. All `JsonFormat[T]`s of a +protocol need to be "mece" (mutually exclusive, collectively exhaustive), i.e. they are not allowed to overlap and +together need to span all types required by the application. This may sound more complicated than it is. -_spray-json_ comes with a `DefaultJsonProtocol`, which already covers all of Scalas value types as well as the most important reference and collection types. As long as your code uses nothing more than these you only need the `DefaultJsonProtocol`. -Here are the types already taken care of by the `DefaultJsonProtocol`: +_spray-json_ comes with a `DefaultJsonProtocol`, which already covers all of Scalas value types as well as the most +important reference and collection types. As long as your code uses nothing more than these you only need the +`DefaultJsonProtocol`. Here are the types already taken care of by the `DefaultJsonProtocol`: * Byte, Short, Int, Long, Float, Double, Char, Unit, Boolean * String, Symbol @@ -74,7 +83,8 @@ Here are the types already taken care of by the `DefaultJsonProtocol`: * collection.{Iterable, Seq, IndexedSeq, LinearSeq, Set} * JsValue -In most cases however you'll also want to convert types not covered by the `DefaultJsonProtocol`. In these cases you need to provide `JsonFormat[T]`s for your custom types. This is not hard at all. +In most cases however you'll also want to convert types not covered by the `DefaultJsonProtocol`. In these cases you +need to provide `JsonFormat[T]`s for your custom types. This is not hard at all. ### Providing JsonFormats for Case Classes @@ -84,37 +94,52 @@ If your custom type `T` is a case class then augmenting the `DefaultJsonProtocol case class Color(name: String, red: Int, green: Int, blue: Int) object MyJsonProtocol extends DefaultJsonProtocol { - implicit val colorFormat = jsonFormat(Color, "name", "red", "green", "blue") + implicit val colorFormat = jsonFormat4(Color) } import MyJsonProtocol._ val json = Color("CadetBlue", 95, 158, 160).toJson - val color = json.fromJson[Color] + val color = json.convertTo[Color] -The `jsonFormat` method reduces the boilerplate to a minimum, just pass it the companion object of your case class as -well as the field names (in order) and it will return a ready-to-use `JsonFormat` for your type. -There is one quirk though: If you explicitly declare the companion object for your case class the notation above will +The `jsonFormatX` methods reduce the boilerplate to a minimum, just pass the right one the companion object of your +case class and it will return a ready-to-use `JsonFormat` for your type (the right one is the one matching the number +of arguments to your case class constructor, e.g. if your case class has 13 fields you need to use the `jsonFormat13` +method). The `jsonFormatX` methods try to extract the field names of your case class before calling the more general +`jsonFormat` overloads, which let you specify the field name manually. So, if spray-json has trouble determining the +field names or if your JSON objects use member names that differ from the case class fields you can also use +`jsonFormat` directly. + +There is one additional quirk: If you explicitly declare the companion object for your case class the notation above will stop working. You'll have to explicitly refer to the companion objects `apply` method to fix this: case class Color(name: String, red: Int, green: Int, blue: Int) object Color object MyJsonProtocol extends DefaultJsonProtocol { - implicit val colorFormat = jsonFormat(Color.apply, "name", "red", "green", "blue") + implicit val colorFormat = jsonFormat4(Color.apply) } -If your case class is generic in that it takes type parameters itself the `jsonFormat` method can also help you. +If your case class is generic in that it takes type parameters itself the `jsonFormat` methods can also help you. However, there is a little more boilerplate required as you need to add context bounds for all type parameters and explicitly refer to the case classes `apply` method as in this example: case class NamedList[A](name: String, items: List[A]) object MyJsonProtocol extends DefaultJsonProtocol { - implicit def namedListFormat[A :JsonFormat] = jsonFormat(NamedList.apply[A], "name", "items") + implicit def namedListFormat[A :JsonFormat] = jsonFormat2(NamedList.apply[A]) } +#### NullOptions + +The `NullOptions` 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`.) + + ### Providing JsonFormats for other Types Of course you can also supply (de)serialization logic for types that aren't case classes. @@ -124,14 +149,13 @@ Here is one way to do it: object MyJsonProtocol extends DefaultJsonProtocol { implicit object ColorJsonFormat extends JsonFormat[Color] { - def write(c: Color) = { + def write(c: Color) = JsArray(JsString(c.name), JsNumber(c.red), JsNumber(c.green), JsNumber(c.blue)) - } + def read(value: JsValue) = value match { - case JsArray(JsString(name) :: JsNumber(red) :: JsNumber(green) :: JsNumber(blue) :: Nil) => { + case JsArray(JsString(name) :: JsNumber(red) :: JsNumber(green) :: JsNumber(blue) :: Nil) => new Color(name, red.toInt, green.toInt, blue.toInt) - } - case _ => throw new DeserializationException("Color expected") + case _ => deserializationError("Color expected") } } } @@ -139,37 +163,47 @@ Here is one way to do it: import MyJsonProtocol._ val json = Color("CadetBlue", 95, 158, 160).toJson - val color = json.fromJson[Color] + val color = json.convertTo[Color] This serializes `Color` instances as a JSON array, which is compact but does not make the elements semantics explicit. You need to know that the color components are ordered "red, green, blue". Another way would be to serialize `Color`s as JSON objects: - object MyJsonProtocol extends DefaultJsonProtocol { + object MyJsonProtocol extends DefaultJsonProtocol { implicit object ColorJsonFormat extends JsonFormat[Color] { def write(c: Color) = JsObject( - JsField("name", c.name), - JsField("red", c.red), - JsField("green", c.green), - JsField("blue", c.blue) + "name" -> JsString(c.name), + "red" -> JsNumber(c.red), + "green" -> JsNumber(c.green), + "blue" -> JsNumber(c.blue) ) - def read(value: JsValue) = value match { - case JsObject( - JsField("name", JsString(name)), - JsField("red", JsNumber(red)), - JsField("green", JsNumber(green)), - JsField("blue", JsNumber(blue)) - ) => { - new Color(name.value, red.value.toInt, green.value.toInt, blue.value.toInt) + def read(value: JsValue) = { + value.asJsObject.getFields("name", "red", "green", "blue") match { + case Seq(JsString(name), JsNumber(red), JsNumber(green), JsNumber(blue)) => + new Color(name, red.toInt, green.toInt, blue.toInt) + case _ => throw new DeserializationException("Color expected") } - case _ => throw new DeserializationException("Color expected") } } } -This is a bit more verbose in its definition and the resulting JSON but transports the field semantics over to the JSON side. -Note that this is the approach _spray-json_ uses for case classes. +This is a bit more verbose in its definition and the resulting JSON but transports the field semantics over to the +JSON side. Note that this is the approach _spray-json_ uses for case classes. + + +### JsonFormats for recursive Types + +If your type is recursive such as + + case class Foo(i: Int, foo: Foo) + +you need to wrap your format constructor with `lazyFormat` and supply an explicit type annotation: + + implicit val fooFormat: JsonFormat[Foo] = lazyFormat(jsonFormat(Foo, "i", "foo")) + +Otherwise your code will either not compile (no explicit type annotation) or throw an NPE at runtime (no `lazyFormat` +wrapper). ### API Documentation @@ -200,7 +234,7 @@ _spray-json_ project under the project’s open source license. [JSON]: http://json.org [parboiled]: http://parboiled.org - [scala-tools.org]: http://scala-tools.org + [repo.spray.cc]: http://repo.spray.cc [SJSON]: https://github.com/debasishg/sjson [Databinder-Dispatch]: https://github.com/n8han/Databinder-Dispatch [APL 2.0]: http://www.apache.org/licenses/LICENSE-2.0 diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..73e4f98 --- /dev/null +++ b/build.sbt @@ -0,0 +1,49 @@ +name := "spray-json" + +version := "1.1.0" + +organization := "cc.spray" + +organizationHomepage := Some(new URL("http://spray.cc")) + +description := "A Scala library for easy and idiomatic JSON (de)serialization" + +homepage := Some(new URL("https://github.com/spray/spray-json")) + +startYear := Some(2011) + +licenses := Seq("Apache 2" -> new URL("http://www.apache.org/licenses/LICENSE-2.0.txt")) + +scalaVersion := "2.9.1" + +scalacOptions := Seq("-deprecation", "-encoding", "utf8") + +libraryDependencies ++= Seq( + "org.parboiled" % "parboiled-scala" % "1.0.2" % "compile", + "org.specs2" %% "specs2" % "1.6.1" % "test" +) + +scaladocOptions <<= (name, version).map { (n, v) => Seq("-doc-title", n + " " + v) } + +credentials += Credentials(Path.userHome / ".ivy2" / ".credentials") + +publishMavenStyle := true + +publishTo <<= version { version => + Some { + "spray repo" at { + // public uri is repo.spray.cc, we use an SSH tunnel to the nexus here + "http://localhost:42424/content/repositories/" + { + if (version.trim.endsWith("SNAPSHOT")) "snapshots/" else"releases/" + } + } + } +} + +seq(lsSettings:_*) + +(LsKeys.tags in LsKeys.lsync) := Seq("json") + +(LsKeys.docsUrl in LsKeys.lsync) := Some(new URL("http://spray.github.com/spray/api/spray-json/")) + +(externalResolvers in LsKeys.lsync) := Seq("spray repo" at "http://repo.spray.cc")
\ No newline at end of file diff --git a/notes/1.0.1.markdown b/notes/1.0.1.markdown index 78ad0a4..dfe4905 100644 --- a/notes/1.0.1.markdown +++ b/notes/1.0.1.markdown @@ -4,6 +4,3 @@ - Improved compile time error messages with `@implicitNotFound` annotations - Improved documentation - Smaller fixes and additions - - [JSON]: http://json.org - [parboiled]: http://parboiled.org diff --git a/notes/1.1.0.markdown b/notes/1.1.0.markdown new file mode 100644 index 0000000..2d09720 --- /dev/null +++ b/notes/1.1.0.markdown @@ -0,0 +1,14 @@ +Starting with this release _spray-json_ artifacts live on <http://repo.spray.cc/> +and will have group id `cc.spray` rather than `cc.spray.json` as before. + +Changes since the last release (1.0.1): + +- Added automatic case class field name extraction via new `jsonFormatX` overloads +- Added `asJson` pimp to Strings +- Added `RootJsonFormat` (`JsonFormat` for types corresponding to JSON document roots) +- Fixed problem of JSON object deserialization not being member-order independent + (removed `JsField`, turned `JsObject(List[JsField])` into `JsObject(Map[String, JsValue])`) +- Fixed issue #8 (Allow (de)serialization of `NaN` (Double)), thx to @stefritz +- Fixed #6 (rename `JsValue:fromJson` to `convertTo`, add `.prettyPrint` and `.compactPrint`) +- Improved deserialization error messages +- Upgraded to SBT 0.11.2 diff --git a/project/build.properties b/project/build.properties index fef6e3c..fdfc61f 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,6 +1 @@ -project.organization=cc.spray.json -project.name=spray-json -sbt.version=0.7.7 -project.version=1.0.1 -build.scala.versions=2.9.1 -project.initialize=false +sbt.version=0.11.2
\ No newline at end of file diff --git a/project/build/Project.scala b/project/build/Project.scala deleted file mode 100644 index 081633e..0000000 --- a/project/build/Project.scala +++ /dev/null @@ -1,79 +0,0 @@ -import sbt._ -import Process._ - -class Project(info: ProjectInfo) extends DefaultProject(info) with posterous.Publish { - - // ------------------------------------------------------------------------------------------------------------------- - // All repositories *must* go here! See ModuleConfigurations below. - // ------------------------------------------------------------------------------------------------------------------- - object Repositories { - // e.g. val AkkaRepo = MavenRepository("Akka Repository", "http://akka.io/repository") - } - - // ------------------------------------------------------------------------------------------------------------------- - // ModuleConfigurations - // Every dependency that cannot be resolved from the built-in repositories (Maven Central and Scala Tools Releases) - // must be resolved from a ModuleConfiguration. This will result in a significant acceleration of the update action. - // Therefore, if repositories are defined, this must happen as def, not as val. - // ------------------------------------------------------------------------------------------------------------------- - import Repositories._ - - // ------------------------------------------------------------------------------------------------------------------- - // Dependencies - // ------------------------------------------------------------------------------------------------------------------- - val parboiledC = "org.parboiled" % "parboiled-core" % "1.0.2" % "compile" withSources() - val parboiledS = "org.parboiled" % "parboiled-scala" % "1.0.2" % "compile" withSources() - - // ------------------------------------------------------------------------------------------------------------------- - // Testing with Specs2 - // ------------------------------------------------------------------------------------------------------------------- - val specs2 = "org.specs2" %% "specs2" % "1.6.1" % "test" withSources() - - def specs2Framework = new TestFramework("org.specs2.runner.SpecsFramework") - override def testFrameworks = super.testFrameworks ++ Seq(specs2Framework) - - // ------------------------------------------------------------------------------------------------------------------- - // Options - // ------------------------------------------------------------------------------------------------------------------- - override def compileOptions = super.compileOptions ++ Seq("-deprecation", "-encoding", "utf8").map(CompileOption(_)) - override def documentOptions: Seq[ScaladocOption] = documentTitle(name + " " + version) :: Nil - - // ------------------------------------------------------------------------------------------------------------------- - // Publishing - // ------------------------------------------------------------------------------------------------------------------- - //val publishTo = "Scala Tools Snapshots" at "http://nexus.scala-tools.org/content/repositories/snapshots/" - val publishTo = "Scala Tools Releases" at "http://nexus.scala-tools.org/content/repositories/releases/" - - Credentials(Path.userHome / ".ivy2" / ".credentials", log) - override def managedStyle = ManagedStyle.Maven - override def packageDocsJar = defaultJarPath("-scaladoc.jar") - override def packageSrcJar = defaultJarPath("-sources.jar") - val sourceArtifact = Artifact(artifactID, "src", "jar", Some("sources"), Nil, None) - val docsArtifact = Artifact(artifactID, "docs", "jar", Some("scaladoc"), Nil, None) - override def packageToPublishActions = super.packageToPublishActions ++ Seq(packageDocs, packageSrc) - - override def pomExtra = ( - <name>spray JSON</name> - <url>http://spray.cc/</url> - <inceptionYear>2011</inceptionYear> - <description>A Scala library for easy and idiomatic JSON (de)serialization</description> - <licenses> - <license> - <name>Apache 2</name> - <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> - <distribution>repo</distribution> - </license> - </licenses> - <developers> - <developer> - <id>sirthias</id> - <name>Mathias Doenitz</name> - <timezone>+1</timezone> - <email>mathias [at] spray.cc</email> - </developer> - </developers> - <scm> - <url>http://github.com/spray/spray-json/</url> - </scm> - ) -} diff --git a/project/idea.properties b/project/idea.properties deleted file mode 100644 index 6716523..0000000 --- a/project/idea.properties +++ /dev/null @@ -1,2 +0,0 @@ -include.sbt.project.definition.module = false -excluded.folders = .idea,lib_managed,target,test-output
\ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..9660fcf --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,6 @@ +addSbtPlugin("me.lessis" % "ls-sbt" % "0.1.1") + +resolvers ++= Seq( + "less is" at "http://repo.lessis.me", + "coda" at "http://repo.codahale.com" +)
\ No newline at end of file diff --git a/project/plugins/Plugins.scala b/project/plugins/Plugins.scala deleted file mode 100644 index a6b0cfd..0000000 --- a/project/plugins/Plugins.scala +++ /dev/null @@ -1,26 +0,0 @@ -import sbt._ - -class Plugins(info: ProjectInfo) extends PluginDefinition(info) { - - // ------------------------------------------------------------------------------------------------------------------- - // All repositories *must* go here! See ModuleConigurations below. - // ------------------------------------------------------------------------------------------------------------------- - object Repositories { - // e.g. val akkaRepo = MavenRepository("Akka Repository", "http://akka.io/repository") - } - - // ------------------------------------------------------------------------------------------------------------------- - // ModuleConfigurations - // Every dependency that cannot be resolved from the built-in repositories (Maven Central and Scala Tools Releases) - // must be resolved from a ModuleConfiguration. This will result in a significant acceleration of the update action. - // Therefore, if repositories are defined, this must happen as def, not as val. - // ------------------------------------------------------------------------------------------------------------------- - import Repositories._ - val posterousSbtModuleConfig = ModuleConfiguration("net.databinder", ScalaToolsReleases) - - // ------------------------------------------------------------------------------------------------------------------- - // Plugins - // ------------------------------------------------------------------------------------------------------------------- - - val posterousSbt = "net.databinder" % "posterous-sbt" % "0.1.7" -} diff --git a/src/main/ls/1.1.0.json b/src/main/ls/1.1.0.json new file mode 100644 index 0000000..9504e42 --- /dev/null +++ b/src/main/ls/1.1.0.json @@ -0,0 +1,22 @@ + +{ + "organization":"cc.spray", + "name":"spray-json", + "version":"1.1.0", + "description":"A Scala library for easy and idiomatic JSON (de)serialization", + "site":"https://github.com/spray/spray-json", + "tags":["json"], + "docs":"http://spray.github.com/spray/api/spray-json/", + "licenses": [{ + "name": "Apache 2", + "url": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }], + "resolvers": ["http://repo.spray.cc"], + "dependencies": [{ + "organization":"org.parboiled", + "name": "parboiled-scala", + "version": "1.0.2" + }], + "scalas": ["2.9.1"], + "sbt": false +}
\ No newline at end of file diff --git a/src/main/scala/cc/spray/json/AdditionalFormats.scala b/src/main/scala/cc/spray/json/AdditionalFormats.scala index db9d3ba..889fdaf 100644 --- a/src/main/scala/cc/spray/json/AdditionalFormats.scala +++ b/src/main/scala/cc/spray/json/AdditionalFormats.scala @@ -36,6 +36,12 @@ trait AdditionalFormats { } /** + * 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] { @@ -45,6 +51,12 @@ trait AdditionalFormats { } /** + * 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] { @@ -54,21 +66,35 @@ trait AdditionalFormats { } /** + * 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]{ + 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.fromJson) + Right(json.convertTo) } catch { case e: Exception => Left(e) } diff --git a/src/main/scala/cc/spray/json/BasicFormats.scala b/src/main/scala/cc/spray/json/BasicFormats.scala index e16b4ca..0389e3a 100644 --- a/src/main/scala/cc/spray/json/BasicFormats.scala +++ b/src/main/scala/cc/spray/json/BasicFormats.scala @@ -26,7 +26,7 @@ trait BasicFormats { def write(x: Int) = JsNumber(x) def read(value: JsValue) = value match { case JsNumber(x) => x.intValue - case _ => throw new DeserializationException("Int expected") + case x => deserializationError("Expected Int as JsNumber, but got " + x) } } @@ -34,7 +34,7 @@ trait BasicFormats { def write(x: Long) = JsNumber(x) def read(value: JsValue) = value match { case JsNumber(x) => x.longValue - case _ => throw new DeserializationException("Long expected") + case x => deserializationError("Expected Long as JsNumber, but got " + x) } } @@ -42,7 +42,8 @@ trait BasicFormats { def write(x: Float) = JsNumber(x) def read(value: JsValue) = value match { case JsNumber(x) => x.floatValue - case _ => throw new DeserializationException("Float expected") + case JsNull => Float.NaN + case x => deserializationError("Expected Float as JsNumber, but got " + x) } } @@ -50,7 +51,8 @@ trait BasicFormats { def write(x: Double) = JsNumber(x) def read(value: JsValue) = value match { case JsNumber(x) => x.doubleValue - case _ => throw new DeserializationException("Double expected") + case JsNull => Double.NaN + case x => deserializationError("Expected Double as JsNumber, but got " + x) } } @@ -58,7 +60,7 @@ trait BasicFormats { def write(x: Byte) = JsNumber(x) def read(value: JsValue) = value match { case JsNumber(x) => x.byteValue - case _ => throw new DeserializationException("Byte expected") + case x => deserializationError("Expected Byte as JsNumber, but got " + x) } } @@ -66,7 +68,7 @@ trait BasicFormats { def write(x: Short) = JsNumber(x) def read(value: JsValue) = value match { case JsNumber(x) => x.shortValue - case _ => throw new DeserializationException("Short expected") + case x => deserializationError("Expected Short as JsNumber, but got " + x) } } @@ -74,7 +76,7 @@ trait BasicFormats { def write(x: BigDecimal) = JsNumber(x) def read(value: JsValue) = value match { case JsNumber(x) => x - case _ => throw new DeserializationException("String expected") + case x => deserializationError("Expected BigDecimal as JsNumber, but got " + x) } } @@ -82,7 +84,7 @@ trait BasicFormats { def write(x: BigInt) = JsNumber(x) def read(value: JsValue) = value match { case JsNumber(x) => x.toBigInt - case _ => throw new DeserializationException("BigInt expected") + case x => deserializationError("Expected BigInt as JsNumber, but got " + x) } } @@ -96,7 +98,7 @@ trait BasicFormats { def read(value: JsValue) = value match { case JsTrue => true case JsFalse => false - case _ => throw new DeserializationException("Boolean expected") + case x => deserializationError("Expected JsBoolean, but got " + x) } } @@ -104,7 +106,7 @@ trait BasicFormats { 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 _ => throw new DeserializationException("Char expected") + case x => deserializationError("Expected Char as single-character JsString, but got " + x) } } @@ -112,7 +114,7 @@ trait BasicFormats { def write(x: String) = JsString(x) def read(value: JsValue) = value match { case JsString(x) => x - case _ => throw new DeserializationException("String expected") + case x => deserializationError("Expected String as JsString, but got " + x) } } @@ -120,8 +122,8 @@ trait BasicFormats { def write(x: Symbol) = JsString(x.name) def read(value: JsValue) = value match { case JsString(x) => Symbol(x) - case _ => throw new DeserializationException("Symbol expected") + case x => deserializationError("Expected Symbol as JsString, but got " + x) } } -}
\ No newline at end of file +} diff --git a/src/main/scala/cc/spray/json/CollectionFormats.scala b/src/main/scala/cc/spray/json/CollectionFormats.scala index 07592a8..93e41ab 100644 --- a/src/main/scala/cc/spray/json/CollectionFormats.scala +++ b/src/main/scala/cc/spray/json/CollectionFormats.scala @@ -22,22 +22,22 @@ trait CollectionFormats { /** * Supplies the JsonFormat for Lists. */ - implicit def listFormat[T :JsonFormat] = new JsonFormat[List[T]] { + implicit def listFormat[T :JsonFormat] = new RootJsonFormat[List[T]] { def write(list: List[T]) = JsArray(list.map(_.toJson)) def read(value: JsValue) = value match { - case JsArray(elements) => elements.map(_.fromJson[T]) - case _ => throw new DeserializationException("List expected") + case JsArray(elements) => elements.map(_.convertTo[T]) + case x => deserializationError("Expected List as JsArray, but got " + x) } } /** * Supplies the JsonFormat for Arrays. */ - implicit def arrayFormat[T :JsonFormat :ClassManifest] = new JsonFormat[Array[T]] { + implicit def arrayFormat[T :JsonFormat :ClassManifest] = new RootJsonFormat[Array[T]] { def write(array: Array[T]) = JsArray(array.map(_.toJson).toList) def read(value: JsValue) = value match { - case JsArray(elements) => elements.map(_.fromJson[T]).toArray[T] - case _ => throw new DeserializationException("Array expected") + case JsArray(elements) => elements.map(_.convertTo[T]).toArray[T] + case x => deserializationError("Expected Array as JsArray, but got " + x) } } @@ -45,18 +45,20 @@ trait CollectionFormats { * Supplies the JsonFormat for Maps. The implicitly available JsonFormat for the key type K must * always write JsStrings, otherwise a [[cc.spray.json.SerializationException]] will be thrown. */ - implicit def mapFormat[K :JsonFormat, V :JsonFormat] = new JsonFormat[Map[K, V]] { + implicit def mapFormat[K :JsonFormat, V :JsonFormat] = new RootJsonFormat[Map[K, V]] { def write(m: Map[K, V]) = JsObject { - m.toList.map { t => - t._1.toJson match { - case JsString(x) => JsField(x, t._2.toJson) + 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 JsObject(fields) => fields.map(field => (JsString(field.name).fromJson[K], field.value.fromJson[V])).toMap - case _ => throw new DeserializationException("Map expected") + case x: JsObject => x.fields.map { field => + (JsString(field._1).convertTo[K], field._2.convertTo[V]) + } (collection.breakOut) + case x => deserializationError("Expected Map as JsObject, but got " + x) } } @@ -81,11 +83,11 @@ trait CollectionFormats { * A JsonFormat construction helper that creates a JsonFormat for an Iterable type I from a builder function * List => I. */ - def viaList[I <: Iterable[T], T :JsonFormat](f: List[T] => I): JsonFormat[I] = new JsonFormat[I] { + def viaList[I <: Iterable[T], T :JsonFormat](f: List[T] => I): RootJsonFormat[I] = new RootJsonFormat[I] { def write(iterable: I) = JsArray(iterable.map(_.toJson).toList) def read(value: JsValue) = value match { - case JsArray(elements) => f(elements.map(_.fromJson[T])) - case _ => throw new DeserializationException("Collection expected") + case JsArray(elements) => f(elements.map(_.convertTo[T])) + case x => deserializationError("Expected Collection as JsArray, but got " + x) } } diff --git a/src/main/scala/cc/spray/json/CompactPrinter.scala b/src/main/scala/cc/spray/json/CompactPrinter.scala index 329c08e..467160d 100644 --- a/src/main/scala/cc/spray/json/CompactPrinter.scala +++ b/src/main/scala/cc/spray/json/CompactPrinter.scala @@ -31,12 +31,12 @@ trait CompactPrinter extends JsonPrinter { } } - private def printObject(members: List[JsField], sb: StringBuilder) { + private def printObject(members: Map[String, JsValue], sb: StringBuilder) { sb.append('{') printSeq(members, sb.append(',')) { m => - printString(m.name, sb) + printString(m._1, sb) sb.append(':') - print(m.value, sb) + print(m._2, sb) } sb.append('}') } diff --git a/src/main/scala/cc/spray/json/DeserializationException.scala b/src/main/scala/cc/spray/json/DeserializationException.scala deleted file mode 100644 index 283d996..0000000 --- a/src/main/scala/cc/spray/json/DeserializationException.scala +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 cc.spray.json - -class DeserializationException(msg: String) extends RuntimeException(msg)
\ No newline at end of file diff --git a/src/main/scala/cc/spray/json/JsValue.scala b/src/main/scala/cc/spray/json/JsValue.scala index 86dbb06..d14312b 100644 --- a/src/main/scala/cc/spray/json/JsValue.scala +++ b/src/main/scala/cc/spray/json/JsValue.scala @@ -18,76 +18,44 @@ package cc.spray.json -import collection.mutable.ListBuffer +import collection.immutable.ListMap + /** * The general type of a JSON AST node. */ -sealed trait JsValue { - override def toString = CompactPrinter(this) +sealed abstract class JsValue { + override def toString = compactPrint def toString(printer: (JsValue => String)) = printer(this) - def fromJson[T :JsonReader]: T = jsonReader[T].read(this) -} -object JsValue { + def compactPrint = CompactPrinter(this) + def prettyPrint = PrettyPrinter(this) + def convertTo[T :JsonReader]: T = jsonReader[T].read(this) /** - * General converter to a JsValue. - * Throws an IllegalArgumentException if the given value cannot be converted. + * Returns `this` if this JsValue is a JsObject, otherwise throws a DeserializationException with the given error msg. */ - def apply(value: Any): JsValue = value match { - case null => JsNull - case true => JsTrue - case false => JsFalse - case x: JsValue => x - case x: String => JsString(x) - case x: Int => JsNumber(x) - case x: Long => JsNumber(x) - case x: Double => JsNumber(x) - case x: Char => JsString(String.valueOf(x)) - case x: Float => JsNumber(x) - case x: Byte => JsNumber(x) - case x: Short => JsNumber(x) - case x: BigInt => JsNumber(x) - case x: BigDecimal => JsNumber(x) - case x: Symbol => JsString(x.name) - case x: collection.Map[_, _] => JsObject(fromSeq(x)) - case x@ collection.Seq((_, _), _*) => JsObject(fromSeq(x.asInstanceOf[Seq[(_, _)]])) - case x: collection.Seq[_] => JsArray(x.toList.map(JsValue.apply)) - case x => throw new IllegalArgumentException(x.toString + " cannot be converted to a JsValue") - } + def asJsObject(errorMsg: String = "JSON object expected"): JsObject = deserializationError(errorMsg) - private def fromSeq(seq: Iterable[(_, _)]) = { - val list = ListBuffer.empty[JsField] - seq.foreach { - case (key: String, value) => list += JsField(key, JsValue(value)) - case (key: Symbol, value) => list += JsField(key.name, JsValue(value)) - case (key: JsString, value) => list += JsField(key.value, JsValue(value)) - case (x, _) => throw new IllegalArgumentException(x.toString + " cannot be converted to a JsString") - } - list.toList - } + /** + * 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: List[JsField]) extends JsValue { - lazy val asMap: Map[String, JsValue] = { - val b = Map.newBuilder[String, JsValue] - for (JsField(name, value) <- fields) b += ((name, value)) - b.result() - } +case class JsObject(fields: Map[String, JsValue]) extends JsValue { + override def asJsObject(errorMsg: String) = this + def getFields(fieldNames: String*): Seq[JsValue] = fieldNames.flatMap(fields.get) } object JsObject { - def apply(members: JsField*) = new JsObject(members.toList) -} - -/** - * The members/fields of a JSON object. - */ -case class JsField(name: String, value: JsValue) extends JsValue -object JsField { - def apply(name: String, value: Any) = new JsField(name, JsValue(value)) + // we use a ListMap in order to preserve the field order + def apply(members: JsField*) = new JsObject(ListMap(members: _*)) + def apply(members: List[JsField]) = new JsObject(ListMap(members: _*)) } /** @@ -103,6 +71,10 @@ object JsArray { */ case class JsString(value: String) extends JsValue +object JsString { + def apply(value: Symbol) = new JsString(value.name) +} + /** * A JSON number. */ @@ -110,7 +82,11 @@ case class JsNumber(value: BigDecimal) extends JsValue object JsNumber { def apply(n: Int) = new JsNumber(BigDecimal(n)) def apply(n: Long) = new JsNumber(BigDecimal(n)) - def apply(n: Double) = 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)) } @@ -118,7 +94,7 @@ object JsNumber { /** * JSON Booleans. */ -sealed trait JsBoolean extends JsValue { +sealed abstract class JsBoolean extends JsValue { def value: Boolean } object JsBoolean { @@ -135,4 +111,4 @@ case object JsFalse extends JsBoolean { /** * The representation for JSON null. */ -case object JsNull extends JsValue
\ No newline at end of file +case object JsNull extends JsValue diff --git a/src/main/scala/cc/spray/json/JsonFormat.scala b/src/main/scala/cc/spray/json/JsonFormat.scala index c5bc53e..683a823 100644 --- a/src/main/scala/cc/spray/json/JsonFormat.scala +++ b/src/main/scala/cc/spray/json/JsonFormat.scala @@ -51,3 +51,21 @@ object JsonWriter { * 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/src/main/scala/cc/spray/json/JsonParser.scala b/src/main/scala/cc/spray/json/JsonParser.scala index 4a2d64b..21b1d68 100644 --- a/src/main/scala/cc/spray/json/JsonParser.scala +++ b/src/main/scala/cc/spray/json/JsonParser.scala @@ -31,16 +31,16 @@ object JsonParser extends Parser { def Json = rule { WhiteSpace ~ Value ~ EOI } def JsonObject: Rule1[JsObject] = rule { - "{ " ~ zeroOrMore(Pair, separator = ", ") ~ "} " ~~> (JsObject(_)) + "{ " ~ zeroOrMore(Pair, separator = ", ") ~ "} " ~~> (JsObject(_ :_*)) } - def Pair = rule { JsonStringUnwrapped ~ ": " ~ Value ~~> (JsField(_, _)) } + def Pair = rule { JsonStringUnwrapped ~ ": " ~ Value ~~> ((_, _)) } def Value: Rule1[JsValue] = rule { JsonString | JsonNumber | JsonObject | JsonArray | JsonTrue | JsonFalse | JsonNull } - def JsonString = rule { JsonStringUnwrapped ~~> JsString } + def JsonString = rule { JsonStringUnwrapped ~~> (JsString(_)) } def JsonStringUnwrapped = rule { "\"" ~ Characters ~ "\" " ~~> (_.toString) } diff --git a/src/main/scala/cc/spray/json/PrettyPrinter.scala b/src/main/scala/cc/spray/json/PrettyPrinter.scala index 93ac0b6..429c984 100644 --- a/src/main/scala/cc/spray/json/PrettyPrinter.scala +++ b/src/main/scala/cc/spray/json/PrettyPrinter.scala @@ -37,13 +37,13 @@ trait PrettyPrinter extends JsonPrinter { } } - private def printObject(members: List[JsField], sb: StringBuilder, indent: Int) { + private def printObject(members: Map[String, JsValue], sb: StringBuilder, indent: Int) { sb.append("{\n") printSeq(members, sb.append(",\n")) { m => printIndent(sb, indent + Indent) - printString(m.name, sb) + printString(m._1, sb) sb.append(": ") - print(m.value, sb, indent + Indent) + print(m._2, sb, indent + Indent) } sb.append('\n') printIndent(sb, indent) diff --git a/src/main/scala/cc/spray/json/ProductFormats.scala b/src/main/scala/cc/spray/json/ProductFormats.scala index d0263c8..1f26259 100644 --- a/src/main/scala/cc/spray/json/ProductFormats.scala +++ b/src/main/scala/cc/spray/json/ProductFormats.scala @@ -1,6 +1,5 @@ /* - * Original implementation (C) 2009-2011 Debasish Ghosh - * Adapted and extended in 2011 by Mathias Doenitz + * 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. @@ -17,8 +16,6 @@ package cc.spray.json -import annotation.tailrec - /** * Provides the helpers for constructing custom JsonFormat implementations for types implementing the Product trait * (especially case classes) @@ -26,7 +23,11 @@ import annotation.tailrec trait ProductFormats { this: StandardFormats => - def jsonFormat[A :JF, T <: Product](construct: A => T, a: String) = new JF[T]{ + def jsonFormat1[A :JF, T <: Product :ClassManifest](construct: A => T): RootJsonFormat[T] = { + val Array(a) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a) + } + def jsonFormat[A :JF, T <: Product](construct: A => T, a: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0) ) @@ -35,7 +36,11 @@ trait ProductFormats { ) } - def jsonFormat[A :JF, B :JF, T <: Product](construct: (A, B) => T, a: String, b: String) = new JF[T]{ + def jsonFormat2[A :JF, B :JF, T <: Product :ClassManifest](construct: (A, B) => T): RootJsonFormat[T] = { + val Array(a, b) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b) + } + def jsonFormat[A :JF, B :JF, T <: Product](construct: (A, B) => T, a: String, b: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1)) @@ -46,8 +51,12 @@ trait ProductFormats { ) } + def jsonFormat3[A :JF, B :JF, C :JF, T <: Product :ClassManifest](construct: (A, B, C) => T): RootJsonFormat[T] = { + val Array(a, b, c) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c) + } def jsonFormat[A :JF, B :JF, C :JF, T <: Product](construct: (A, B, C) => T, - a: String, b: String, c: String) = new JF[T]{ + a: String, b: String, c: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -60,8 +69,13 @@ trait ProductFormats { ) } + def jsonFormat4[A :JF, B :JF, C :JF, D :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D) => T): RootJsonFormat[T] = { + val Array(a, b, c, d) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, T <: Product](construct: (A, B, C, D) => T, - a: String, b: String, c: String, d: String) = new JF[T]{ + a: String, b: String, c: String, d: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -76,8 +90,13 @@ trait ProductFormats { ) } + def jsonFormat5[A :JF, B :JF, C :JF, D :JF, E :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, T <: Product](construct: (A, B, C, D, E) => T, - a: String, b: String, c: String, d: String, e: String) = new JF[T]{ + a: String, b: String, c: String, d: String, e: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -93,9 +112,14 @@ trait ProductFormats { fromField[E](value, e) ) } - + + def jsonFormat6[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, T <: Product](construct: (A, B, C, D, E, F) => T, - a: String, b: String, c: String, d: String, e: String, f: String) = new JF[T]{ + a: String, b: String, c: String, d: String, e: String, f: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -113,9 +137,14 @@ trait ProductFormats { fromField[F](value, f) ) } - + + def jsonFormat7[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, T <: Product](construct: (A, B, C, D, E, F, G) => T, - a: String, b: String, c: String, d: String, e: String, f: String, g: String) = new JF[T]{ + a: String, b: String, c: String, d: String, e: String, f: String, g: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -135,10 +164,15 @@ trait ProductFormats { fromField[G](value, g) ) } - + + def jsonFormat8[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G, H) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g, h) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g, h) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, T <: Product] (construct: (A, B, C, D, E, F, G, H) => T, - a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String) = new JF[T]{ + a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -160,10 +194,15 @@ trait ProductFormats { fromField[H](value, h) ) } - + + def jsonFormat9[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G, H, I) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g, h, i) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g, h, i) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, T <: Product] - (construct: (A, B, C, D, E, F, G, H, I) => T, - a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String, i: String) = new JF[T]{ + (construct: (A, B, C, D, E, F, G, H, I) => T, a: String, b: String, c: String, d: String, e: String, f: String, + g: String, h: String, i: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -187,10 +226,15 @@ trait ProductFormats { fromField[I](value, i) ) } - + + def jsonFormat10[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G, H, I, J) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g, h, i, j) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g, h, i, j) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, T <: Product] (construct: (A, B, C, D, E, F, G, H, I, J) => T, a: String, b: String, c: String, d: String, e: String, - f: String, g: String, h: String, i: String, j: String) = new JF[T]{ + f: String, g: String, h: String, i: String, j: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -216,10 +260,15 @@ trait ProductFormats { fromField[J](value, j) ) } - + + def jsonFormat11[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G, H, I, J, K) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g, h, i, j, k) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g, h, i, j, k) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, T <: Product] (construct: (A, B, C, D, E, F, G, H, I, J, K) => T, a: String, b: String, c: String, d: String, e: String, - f: String, g: String, h: String, i: String, j: String, k: String) = new JF[T]{ + f: String, g: String, h: String, i: String, j: String, k: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -248,9 +297,14 @@ trait ProductFormats { ) } + def jsonFormat12[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, L: JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G, H, I, J, K, L) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g, h, i, j, k, l) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g, h, i, j, k, l) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, L :JF, T <: Product] (construct: (A, B, C, D, E, F, G, H, I, J, K, L) => T, a: String, b: String, c: String, d: String, e: String, - f: String, g: String, h: String, i: String, j: String, k: String, l: String) = new JF[T]{ + f: String, g: String, h: String, i: String, j: String, k: String, l: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -281,9 +335,14 @@ trait ProductFormats { ) } + def jsonFormat13[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, L: JF, M :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G, H, I, J, K, L, M) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g, h, i, j, k, l, m) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g, h, i, j, k, l, m) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, L :JF, M :JF, T <: Product] (construct: (A, B, C, D, E, F, G, H, I, J, K, L, M) => T, a: String, b: String, c: String, d: String, e: String, - f: String, g: String, h: String, i: String, j: String, k: String, l: String, m: String) = new JF[T]{ + f: String, g: String, h: String, i: String, j: String, k: String, l: String, m: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -316,9 +375,15 @@ trait ProductFormats { ) } + def jsonFormat14[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, L: JF, M :JF, N :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G, H, I, J, K, L, M, N) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g, h, i, j, k, l, m, n) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g, h, i, j, k, l, m, n) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, L :JF, M :JF, N :JF, T <: Product] - (construct: (A, B, C, D, E, F, G, H, I, J, K, L, M, N) => T, a: String, b: String, c: String, d: String, e: String, - f: String, g: String, h: String, i: String, j: String, k: String, l: String, m: String, n: String) = new JF[T]{ + (construct: (A, B, C, D, E, F, G, H, I, J, K, L, M, N) => T, a: String, b: String, c: String, d: String, + e: String, f: String, g: String, h: String, i: String, j: String, k: String, l: String, m: String, + n: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -353,9 +418,15 @@ trait ProductFormats { ) } + def jsonFormat15[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, L: JF, M :JF, N :JF, O :JF, T <: Product :ClassManifest] + (construct: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) => T): RootJsonFormat[T] = { + val Array(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) = extractFieldNames(classManifest[T]) + jsonFormat(construct, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) + } def jsonFormat[A :JF, B :JF, C :JF, D :JF, E :JF, F :JF, G :JF, H :JF, I :JF, J :JF, K :JF, L :JF, M :JF, N :JF, O :JF, T <: Product] - (construct: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) => T, a: String, b: String, c: String, d: String, e: String, - f: String, g: String, h: String, i: String, j: String, k: String, l: String, m: String, n: String, o: String) = new JF[T]{ + (construct: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) => T, a: String, b: String, c: String, d: String, + e: String, f: String, g: String, h: String, i: String, j: String, k: String, l: String, m: String, n: String, + o: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = JsObject( productElement2Field[A](a, p, 0, productElement2Field[B](b, p, 1, @@ -399,25 +470,41 @@ trait ProductFormats { val value = p.productElement(ix).asInstanceOf[T] writer match { case _: OptionFormat[_] if (value == None) => rest - case _ => JsField(fieldName, writer.write(value)) :: rest + case _ => (fieldName, writer.write(value)) :: rest } } - + private def fromField[T](value: JsValue, fieldName: String)(implicit reader: JsonReader[T]) = { - @tailrec - def getFrom(fields: List[JsField]): T = { - if (fields.isEmpty) { - if (reader.isInstanceOf[OptionFormat[_]]) None.asInstanceOf[T] - else throw new DeserializationException("Object is missing required member '" + fieldName + "'") - } else if (fields.head.name == fieldName) { - reader.read(fields.head.value) - } else { - getFrom(fields.tail) - } - } value match { - case x: JsObject => getFrom(x.fields) - case _ => throw new DeserializationException("Object expected") + case x: JsObject => + var fieldFound = false + try { + val fieldValue = x.fields(fieldName) + fieldFound = true + reader.read(fieldValue) + } + catch { + case e: NoSuchElementException if !fieldFound => + if (reader.isInstanceOf[OptionFormat[_]]) None.asInstanceOf[T] + else deserializationError("Object is missing required member '" + fieldName + "'", e) + } + case _ => deserializationError("Object expected") + } + } + + protected def extractFieldNames(classManifest: ClassManifest[_]): Array[String] = { + val clazz = classManifest.erasure + try { + val copyDefaultMethods = clazz.getMethods.filter(_.getName.startsWith("copy$default$")) + val fields = clazz.getDeclaredFields.filterNot(_.getName.startsWith("$")) + if (copyDefaultMethods.length != fields.length) + sys.error("Case class declares additional fields") + if (fields.zip(copyDefaultMethods).exists { case (f, m) => f.getType != m.getReturnType }) + sys.error("Cannot determine field order") + fields.map(_.getName) + } catch { + case ex => throw new RuntimeException("Cannot automatically determine case class field names and order, " + + "please use the 'jsonFormat' overload with explicit field name specification", ex) } } } @@ -435,6 +522,6 @@ trait NullOptions extends ProductFormats { 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] - JsField(fieldName, writer.write(value)) :: rest + (fieldName, writer.write(value)) :: rest } }
\ No newline at end of file diff --git a/src/main/scala/cc/spray/json/SerializationException.scala b/src/main/scala/cc/spray/json/SerializationException.scala deleted file mode 100644 index f8cbc00..0000000 --- a/src/main/scala/cc/spray/json/SerializationException.scala +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 cc.spray.json - -class SerializationException(msg: String) extends RuntimeException(msg)
\ No newline at end of file diff --git a/src/main/scala/cc/spray/json/StandardFormats.scala b/src/main/scala/cc/spray/json/StandardFormats.scala index 1c44716..78bc539 100644 --- a/src/main/scala/cc/spray/json/StandardFormats.scala +++ b/src/main/scala/cc/spray/json/StandardFormats.scala @@ -36,7 +36,7 @@ trait StandardFormats { } def read(value: JsValue) = value match { case JsNull => None - case x => Some(x.fromJson[T]) + case x => Some(x.convertTo[T]) } } @@ -45,75 +45,72 @@ trait StandardFormats { case Right(a) => a.toJson case Left(b) => b.toJson } - def read(value: JsValue) = (value.fromJson(safeReader[A]), value.fromJson(safeReader[B])) match { + 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[_, _]) => throw new DeserializationException("Ambiguous Either value: can be read as both, Left and Right, values") - case (Left(ea), Left(eb)) => throw new DeserializationException("Could not read Either value:\n" + ea + "---------- and ----------\n" + eb) + 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.fromJson[A]) + def read(value: JsValue) = Tuple1(value.convertTo[A]) } - implicit def tuple2Format[A :JF, B :JF] = new JF[(A, B)] { + 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(a :: b :: Nil) => (a.fromJson[A], b.fromJson[B]) - case _ => throw new DeserializationException("Tuple2 expected") + case JsArray(a :: b :: Nil) => (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 JF[(A, B, C)] { + 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(a :: b :: c :: Nil) => (a.fromJson[A], b.fromJson[B], c.fromJson[C]) - case _ => throw new DeserializationException("Tuple3 expected") + case JsArray(a :: b :: c :: Nil) => (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 JF[(A, B, C, D)] { + 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(a :: b :: c :: d :: Nil) => (a.fromJson[A], b.fromJson[B], c.fromJson[C], d.fromJson[D]) - case _ => throw new DeserializationException("Tuple4 expected") + case JsArray(a :: b :: c :: d :: Nil) => (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 JF[(A, B, C, D, E)] { + 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(a :: b :: c :: d :: e :: Nil) => { - (a.fromJson[A], b.fromJson[B], c.fromJson[C], d.fromJson[D], e.fromJson[E]) - } - case _ => throw new DeserializationException("Tuple5 expected") + case JsArray(a :: b :: c :: d :: e :: Nil) => + (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 JF[(A, B, C, D, E, F)] { + 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(a :: b :: c :: d :: e :: f :: Nil) => { - (a.fromJson[A], b.fromJson[B], c.fromJson[C], d.fromJson[D], e.fromJson[E], f.fromJson[F]) - } - case _ => throw new DeserializationException("Tuple6 expected") + case JsArray(a :: b :: c :: d :: e :: f :: Nil) => + (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 JF[(A, B, C, D, E, F, G)] { + 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._6.toJson) def read(value: JsValue) = value match { - case JsArray(a :: b :: c :: d :: e :: f :: g :: Nil) => { - (a.fromJson[A], b.fromJson[B], c.fromJson[C], d.fromJson[D], e.fromJson[E], f.fromJson[F], g.fromJson[G]) - } - case _ => throw new DeserializationException("Tuple7 expected") + case JsArray(a :: b :: c :: d :: e :: f :: g :: Nil) => + (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/src/main/scala/cc/spray/json/package.scala b/src/main/scala/cc/spray/json/package.scala index ee50ec0..ec12939 100644 --- a/src/main/scala/cc/spray/json/package.scala +++ b/src/main/scala/cc/spray/json/package.scala @@ -17,16 +17,29 @@ package cc.spray package object json { - - def jsonReader[T](implicit reader: JsonReader[T]) = reader + + type JsField = (String, JsValue) + + def deserializationError(msg: String, cause: Throwable = null) = throw new DeserializationException(msg, cause) + 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 pimpAny[T](any: T): PimpedAny[T] = new PimpedAny(any) - + implicit def pimpAny[T](any: T) = new PimpedAny(any) + implicit def pimpString(string: String) = new PimpedString(string) } package json { + + class DeserializationException(msg: String, cause: Throwable = null) extends RuntimeException(msg, cause) + class SerializationException(msg: String) extends RuntimeException(msg) + private[json] class PimpedAny[T](any: T) { def toJson(implicit writer: JsonWriter[T]): JsValue = writer.write(any) } + + private[json] class PimpedString(string: String) { + def asJson: JsValue = JsonParser(string) + } }
\ No newline at end of file diff --git a/src/test/scala/cc/spray/json/AdditionalFormatsSpec.scala b/src/test/scala/cc/spray/json/AdditionalFormatsSpec.scala index 7368128..f25c9d8 100644 --- a/src/test/scala/cc/spray/json/AdditionalFormatsSpec.scala +++ b/src/test/scala/cc/spray/json/AdditionalFormatsSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ @@ -10,8 +26,8 @@ class AdditionalFormatsSpec extends Specification { implicit def containerReader[T :JsonFormat] = lift { new JsonReader[Container[T]] { def read(value: JsValue) = value match { - case JsObject(JsField("content", obj: JsValue) :: Nil) => Container(Some(jsonReader[T].read(obj))) - case _ => throw new DeserializationException("Unexpected format: " + value.toString) + case JsObject(fields) if fields.contains("content") => Container(Some(jsonReader[T].read(fields("content")))) + case _ => deserializationError("Unexpected format: " + value.toString) } } } @@ -20,7 +36,7 @@ class AdditionalFormatsSpec extends Specification { object WriterProtocol extends DefaultJsonProtocol { implicit def containerWriter[T :JsonFormat] = lift { new JsonWriter[Container[T]] { - def write(obj: Container[T]) = JsObject(JsField("content", obj.inner.toJson)) + def write(obj: Container[T]) = JsObject("content" -> obj.inner.toJson) } } } @@ -35,7 +51,21 @@ class AdditionalFormatsSpec extends Specification { "properly read a Container[Container[List[Int]]] from JSON" in { import ReaderProtocol._ - JsonParser("""{"content":{"content":[1,2,3]}}""").fromJson[Container[Container[List[Int]]]] mustEqual obj + """{"content":{"content":[1,2,3]}}""".asJson.convertTo[Container[Container[List[Int]]]] mustEqual obj + } + } + + case class Foo(id: Long, name: String, foos: Option[List[Foo]] = None) + + object FooProtocol extends DefaultJsonProtocol { + implicit val FooProtocol: JsonFormat[Foo] = lazyFormat(jsonFormat(Foo, "id", "name", "foos")) + } + + "The lazyFormat wrapper" should { + "enable recursive format definitions" in { + import FooProtocol._ + Foo(1, "a", Some(Foo(2, "b", Some(Foo(3, "c") :: Nil)) :: Foo(4, "d") :: Nil)).toJson.toString mustEqual + """{"id":1,"name":"a","foos":[{"id":2,"name":"b","foos":[{"id":3,"name":"c"}]},{"id":4,"name":"d"}]}""" } } }
\ No newline at end of file diff --git a/src/test/scala/cc/spray/json/BasicFormatsSpec.scala b/src/test/scala/cc/spray/json/BasicFormatsSpec.scala index fcae4bb..f873948 100644 --- a/src/test/scala/cc/spray/json/BasicFormatsSpec.scala +++ b/src/test/scala/cc/spray/json/BasicFormatsSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ @@ -9,7 +25,7 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { 42.toJson mustEqual JsNumber(42) } "convert a JsNumber to an Int" in { - JsNumber(42).fromJson[Int] mustEqual 42 + JsNumber(42).convertTo[Int] mustEqual 42 } } @@ -18,7 +34,7 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { 42L.toJson mustEqual JsNumber(42L) } "convert a JsNumber to a Long" in { - JsNumber(42L).fromJson[Long] mustEqual 42L + JsNumber(42L).convertTo[Long] mustEqual 42L } } @@ -26,8 +42,20 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { "convert a Float to a JsNumber" in { 4.2f.toJson mustEqual JsNumber(4.2f) } + "convert a Float.NaN to a JsNull" in { + Float.NaN.toJson mustEqual JsNull + } + "convert a Float.PositiveInfinity to a JsNull" in { + Float.PositiveInfinity.toJson mustEqual JsNull + } + "convert a Float.NegativeInfinity to a JsNull" in { + Float.NegativeInfinity.toJson mustEqual JsNull + } "convert a JsNumber to a Float" in { - JsNumber(4.2f).fromJson[Float] mustEqual 4.2f + JsNumber(4.2f).convertTo[Float] mustEqual 4.2f + } + "convert a JsNull to a Float" in { + JsNull.convertTo[Float].isNaN mustEqual Float.NaN.isNaN } } @@ -35,8 +63,20 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { "convert a Double to a JsNumber" in { 4.2.toJson mustEqual JsNumber(4.2) } + "convert a Double.NaN to a JsNull" in { + Double.NaN.toJson mustEqual JsNull + } + "convert a Double.PositiveInfinity to a JsNull" in { + Double.PositiveInfinity.toJson mustEqual JsNull + } + "convert a Double.NegativeInfinity to a JsNull" in { + Double.NegativeInfinity.toJson mustEqual JsNull + } "convert a JsNumber to a Double" in { - JsNumber(4.2).fromJson[Double] mustEqual 4.2 + JsNumber(4.2).convertTo[Double] mustEqual 4.2 + } + "convert a JsNull to a Double" in { + JsNull.convertTo[Double].isNaN mustEqual Double.NaN.isNaN } } @@ -45,7 +85,7 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { 42.asInstanceOf[Byte].toJson mustEqual JsNumber(42) } "convert a JsNumber to a Byte" in { - JsNumber(42).fromJson[Byte] mustEqual 42 + JsNumber(42).convertTo[Byte] mustEqual 42 } } @@ -54,7 +94,7 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { 42.asInstanceOf[Short].toJson mustEqual JsNumber(42) } "convert a JsNumber to a Short" in { - JsNumber(42).fromJson[Short] mustEqual 42 + JsNumber(42).convertTo[Short] mustEqual 42 } } @@ -63,7 +103,7 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { BigDecimal(42).toJson mustEqual JsNumber(42) } "convert a JsNumber to a BigDecimal" in { - JsNumber(42).fromJson[BigDecimal] mustEqual BigDecimal(42) + JsNumber(42).convertTo[BigDecimal] mustEqual BigDecimal(42) } } @@ -72,7 +112,7 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { BigInt(42).toJson mustEqual JsNumber(42) } "convert a JsNumber to a BigInt" in { - JsNumber(42).fromJson[BigInt] mustEqual BigInt(42) + JsNumber(42).convertTo[BigInt] mustEqual BigInt(42) } } @@ -81,15 +121,15 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { ().toJson mustEqual JsNumber(1) } "convert a JsNumber to Unit" in { - JsNumber(1).fromJson[Unit] mustEqual () + JsNumber(1).convertTo[Unit] mustEqual () } } "The BooleanJsonFormat" should { "convert true to a JsTrue" in { true.toJson mustEqual JsTrue } "convert false to a JsFalse" in { false.toJson mustEqual JsFalse } - "convert a JsTrue to true" in { JsTrue.fromJson[Boolean] mustEqual true } - "convert a JsFalse to false" in { JsFalse.fromJson[Boolean] mustEqual false } + "convert a JsTrue to true" in { JsTrue.convertTo[Boolean] mustEqual true } + "convert a JsFalse to false" in { JsFalse.convertTo[Boolean] mustEqual false } } "The CharJsonFormat" should { @@ -97,7 +137,7 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { 'c'.toJson mustEqual JsString("c") } "convert a JsString to a Char" in { - JsString("c").fromJson[Char] mustEqual 'c' + JsString("c").convertTo[Char] mustEqual 'c' } } @@ -106,7 +146,7 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { "Hello".toJson mustEqual JsString("Hello") } "convert a JsString to a String" in { - JsString("Hello").fromJson[String] mustEqual "Hello" + JsString("Hello").convertTo[String] mustEqual "Hello" } } @@ -115,8 +155,8 @@ class BasicFormatsSpec extends Specification with DefaultJsonProtocol { 'Hello.toJson mustEqual JsString("Hello") } "convert a JsString to a Symbol" in { - JsString("Hello").fromJson[Symbol] mustEqual 'Hello + JsString("Hello").convertTo[Symbol] mustEqual 'Hello } } -}
\ No newline at end of file +} diff --git a/src/test/scala/cc/spray/json/CollectionFormatsSpec.scala b/src/test/scala/cc/spray/json/CollectionFormatsSpec.scala index d0cb509..a252b1f 100644 --- a/src/test/scala/cc/spray/json/CollectionFormatsSpec.scala +++ b/src/test/scala/cc/spray/json/CollectionFormatsSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ @@ -12,7 +28,7 @@ class CollectionFormatsSpec extends Specification with DefaultJsonProtocol { list.toJson mustEqual json } "convert a JsArray of JsNumbers to a List[Int]" in { - json.fromJson[List[Int]] mustEqual list + json.convertTo[List[Int]] mustEqual list } } @@ -23,18 +39,18 @@ class CollectionFormatsSpec extends Specification with DefaultJsonProtocol { array.toJson mustEqual json } "convert a JsArray of JsNumbers to an Array[Int]" in { - Arrays.equals(json.fromJson[Array[Int]], array) must beTrue + Arrays.equals(json.convertTo[Array[Int]], array) must beTrue } } "The mapFormat" should { val map = Map("a" -> 1, "b" -> 2, "c" -> 3) - val json = JsObject(JsField("a", 1), JsField("b", 2), JsField("c", 3)) + val json = JsObject("a" -> JsNumber(1), "b" -> JsNumber(2), "c" -> JsNumber(3)) "convert a Map[String, Long] to a JsObject" in { map.toJson mustEqual json } "be able to convert a JsObject to a Map[String, Long]" in { - json.fromJson[Map[String, Long]] mustEqual map + json.convertTo[Map[String, Long]] mustEqual map } "throw an Exception when trying to serialize a map whose key are not serialized to JsStrings" in { Map(1 -> "a").toJson must throwA(new SerializationException("Map key must be formatted as JsString, not '1'")) @@ -48,7 +64,7 @@ class CollectionFormatsSpec extends Specification with DefaultJsonProtocol { set.toJson mustEqual json } "convert a JsArray of JsNumbers to a Set[Int]" in { - json.fromJson[Set[Int]] mustEqual set + json.convertTo[Set[Int]] mustEqual set } } @@ -59,7 +75,7 @@ class CollectionFormatsSpec extends Specification with DefaultJsonProtocol { seq.toJson mustEqual json } "convert a JsArray of JsNumbers to a IndexedSeq[Int]" in { - json.fromJson[collection.IndexedSeq[Int]] mustEqual seq + json.convertTo[collection.IndexedSeq[Int]] mustEqual seq } } diff --git a/src/test/scala/cc/spray/json/CompactPrinterSpec.scala b/src/test/scala/cc/spray/json/CompactPrinterSpec.scala index 3bc4870..c441864 100644 --- a/src/test/scala/cc/spray/json/CompactPrinterSpec.scala +++ b/src/test/scala/cc/spray/json/CompactPrinterSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ @@ -36,11 +52,11 @@ class CompactPrinterSpec extends Specification { CompactPrinter(JsString("\"\\\b\f\n\r\t\u12AB")) mustEqual """"\"\\\b\f\n\r\t""" + "\\u12ab\"" } "properly print a simple JsObject" in ( - CompactPrinter(JsObject(JsField("key", 42), JsField("key2", "value"))) + CompactPrinter(JsObject("key" -> JsNumber(42), "key2" -> JsString("value"))) mustEqual """{"key":42,"key2":"value"}""" ) "properly print a simple JsArray" in ( - CompactPrinter(JsArray(JsNull, JsNumber(1.23), JsObject(JsField("key", true)))) + CompactPrinter(JsArray(JsNull, JsNumber(1.23), JsObject("key" -> JsBoolean(true)))) mustEqual """[null,1.23,{"key":true}]""" ) "properly print a JSON padding (JSONP) if requested" in { diff --git a/src/test/scala/cc/spray/json/CustomFormatSpec.scala b/src/test/scala/cc/spray/json/CustomFormatSpec.scala new file mode 100644 index 0000000..e8feb59 --- /dev/null +++ b/src/test/scala/cc/spray/json/CustomFormatSpec.scala @@ -0,0 +1,45 @@ +/* + * 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 cc.spray.json + +import org.specs2.mutable.Specification + +class CustomFormatSpec extends Specification with DefaultJsonProtocol { + + case class MyType(name: String, value: Int) + + implicit val MyTypeProtocol = new RootJsonFormat[MyType] { + def read(json: JsValue) = { + json.asJsObject.getFields("name", "value") match { + case Seq(JsString(name), JsNumber(value)) => MyType(name, value.toInt) + case _ => deserializationError("Expected fields: 'name' (JSON string) and 'value' (JSON number)") + } + } + def write(obj: MyType) = JsObject("name" -> JsString(obj.name), "value" -> JsNumber(obj.value)) + } + + "A custom JsonFormat built with 'asJsonObject'" should { + val value = MyType("bob", 42) + "correctly deserialize valid JSON content" in { + """{ "name": "bob", "value": 42 }""".asJson.convertTo[MyType] mustEqual value + } + "support full round-trip (de)serialization" in { + value.toJson.convertTo[MyType] mustEqual value + } + } + +}
\ No newline at end of file diff --git a/src/test/scala/cc/spray/json/JsonParserSpec.scala b/src/test/scala/cc/spray/json/JsonParserSpec.scala index 0860d05..cfe270f 100644 --- a/src/test/scala/cc/spray/json/JsonParserSpec.scala +++ b/src/test/scala/cc/spray/json/JsonParserSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ @@ -35,16 +51,16 @@ class JsonParserSpec extends Specification { } "properly parse a simple JsObject" in ( JsonParser(""" { "key" :42, "key2": "value" }""") mustEqual - JsObject(JsField("key", 42), JsField("key2", "value")) + JsObject("key" -> JsNumber(42), "key2" -> JsString("value")) ) "properly parse a simple JsArray" in ( JsonParser("""[null, 1.23 ,{"key":true } ] """) mustEqual - JsArray(JsNull, JsNumber(1.23), JsObject(JsField("key", true))) + JsArray(JsNull, JsNumber(1.23), JsObject("key" -> JsBoolean(true))) ) "be reentrant" in { val largeJsonSource = FileUtils.readAllCharsFromResource("test.json") List.fill(20)(largeJsonSource).par.map(JsonParser(_)).toList.map { - _.asInstanceOf[JsObject].asMap("questions").asInstanceOf[JsArray].elements.size + _.asInstanceOf[JsObject].fields("questions").asInstanceOf[JsArray].elements.size } mustEqual List.fill(20)(100) } } diff --git a/src/test/scala/cc/spray/json/PrettyPrinterSpec.scala b/src/test/scala/cc/spray/json/PrettyPrinterSpec.scala index feed8a9..3bc29ff 100644 --- a/src/test/scala/cc/spray/json/PrettyPrinterSpec.scala +++ b/src/test/scala/cc/spray/json/PrettyPrinterSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ diff --git a/src/test/scala/cc/spray/json/ProductFormatsSpec.scala b/src/test/scala/cc/spray/json/ProductFormatsSpec.scala index 9a692ec..c34f491 100644 --- a/src/test/scala/cc/spray/json/ProductFormatsSpec.scala +++ b/src/test/scala/cc/spray/json/ProductFormatsSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ @@ -9,8 +25,8 @@ class ProductFormatsSpec extends Specification { trait TestProtocol { this: DefaultJsonProtocol => - implicit val test2Format = jsonFormat(Test2, "a", "b") - implicit def test3Format[A: JsonFormat, B: JsonFormat] = jsonFormat(Test3.apply[A, B], "as", "bs") + implicit val test2Format = jsonFormat2(Test2) + implicit def test3Format[A: JsonFormat, B: JsonFormat] = jsonFormat2(Test3.apply[A, B]) } object TestProtocol1 extends DefaultJsonProtocol with TestProtocol object TestProtocol2 extends DefaultJsonProtocol with TestProtocol with NullOptions @@ -18,38 +34,38 @@ class ProductFormatsSpec extends Specification { "A JsonFormat created with `jsonFormat`, for a case class with 2 elements," should { import TestProtocol1._ val obj = Test2(42, Some(4.2)) - val json = JsObject(JsField("a", 42), JsField("b", 4.2)) + val json = JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2)) "convert to a respective JsObject" in { obj.toJson mustEqual json } "convert a JsObject to the respective case class instance" in { - json.fromJson[Test2] mustEqual obj + json.convertTo[Test2] mustEqual obj } "throw a DeserializationException if the JsObject does not all required members" in ( - JsObject(JsField("b", 4.2)).fromJson[Test2] must + JsObject("b" -> JsNumber(4.2)).convertTo[Test2] must throwA(new DeserializationException("Object is missing required member 'a'")) ) "not require the presence of optional fields for deserialization" in { - JsObject(JsField("a", 42)).fromJson[Test2] mustEqual Test2(42, None) + JsObject("a" -> JsNumber(42)).convertTo[Test2] mustEqual Test2(42, None) } "not render `None` members during serialization" in { - Test2(42, None).toJson mustEqual JsObject(JsField("a", 42)) + Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42)) } "ignore additional members during deserialization" in { - JsObject(JsField("a", 42), JsField("b", 4.2), JsField("c", 'no)).fromJson[Test2] mustEqual obj + JsObject("a" -> JsNumber(42), "b" -> JsNumber(4.2), "c" -> JsString('no)).convertTo[Test2] mustEqual obj } "not depend on any specific member order for deserialization" in { - JsObject(JsField("b", 4.2), JsField("a", 42)).fromJson[Test2] mustEqual obj + JsObject("b" -> JsNumber(4.2), "a" -> JsNumber(42)).convertTo[Test2] mustEqual obj } "throw a DeserializationException if the JsValue is not a JsObject" in ( - JsNull.fromJson[Test2] must throwA(new DeserializationException("Object expected")) + JsNull.convertTo[Test2] must throwA(new DeserializationException("Object expected")) ) } "A JsonProtocol mixing in NullOptions" should { "render `None` members to `null`" in { import TestProtocol2._ - Test2(42, None).toJson mustEqual JsObject(JsField("a", 42), JsField("b", JsNull)) + Test2(42, None).toJson mustEqual JsObject("a" -> JsNumber(42), "b" -> JsNull) } } @@ -57,14 +73,14 @@ class ProductFormatsSpec extends Specification { import TestProtocol1._ val obj = Test3(42 :: 43 :: Nil, "x" :: "y" :: "z" :: Nil) val json = JsObject( - JsField("as", JsArray(JsNumber(42), JsNumber(43))), - JsField("bs", JsArray(JsString("x"), JsString("y"), JsString("z"))) + "as" -> JsArray(JsNumber(42), JsNumber(43)), + "bs" -> JsArray(JsString("x"), JsString("y"), JsString("z")) ) "convert to a respective JsObject" in { obj.toJson mustEqual json } "convert a JsObject to the respective case class instance" in { - json.fromJson[Test3[Int, String]] mustEqual obj + json.convertTo[Test3[Int, String]] mustEqual obj } } diff --git a/src/test/scala/cc/spray/json/ReadmeSpec.scala b/src/test/scala/cc/spray/json/ReadmeSpec.scala index 843d1fc..791720f 100644 --- a/src/test/scala/cc/spray/json/ReadmeSpec.scala +++ b/src/test/scala/cc/spray/json/ReadmeSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ @@ -8,58 +24,74 @@ class ReadmeSpec extends Specification { "behave as expected" in { import DefaultJsonProtocol._ - val json = """{ "some": "JSON source" }""" - val jsonAst = JsonParser(json) - jsonAst mustEqual JsObject(JsField("some", "JSON source")) + val source = """{ "some": "JSON source" }""" + val jsonAst = source.asJson + jsonAst mustEqual JsObject("some" -> JsString("JSON source")) - val json2 = PrettyPrinter(jsonAst) + val json2 = jsonAst.prettyPrint json2 mustEqual """{ | "some": "JSON source" |}""".stripMargin - + val jsonAst2 = List(1, 2, 3).toJson jsonAst2 mustEqual JsArray(JsNumber(1), JsNumber(2), JsNumber(3)) } } - + case class Color(name: String, red: Int, green: Int, blue: Int) - + val color = Color("CadetBlue", 95, 158, 160) + "The case class example" should { "behave as expected" in { object MyJsonProtocol extends DefaultJsonProtocol { - implicit val colorFormat = jsonFormat(Color, "name", "red", "green", "blue") - } + implicit val colorFormat = jsonFormat4(Color) + } import MyJsonProtocol._ - - val json = Color("CadetBlue", 95, 158, 160).toJson - val color = json.fromJson[Color] - - color mustEqual Color("CadetBlue", 95, 158, 160) + color.toJson.convertTo[Color] mustEqual color } } - - "The non case class example" should { + + "The non case class (array) example" should { "behave as expected" in { object MyJsonProtocol extends DefaultJsonProtocol { implicit object ColorJsonFormat extends JsonFormat[Color] { - def write(c: Color) = { + def write(c: Color) = JsArray(JsString(c.name), JsNumber(c.red), JsNumber(c.green), JsNumber(c.blue)) - } + def read(value: JsValue) = value match { - case JsArray(JsString(name) :: JsNumber(red) :: JsNumber(green) :: JsNumber(blue) :: Nil) => { + case JsArray(JsString(name) :: JsNumber(red) :: JsNumber(green) :: JsNumber(blue) :: Nil) => new Color(name, red.toInt, green.toInt, blue.toInt) + case _ => deserializationError("Color expected") + } + } + } + import MyJsonProtocol._ + color.toJson.convertTo[Color] mustEqual color + } + } + + "The non case class (object) example" should { + "behave as expected" in { + object MyJsonProtocol extends DefaultJsonProtocol { + implicit object ColorJsonFormat extends JsonFormat[Color] { + def write(c: Color) = JsObject( + "name" -> JsString(c.name), + "red" -> JsNumber(c.red), + "green" -> JsNumber(c.green), + "blue" -> JsNumber(c.blue) + ) + def read(value: JsValue) = { + value.asJsObject.getFields("name", "red", "green", "blue") match { + case Seq(JsString(name), JsNumber(red), JsNumber(green), JsNumber(blue)) => + new Color(name, red.toInt, green.toInt, blue.toInt) + case _ => throw new DeserializationException("Color expected") } - case _ => throw new DeserializationException("Color expected") } } - } + } import MyJsonProtocol._ - - val json = Color("CadetBlue", 95, 158, 160).toJson - val color = json.fromJson[Color] - - color mustEqual Color("CadetBlue", 95, 158, 160) + color.toJson.convertTo[Color] mustEqual color } } diff --git a/src/test/scala/cc/spray/json/StandardFormatsSpec.scala b/src/test/scala/cc/spray/json/StandardFormatsSpec.scala index ad9485c..ed6283f 100644 --- a/src/test/scala/cc/spray/json/StandardFormatsSpec.scala +++ b/src/test/scala/cc/spray/json/StandardFormatsSpec.scala @@ -1,3 +1,19 @@ +/* + * 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 cc.spray.json import org.specs2.mutable._ @@ -10,13 +26,13 @@ class StandardFormatsSpec extends Specification with DefaultJsonProtocol { None.asInstanceOf[Option[Int]].toJson mustEqual JsNull } "convert JsNull to None" in { - JsNull.fromJson[Option[Int]] mustEqual None + JsNull.convertTo[Option[Int]] mustEqual None } "convert Some(Hello) to JsString(Hello)" in { Some("Hello").asInstanceOf[Option[String]].toJson mustEqual JsString("Hello") } "convert JsString(Hello) to Some(Hello)" in { - JsString("Hello").fromJson[Option[String]] mustEqual Some("Hello") + JsString("Hello").convertTo[Option[String]] mustEqual Some("Hello") } } @@ -31,10 +47,10 @@ class StandardFormatsSpec extends Specification with DefaultJsonProtocol { b.toJson mustEqual JsString("Hello") } "convert the left side of an Either value from Json" in { - JsNumber(42).fromJson[Either[Int, String]] mustEqual Left(42) + JsNumber(42).convertTo[Either[Int, String]] mustEqual Left(42) } "convert the right side of an Either value from Json" in { - JsString("Hello").fromJson[Either[Int, String]] mustEqual Right("Hello") + JsString("Hello").convertTo[Either[Int, String]] mustEqual Right("Hello") } } @@ -43,7 +59,7 @@ class StandardFormatsSpec extends Specification with DefaultJsonProtocol { Tuple1(42).toJson mustEqual JsNumber(42) } "be able to convert a JsNumber to a Tuple1[Int]" in { - JsNumber(42).fromJson[Tuple1[Int]] mustEqual Tuple1(42) + JsNumber(42).convertTo[Tuple1[Int]] mustEqual Tuple1(42) } } @@ -53,7 +69,7 @@ class StandardFormatsSpec extends Specification with DefaultJsonProtocol { (42, 4.2).toJson mustEqual json } "be able to convert a JsArray to a (Int, Double)]" in { - json.fromJson[(Int, Double)] mustEqual (42, 4.2) + json.convertTo[(Int, Double)] mustEqual (42, 4.2) } } |