├── .gitignore ├── .jvmopts ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── project ├── ProjectPlugin.scala ├── build.properties ├── plugins.sbt └── project │ └── plugins.sbt └── scala-json-rpc └── src ├── main └── scala │ └── com │ └── dhpcs │ └── jsonrpc │ ├── JsonRpcMessage.scala │ └── MessageCompanions.scala └── test └── scala └── com └── dhpcs └── jsonrpc ├── JsonRpcMessageSpec.scala └── MessageCompanionsSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -Xms512M 2 | -Xmx4096M 3 | -Xss2M 4 | -XX:MaxMetaspaceSize=1024M 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | jdk: 3 | - oraclejdk8 4 | env: 5 | global: 6 | - secure: aE57mflme9toW7C0wY0DqAn+cUsX3WiGtuF37EIDg+/+7CiiXWgeDhVV7+cxO9ZJTBU4Svw20v4a1CWvJOR2Ji3NHksB347H5H3vuCF/tDobDvu3qfdl+EGN2tUSXxQe+CjWl2vYuo8ls/HMKLb+mApi9wSNrFM7K8ysCevN6LBzmlsyofnA0b1AOQPbFIumlsMg1v6fnYcguIDU75uDlAzhhzIDYhCequm8uad4FSWfQgNN/BknhUU9/vZpcwqAjkuc/vjkamJOHVBBUmwfxY86OqdGsrDNHmyac3PfePwWnLyxLFbng86Jh4Qb+zkUfLEppqgAg8IqWp4mNR/9td0cmn4rH1BT29quSuYv2j8fc3nSUiQ3KPI6D/f2fIjrzKk/EPnmEV+StSWA8g68lVRCGojI+cWaJTeaXv96zxg7V5VRc01ig4cm5xHizYKSAdUQMvQjw/wVQyHcemI5mUHKYft5owzgJaahb/KQTUAde9Xu/WVfcfkQUIjdXC5xCwmQi0+9vUWiy7ZbW15tRSdl6RgCZWce09FqOcloLjzAjUyz/h5bEY88rZsj/RvSn1kXwa4qhddXdfI2iui/PwKK8mJCWMYtV2IZj/KE39mIke7/B2wh5yjvofWrHbCPFG4DwqPSXjNemPBP2C2RYnA0yR1ItXLetRikkLhIdMo= 7 | - secure: WMdNhI3BDH9htPUzP8+RfcFNzTRcRrHszylFdul5SodetzvNP2v3P82wqoTW8vZeGCZPf/QK1NVmgIhFFbS14dlLjBl10/TyX8ou9Ga5HGJGH7YUxKQLorwrZohckkzA2OP3Gs1DM52vsCwP5x2sRKGslKZupgnPF8xf9CmvnjBuk8V0aSoDFFJYj1FbiuIAViKE+VqvcETJBH656MAACSYJnYp55dMVkiW2rUABscpzx62qR8cvO2RKPOCoJccBN6I31T0LiguaWwFUhJ3J1cWFoCbBB6+QsBEwe39a80jr18p+NBOy6HDxzEwyt1HlEcBwjtGIIfhmeSIvKq1NMhT42LzDkR2BRn+88wS/Pt6lvM1Ut19yBRKSt3aYH56O29i9QnDYbSwTdTY48ELFY5NQ0bQ4aG9T8kjO5q2O/vHKFcGC8JjQlAqVxyQy2dB3+UC1rycAYl8X/kd1GTpzhMapMmUB3JUF12mvzNr97YqVzTmcZEH7/lAt5DmDlH2ofoTzmSRmxDpwNdpokZqUiqU5L0Hv3IRjZufvFdWEholhZyFut1oj0smu6XRnXaHLuuoBnTmYbGlc+w5beSQBpc8lIWTb99Ui0b8wb6PxidrRwo4iSENBO0wZ5MmPYC6vSa0pBXB1hXFW195sa9Gh5YXPqItdurcpkIRTw2OwQq8= 8 | stages: 9 | - test 10 | - name: release 11 | if: type NOT IN (pull_request) 12 | jobs: 13 | include: 14 | - stage: test 15 | script: sbt ";reload plugins; sbt:scalafmt::test; scalafmt::test; reload return; 16 | sbt:scalafmt::test; scalafmt::test; test:scalafmt::test; 17 | compile; coverage; test; coverageReport" && bash <(curl -s https://codecov.io/bash) 18 | - stage: release 19 | script: | 20 | git fetch --unshallow && # So that sbt-dynver derives the version correctly 21 | sbt ";scala-json-rpc/releaseEarly" 22 | cache: 23 | directories: 24 | - $HOME/.ivy2/cache 25 | - $HOME/.sbt 26 | - $HOME/.coursier 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scala-json-rpc 2 | 3 | [![Build Status](https://travis-ci.org/dhpcs/scala-json-rpc.svg?branch=master)](https://travis-ci.org/dhpcs/scala-json-rpc) 4 | [![codecov](https://codecov.io/gh/dhpcs/scala-json-rpc/branch/master/graph/badge.svg)](https://codecov.io/gh/dhpcs/scala-json-rpc) 5 | [![Latest Version](https://index.scala-lang.org/dhpcs/scala-json-rpc/scala-json-rpc/latest.svg)](https://index.scala-lang.org/dhpcs/scala-json-rpc/scala-json-rpc) 6 | 7 | Types and play-json Format instances for [JSON-RPC 2.0 8 | messages](http://www.jsonrpc.org/specification), with application command, 9 | response and notification marshalling support. 10 | 11 | Note that up until version 1.5.0 scala-json-rpc was called play-json-rpc. It 12 | was originally developed for use in both the server and client side of 13 | [Liquidity](https://github.com/dhpcs/liquidity). 14 | 15 | 16 | ## Resolver and library dependency 17 | 18 | ```scala 19 | resolvers += Resolver.bintrayRepo("dhpcs", "maven") 20 | 21 | libraryDependencies += "com.dhpcs" %% "scala-json-rpc" % "x.y.z" 22 | ``` 23 | 24 | 25 | ## Command, response and notification marshalling 26 | 27 | The `CommandCompanion`, `ResponseCompanion` and `NotificationCompanion` bases 28 | defined in 29 | [MessageCompanions.scala](scala-json-rpc/src/main/scala/com/dhpcs/jsonrpc/MessageCompanions.scala) 30 | provide readers and writers for hierarchies of application level types. Example 31 | use can be seen in 32 | [MessageCompanionsSpec.scala](scala-json-rpc/src/test/scala/com/dhpcs/jsonrpc/MessageCompanionsSpec.scala). 33 | 34 | 35 | ## JSON-RPC message types 36 | 37 | The JSON-RPC message types are represented by an ADT defined in 38 | [JsonRpcMessage.scala](scala-json-rpc/src/main/scala/com/dhpcs/jsonrpc/JsonRpcMessage.scala). 39 | The JsonRpcMessage 40 | [JsonRpcMessageSpec.scala](scala-json-rpc/src/test/scala/com/dhpcs/jsonrpc/JsonRpcMessageSpec.scala) 41 | shows how they appear when marshalled to and from JSON. 42 | 43 | Note that typical usage does _not_ involve the direct construction of the low 44 | level JSON-RPC message types. The recommended approach is to make use of the 45 | writers provided by the companion bases as demonstrated in the previously 46 | linked marshalling specification. 47 | 48 | The companion object for the `JsonRpcMessage` trait provides a play-json Format 49 | typeclass instance that can read and write "raw" `JsonRpcMessage` types. If you 50 | are reading messages received over e.g. a websocket connection and thus don't 51 | know what message type to expect at a given poiint, you can simply match on the 52 | subtype of the result and then use the appropriate application specific 53 | companion to unmarshall the content to one of your application protocol's 54 | types. Similarly, when marshalling your application protocol's types, you'll 55 | first pass them to the appropriate application specific companion and then 56 | format the result of that as JSON, implicitly making use of the relevant 57 | provided typeclass instance. 58 | 59 | 60 | ## License 61 | 62 | scala-json-rpc is licensed under the Apache 2 License. 63 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val `scala-json-rpc` = project 2 | .in(file("scala-json-rpc")) 3 | .settings( 4 | name := "scala-json-rpc" 5 | ) 6 | .settings( 7 | libraryDependencies ++= Seq( 8 | "com.typesafe.play" %% "play-json" % "2.6.9", 9 | "org.scalatest" %% "scalatest" % "3.0.5" % Test 10 | )) 11 | -------------------------------------------------------------------------------- /project/ProjectPlugin.scala: -------------------------------------------------------------------------------- 1 | import bintray.BintrayPlugin.autoImport._ 2 | import ch.epfl.scala.sbt.release.ReleaseEarlyPlugin.autoImport._ 3 | import com.lucidchart.sbt.scalafmt.ScalafmtCorePlugin 4 | import sbt.Keys._ 5 | import sbt._ 6 | 7 | object ProjectPlugin extends AutoPlugin { 8 | 9 | override def trigger: PluginTrigger = allRequirements 10 | 11 | override def globalSettings: Seq[Setting[_]] = 12 | addCommandAlias( 13 | "validate", 14 | ";reload plugins; sbt:scalafmt::test; scalafmt::test; reload return; " + 15 | "sbt:scalafmt::test; scalafmt::test; test:scalafmt::test; test" 16 | ) 17 | 18 | override def projectSettings: Seq[Setting[_]] = 19 | scalaProjectSettings ++ 20 | scalafmtProjectSettings ++ 21 | testProjectSettings ++ 22 | publishProjectSettings 23 | 24 | private lazy val scalafmtProjectSettings = Seq( 25 | ScalafmtCorePlugin.autoImport.scalafmtVersion := "1.4.0" 26 | ) 27 | 28 | private lazy val scalaProjectSettings = Seq( 29 | scalaVersion := "2.12.4", 30 | // See https://tpolecat.github.io/2017/04/25/scalac-flags.html for explanations. 31 | scalacOptions ++= Seq( 32 | "-deprecation", 33 | "-encoding", 34 | "utf-8", 35 | "-explaintypes", 36 | "-feature", 37 | "-language:existentials", 38 | "-language:experimental.macros", 39 | "-language:higherKinds", 40 | "-language:implicitConversions", 41 | "-unchecked", 42 | "-Xcheckinit", 43 | "-Xfatal-warnings", 44 | "-Xfuture", 45 | "-Xlint:adapted-args", 46 | "-Xlint:by-name-right-associative", 47 | "-Xlint:constant", 48 | "-Xlint:delayedinit-select", 49 | "-Xlint:doc-detached", 50 | "-Xlint:inaccessible", 51 | "-Xlint:infer-any", 52 | "-Xlint:missing-interpolator", 53 | "-Xlint:nullary-override", 54 | "-Xlint:nullary-unit", 55 | "-Xlint:option-implicit", 56 | "-Xlint:package-object-classes", 57 | "-Xlint:poly-implicit-overload", 58 | "-Xlint:private-shadow", 59 | "-Xlint:stars-align", 60 | "-Xlint:type-parameter-shadow", 61 | "-Xlint:unsound-match", 62 | "-Yno-adapted-args", 63 | "-Ypartial-unification", 64 | "-Ywarn-dead-code", 65 | "-Ywarn-extra-implicit", 66 | "-Ywarn-inaccessible", 67 | "-Ywarn-infer-any", 68 | "-Ywarn-nullary-override", 69 | "-Ywarn-nullary-unit", 70 | "-Ywarn-numeric-widen", 71 | "-Ywarn-unused:implicits", 72 | "-Ywarn-unused:imports", 73 | "-Ywarn-unused:locals", 74 | "-Ywarn-unused:params", 75 | "-Ywarn-unused:patvars", 76 | "-Ywarn-unused:privates", 77 | "-Ywarn-value-discard" 78 | ) 79 | ) 80 | 81 | private lazy val testProjectSettings = Seq( 82 | Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oDF") 83 | ) 84 | 85 | private lazy val publishProjectSettings = Seq( 86 | homepage := Some(url("https://github.com/dhpcs/scala-json-rpc/")), 87 | startYear := Some(2015), 88 | description := "A Scala library providing types and JSON format " + 89 | "typeclass instances for JSON-RPC 2.0 messages along with support " + 90 | "for marshalling application level commands, responses and " + 91 | "notifications via JSON-RPC 2.0.", 92 | licenses += "Apache-2.0" -> url( 93 | "https://www.apache.org/licenses/LICENSE-2.0.txt"), 94 | organization := "com.dhpcs", 95 | organizationHomepage := Some(url("https://www.dhpcs.com/")), 96 | organizationName := "dhpcs", 97 | developers := List( 98 | Developer( 99 | id = "dhpiggott", 100 | name = "David Piggott", 101 | email = "david@piggott.me.uk", 102 | url = url("https://www.dhpiggott.net/") 103 | )), 104 | scmInfo := Some( 105 | ScmInfo( 106 | browseUrl = url("https://github.com/dhpcs/scala-json-rpc/"), 107 | connection = "scm:git:https://github.com/dhpcs/scala-json-rpc.git", 108 | devConnection = Some("scm:git:git@github.com:dhpcs/scala-json-rpc.git") 109 | )), 110 | releaseEarlyEnableInstantReleases := false, 111 | releaseEarlyNoGpg := true, 112 | releaseEarlyWith := BintrayPublisher, 113 | releaseEarlyEnableSyncToMaven := false, 114 | bintrayOrganization := Some("dhpcs") 115 | ) 116 | 117 | } 118 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.2") 2 | 3 | scalafmtVersion := "1.4.0" 4 | addSbtPlugin("com.lucidchart" % "sbt-scalafmt-coursier" % "1.15") 5 | 6 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") 7 | addSbtPlugin("ch.epfl.scala" % "sbt-release-early" % "2.1.1") 8 | -------------------------------------------------------------------------------- /project/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15") 2 | -------------------------------------------------------------------------------- /scala-json-rpc/src/main/scala/com/dhpcs/jsonrpc/JsonRpcMessage.scala: -------------------------------------------------------------------------------- 1 | package com.dhpcs.jsonrpc 2 | 3 | import com.dhpcs.jsonrpc.JsonRpcMessage.ParamsOps._ 4 | import com.dhpcs.jsonrpc.JsonRpcMessage._ 5 | import play.api.libs.functional.syntax._ 6 | import play.api.libs.json.Json.toJsFieldJsValueWrapper 7 | import play.api.libs.json.Reads._ 8 | import play.api.libs.json._ 9 | 10 | import scala.collection.immutable.Seq 11 | 12 | sealed abstract class JsonRpcMessage 13 | 14 | object JsonRpcMessage { 15 | 16 | final val Version = "2.0" 17 | 18 | sealed abstract class CorrelationId 19 | 20 | object CorrelationId { 21 | implicit final lazy val CorrelationIdFormat: Format[CorrelationId] = Format( 22 | Reads { 23 | case JsNull => JsSuccess(NoCorrelationId) 24 | case JsNumber(value) => JsSuccess(NumericCorrelationId(value)) 25 | case JsString(value) => JsSuccess(StringCorrelationId(value)) 26 | case _ => JsError("error.expected.jsnumberorjsstring") 27 | }, 28 | Writes { 29 | case NoCorrelationId => JsNull 30 | case StringCorrelationId(value) => JsString(value) 31 | case NumericCorrelationId(value) => JsNumber(value) 32 | } 33 | ) 34 | } 35 | 36 | case object NoCorrelationId extends CorrelationId 37 | final case class NumericCorrelationId(value: BigDecimal) extends CorrelationId 38 | final case class StringCorrelationId(value: String) extends CorrelationId 39 | 40 | sealed abstract class Params 41 | 42 | object ParamsOps { 43 | implicit class RichSomeParamsOpt(val value: Option[SomeParams]) 44 | extends AnyVal { 45 | def lift: Params = value.getOrElse(NoParams) 46 | } 47 | implicit class RichParams(val value: Params) extends AnyVal { 48 | def unlift: Option[SomeParams] = value match { 49 | case NoParams => None 50 | case value: SomeParams => Some(value) 51 | } 52 | } 53 | } 54 | 55 | case object NoParams extends Params 56 | 57 | sealed abstract class SomeParams extends Params 58 | 59 | object SomeParams { 60 | implicit final lazy val SomeParamsFormat: Format[SomeParams] = Format( 61 | Reads { 62 | case jsValue: JsObject => JsSuccess(ObjectParams(jsValue)) 63 | case jsArray: JsArray => JsSuccess(ArrayParams(jsArray)) 64 | case _ => 65 | JsError(JsonValidationError(Seq("error.expected.jsobjectorjsarray"))) 66 | }, 67 | Writes { 68 | case ObjectParams(value) => value 69 | case ArrayParams(value) => value 70 | } 71 | ) 72 | } 73 | 74 | final case class ObjectParams(value: JsObject) extends SomeParams 75 | final case class ArrayParams(value: JsArray) extends SomeParams 76 | 77 | implicit final lazy val JsonRpcMessageFormat: Format[JsonRpcMessage] = Format( 78 | __.read(JsonRpcRequestMessage.JsonRpcRequestMessageFormat) 79 | .map(m => m: JsonRpcMessage) orElse 80 | __.read(JsonRpcRequestMessageBatch.JsonRpcRequestMessageBatchFormat) 81 | .map(m => m: JsonRpcMessage) orElse 82 | __.read(JsonRpcResponseMessage.JsonRpcResponseMessageFormat) 83 | .map(m => m: JsonRpcMessage) orElse 84 | __.read(JsonRpcResponseMessageBatch.JsonRpcResponseMessageBatchFormat) 85 | .map(m => m: JsonRpcMessage) orElse 86 | __.read(JsonRpcNotificationMessage.JsonRpcNotificationMessageFormat) 87 | .map(m => m: JsonRpcMessage) orElse 88 | Reads( 89 | _ => 90 | JsError( 91 | "not a valid request, request batch, response, response batch " + 92 | "or notification message")), 93 | Writes { 94 | case jsonRpcRequestMessage: JsonRpcRequestMessage => 95 | JsonRpcRequestMessage.JsonRpcRequestMessageFormat.writes( 96 | jsonRpcRequestMessage) 97 | case jsonRpcRequestMessageBatch: JsonRpcRequestMessageBatch => 98 | JsonRpcRequestMessageBatch.JsonRpcRequestMessageBatchFormat.writes( 99 | jsonRpcRequestMessageBatch) 100 | case jsonRpcResponseMessage: JsonRpcResponseMessage => 101 | JsonRpcResponseMessage.JsonRpcResponseMessageFormat.writes( 102 | jsonRpcResponseMessage) 103 | case jsonRpcResponseMessageBatch: JsonRpcResponseMessageBatch => 104 | JsonRpcResponseMessageBatch.JsonRpcResponseMessageBatchFormat.writes( 105 | jsonRpcResponseMessageBatch) 106 | case jsonRpcNotificationMessage: JsonRpcNotificationMessage => 107 | JsonRpcNotificationMessage.JsonRpcNotificationMessageFormat.writes( 108 | jsonRpcNotificationMessage) 109 | } 110 | ) 111 | 112 | } 113 | 114 | sealed trait JsonRpcRequestOrNotificationMessage 115 | 116 | object JsonRpcRequestOrNotificationMessage { 117 | implicit final lazy val JsonRpcRequestOrNotificationMessageFormat 118 | : OFormat[JsonRpcRequestOrNotificationMessage] = 119 | OFormat( 120 | Reads(jsValue => 121 | (__ \ "id")(jsValue) match { 122 | case Nil => 123 | jsValue 124 | .validate( 125 | JsonRpcNotificationMessage.JsonRpcNotificationMessageFormat) 126 | .map(m => m: JsonRpcRequestOrNotificationMessage) 127 | case _ => 128 | jsValue 129 | .validate(JsonRpcRequestMessage.JsonRpcRequestMessageFormat) 130 | .map(m => m: JsonRpcRequestOrNotificationMessage) 131 | }), 132 | OWrites[JsonRpcRequestOrNotificationMessage] { 133 | case jsonRpcRequestMessage: JsonRpcRequestMessage => 134 | JsonRpcRequestMessage.JsonRpcRequestMessageFormat.writes( 135 | jsonRpcRequestMessage) 136 | case jsonRpcNotificationMessage: JsonRpcNotificationMessage => 137 | JsonRpcNotificationMessage.JsonRpcNotificationMessageFormat.writes( 138 | jsonRpcNotificationMessage) 139 | } 140 | ) 141 | } 142 | 143 | final case class JsonRpcRequestMessage(method: String, 144 | params: Params, 145 | id: CorrelationId) 146 | extends JsonRpcMessage 147 | with JsonRpcRequestOrNotificationMessage 148 | 149 | object JsonRpcRequestMessage { 150 | 151 | def apply(method: String, 152 | params: JsObject, 153 | id: CorrelationId): JsonRpcRequestMessage = 154 | JsonRpcRequestMessage(method, ObjectParams(params), id) 155 | 156 | def apply(method: String, 157 | params: JsArray, 158 | id: CorrelationId): JsonRpcRequestMessage = 159 | JsonRpcRequestMessage(method, ArrayParams(params), id) 160 | 161 | implicit final lazy val JsonRpcRequestMessageFormat 162 | : OFormat[JsonRpcRequestMessage] = ( 163 | (__ \ "jsonrpc").format(verifying[String](_ == JsonRpcMessage.Version)) and 164 | (__ \ "method").format[String] and 165 | (__ \ "params").formatNullable[SomeParams] and 166 | (__ \ "id").format[CorrelationId] 167 | )( 168 | (_, method, params, id) => JsonRpcRequestMessage(method, params.lift, id), 169 | jsonRpcRequestMessage => 170 | (JsonRpcMessage.Version, 171 | jsonRpcRequestMessage.method, 172 | jsonRpcRequestMessage.params.unlift, 173 | jsonRpcRequestMessage.id) 174 | ) 175 | } 176 | 177 | final case class JsonRpcRequestMessageBatch( 178 | messages: Seq[JsonRpcRequestOrNotificationMessage]) 179 | extends JsonRpcMessage { 180 | require(messages.nonEmpty) 181 | } 182 | 183 | object JsonRpcRequestMessageBatch { 184 | implicit final lazy val JsonRpcRequestMessageBatchFormat 185 | : Format[JsonRpcRequestMessageBatch] = Format( 186 | Reads 187 | .of[Seq[JsonRpcRequestOrNotificationMessage]](verifying(_.nonEmpty)) 188 | .map(JsonRpcRequestMessageBatch(_)), 189 | Writes 190 | .of[Seq[JsonRpcRequestOrNotificationMessage]] 191 | .contramap(_.messages) 192 | ) 193 | } 194 | 195 | sealed abstract class JsonRpcResponseMessage extends JsonRpcMessage { 196 | def id: CorrelationId 197 | } 198 | 199 | object JsonRpcResponseMessage { 200 | implicit final lazy val JsonRpcResponseMessageFormat 201 | : OFormat[JsonRpcResponseMessage] = OFormat( 202 | Reads(jsValue => 203 | (__ \ "error")(jsValue) match { 204 | case Nil => 205 | jsValue 206 | .validate( 207 | JsonRpcResponseSuccessMessage.JsonRpcResponseSuccessMessageFormat) 208 | .map(m => m: JsonRpcResponseMessage) 209 | case _ => 210 | jsValue 211 | .validate( 212 | JsonRpcResponseErrorMessage.JsonRpcResponseErrorMessageFormat) 213 | .map(m => m: JsonRpcResponseMessage) 214 | }), 215 | OWrites[JsonRpcResponseMessage] { 216 | case jsonRpcResponseSuccessMessage: JsonRpcResponseSuccessMessage => 217 | JsonRpcResponseSuccessMessage.JsonRpcResponseSuccessMessageFormat 218 | .writes(jsonRpcResponseSuccessMessage) 219 | case jsonRpcResponseErrorMessage: JsonRpcResponseErrorMessage => 220 | JsonRpcResponseErrorMessage.JsonRpcResponseErrorMessageFormat.writes( 221 | jsonRpcResponseErrorMessage) 222 | } 223 | ) 224 | } 225 | 226 | final case class JsonRpcResponseSuccessMessage(result: JsValue, 227 | id: CorrelationId) 228 | extends JsonRpcResponseMessage 229 | 230 | object JsonRpcResponseSuccessMessage { 231 | implicit final lazy val JsonRpcResponseSuccessMessageFormat 232 | : OFormat[JsonRpcResponseSuccessMessage] = ( 233 | (__ \ "jsonrpc").format(verifying[String](_ == JsonRpcMessage.Version)) and 234 | (__ \ "result").format[JsValue] and 235 | (__ \ "id").format[CorrelationId] 236 | )( 237 | (_, result, id) => JsonRpcResponseSuccessMessage(result, id), 238 | jsonRpcResponseSuccessMessage => 239 | (JsonRpcMessage.Version, 240 | jsonRpcResponseSuccessMessage.result, 241 | jsonRpcResponseSuccessMessage.id) 242 | ) 243 | } 244 | 245 | sealed abstract case class JsonRpcResponseErrorMessage(code: Int, 246 | message: String, 247 | data: Option[JsValue], 248 | id: CorrelationId) 249 | extends JsonRpcResponseMessage 250 | 251 | object JsonRpcResponseErrorMessage { 252 | 253 | implicit final lazy val JsonRpcResponseErrorMessageFormat 254 | : OFormat[JsonRpcResponseErrorMessage] = ( 255 | (__ \ "jsonrpc").format(verifying[String](_ == JsonRpcMessage.Version)) and 256 | (__ \ "error" \ "code").format[Int] and 257 | (__ \ "error" \ "message").format[String] and 258 | // formatNullable allows the key and value to be completely absent 259 | (__ \ "error" \ "data").formatNullable[JsValue] and 260 | (__ \ "id").format[CorrelationId] 261 | )( 262 | (_, code, message, data, id) => 263 | new JsonRpcResponseErrorMessage(code, message, data, id) {}, 264 | jsonRpcResponseErrorMessage => 265 | (JsonRpcMessage.Version, 266 | jsonRpcResponseErrorMessage.code, 267 | jsonRpcResponseErrorMessage.message, 268 | jsonRpcResponseErrorMessage.data, 269 | jsonRpcResponseErrorMessage.id) 270 | ) 271 | 272 | final val ReservedErrorCodeFloor: Int = -32768 273 | final val ReservedErrorCodeCeiling: Int = -32000 274 | 275 | final val ParseErrorCode: Int = -32700 276 | final val InvalidRequestCode: Int = -32600 277 | final val MethodNotFoundCode: Int = -32601 278 | final val InvalidParamsCode: Int = -32602 279 | final val InternalErrorCode: Int = -32603 280 | final val ServerErrorCodeFloor: Int = -32099 281 | final val ServerErrorCodeCeiling: Int = -32000 282 | 283 | def parseError(exception: Throwable, 284 | id: CorrelationId): JsonRpcResponseErrorMessage = rpcError( 285 | ParseErrorCode, 286 | message = "Parse error", 287 | meaning = 288 | "Invalid JSON was received by the server.\nAn error occurred on the " + 289 | "server while parsing the JSON text.", 290 | error = Some(JsString(exception.getMessage)), 291 | id 292 | ) 293 | 294 | def invalidRequest(error: JsError, 295 | id: CorrelationId): JsonRpcResponseErrorMessage = 296 | rpcError( 297 | InvalidRequestCode, 298 | message = "Invalid Request", 299 | meaning = "The JSON sent is not a valid Request object.", 300 | error = Some(JsError.toJson(error)), 301 | id 302 | ) 303 | 304 | def methodNotFound(method: String, 305 | id: CorrelationId): JsonRpcResponseErrorMessage = rpcError( 306 | MethodNotFoundCode, 307 | message = "Method not found", 308 | meaning = "The method does not exist / is not available.", 309 | error = Some(JsString(s"""The method "$method" is not implemented.""")), 310 | id 311 | ) 312 | 313 | def invalidParams(error: JsError, 314 | id: CorrelationId): JsonRpcResponseErrorMessage = 315 | rpcError( 316 | InvalidParamsCode, 317 | message = "Invalid params", 318 | meaning = "Invalid method parameter(s).", 319 | error = Some(JsError.toJson(error)), 320 | id 321 | ) 322 | 323 | def internalError(error: Option[JsValue], 324 | id: CorrelationId): JsonRpcResponseErrorMessage = rpcError( 325 | InternalErrorCode, 326 | message = "Internal error", 327 | meaning = "Internal JSON-RPC error.", 328 | error, 329 | id 330 | ) 331 | 332 | def serverError(code: Int, 333 | error: Option[JsValue], 334 | id: CorrelationId): JsonRpcResponseErrorMessage = { 335 | require(code >= ServerErrorCodeFloor && code <= ServerErrorCodeCeiling) 336 | rpcError( 337 | code, 338 | message = "Server error", 339 | meaning = "Something went wrong in the receiving application.", 340 | error, 341 | id 342 | ) 343 | } 344 | 345 | private[this] def rpcError(code: Int, 346 | message: String, 347 | meaning: String, 348 | error: Option[JsValue], 349 | id: CorrelationId): JsonRpcResponseErrorMessage = 350 | new JsonRpcResponseErrorMessage( 351 | code, 352 | message, 353 | data = 354 | Some( 355 | Json.obj( 356 | ("meaning" -> toJsFieldJsValueWrapper(meaning)) +: 357 | error.toSeq.map(error => 358 | "error" -> toJsFieldJsValueWrapper(error)): _* 359 | ) 360 | ), 361 | id 362 | ) {} 363 | 364 | def applicationError(code: Int, 365 | message: String, 366 | data: Option[JsValue], 367 | id: CorrelationId): JsonRpcResponseErrorMessage = { 368 | require(code > ReservedErrorCodeCeiling || code < ReservedErrorCodeFloor) 369 | new JsonRpcResponseErrorMessage( 370 | code, 371 | message, 372 | data, 373 | id 374 | ) {} 375 | } 376 | } 377 | 378 | final case class JsonRpcResponseMessageBatch( 379 | messages: Seq[JsonRpcResponseMessage]) 380 | extends JsonRpcMessage { 381 | require(messages.nonEmpty) 382 | } 383 | 384 | object JsonRpcResponseMessageBatch { 385 | // This prevents a cyclic dependency on JsonRpcMessageFormat that would 386 | // otherwise be chosen during implicit resolution due to Writes being 387 | // contravariant, combined with https://issues.scala-lang.org/browse/SI-2509. 388 | // See also: https://github.com/playframework/play-json/issues/51. 389 | import JsonRpcResponseMessage.JsonRpcResponseMessageFormat 390 | implicit final lazy val JsonRpcResponseMessageBatchFormat 391 | : Format[JsonRpcResponseMessageBatch] = Format( 392 | Reads 393 | .of[Seq[JsonRpcResponseMessage]](verifying(_.nonEmpty)) 394 | .map(JsonRpcResponseMessageBatch(_)), 395 | Writes 396 | .of[Seq[JsonRpcResponseMessage]] 397 | .contramap(_.messages) 398 | ) 399 | } 400 | 401 | final case class JsonRpcNotificationMessage(method: String, params: Params) 402 | extends JsonRpcMessage 403 | with JsonRpcRequestOrNotificationMessage 404 | 405 | object JsonRpcNotificationMessage { 406 | 407 | def apply(method: String, params: JsObject): JsonRpcNotificationMessage = 408 | JsonRpcNotificationMessage(method, ObjectParams(params)) 409 | 410 | def apply(method: String, params: JsArray): JsonRpcNotificationMessage = 411 | JsonRpcNotificationMessage(method, ArrayParams(params)) 412 | 413 | implicit final lazy val JsonRpcNotificationMessageFormat 414 | : OFormat[JsonRpcNotificationMessage] = ( 415 | (__ \ "jsonrpc").format(verifying[String](_ == JsonRpcMessage.Version)) and 416 | (__ \ "method").format[String] and 417 | (__ \ "params").formatNullable[SomeParams] 418 | )( 419 | (_, method, params) => JsonRpcNotificationMessage(method, params.lift), 420 | jsonRpcNotificationMessage => 421 | (JsonRpcMessage.Version, 422 | jsonRpcNotificationMessage.method, 423 | jsonRpcNotificationMessage.params.unlift) 424 | ) 425 | } 426 | -------------------------------------------------------------------------------- /scala-json-rpc/src/main/scala/com/dhpcs/jsonrpc/MessageCompanions.scala: -------------------------------------------------------------------------------- 1 | package com.dhpcs.jsonrpc 2 | 3 | import com.dhpcs.jsonrpc.JsonRpcMessage._ 4 | import play.api.libs.json._ 5 | 6 | import scala.language.existentials 7 | import scala.reflect.ClassTag 8 | 9 | trait CommandCompanion[A] { 10 | 11 | private[this] lazy val (methodReads, classWrites) = CommandFormats 12 | 13 | protected[this] val CommandFormats: ( 14 | Map[String, Reads[_ <: A]], 15 | Map[Class[_], (String, OWrites[_ <: A])] 16 | ) 17 | 18 | def read(jsonRpcRequestMessage: JsonRpcRequestMessage): JsResult[_ <: A] = 19 | methodReads.get(jsonRpcRequestMessage.method) match { 20 | case None => JsError(s"unknown method ${jsonRpcRequestMessage.method}") 21 | case Some(reads) => 22 | jsonRpcRequestMessage.params match { 23 | case NoParams => JsError("command parameters must be given") 24 | case ArrayParams(_) => JsError("command parameters must be named") 25 | case ObjectParams(value) => 26 | reads.reads(value) match { 27 | // We do this just to reset the path in the success case. 28 | case JsError(invalid) => JsError(invalid) 29 | case JsSuccess(valid, _) => JsSuccess(valid) 30 | } 31 | } 32 | } 33 | 34 | def write[B <: A](command: B, id: CorrelationId): JsonRpcRequestMessage = { 35 | val (method, writes) = 36 | classWrites.getOrElse(command.getClass, 37 | throw new IllegalArgumentException( 38 | s"No format found for ${command.getClass}")) 39 | val bWrites = writes.asInstanceOf[OWrites[B]] 40 | JsonRpcRequestMessage(method, bWrites.writes(command), id) 41 | } 42 | } 43 | 44 | trait ResponseCompanion[A] { 45 | 46 | private[this] lazy val (methodReads, classWrites) = ResponseFormats 47 | 48 | protected[this] val ResponseFormats: ( 49 | Map[String, Reads[_ <: A]], 50 | Map[Class[_], (String, Writes[_ <: A])] 51 | ) 52 | 53 | def read(jsonRpcResponseSuccessMessage: JsonRpcResponseSuccessMessage, 54 | method: String): JsResult[_ <: A] = 55 | methodReads(method).reads(jsonRpcResponseSuccessMessage.result) match { 56 | // We do this just to reset the path in the success case. 57 | case JsError(invalid) => JsError(invalid) 58 | case JsSuccess(valid, _) => JsSuccess(valid) 59 | } 60 | 61 | def write[B <: A](response: B, 62 | id: CorrelationId): JsonRpcResponseSuccessMessage = { 63 | val (_, writes) = 64 | classWrites.getOrElse(response.getClass, 65 | throw new IllegalArgumentException( 66 | s"No format found for ${response.getClass}")) 67 | val bWrites = writes.asInstanceOf[Writes[B]] 68 | JsonRpcResponseSuccessMessage(bWrites.writes(response), id) 69 | } 70 | } 71 | 72 | trait NotificationCompanion[A] { 73 | 74 | private[this] lazy val (methodReads, classWrites) = NotificationFormats 75 | 76 | protected[this] val NotificationFormats: (Map[String, Reads[_ <: A]], 77 | Map[Class[_], 78 | (String, OWrites[_ <: A])]) 79 | 80 | def read(jsonRpcNotificationMessage: JsonRpcNotificationMessage) 81 | : JsResult[_ <: A] = 82 | methodReads.get(jsonRpcNotificationMessage.method) match { 83 | case None => 84 | JsError(s"unknown method ${jsonRpcNotificationMessage.method}") 85 | case Some(reads) => 86 | jsonRpcNotificationMessage.params match { 87 | case NoParams => JsError("notification parameters must be given") 88 | case ArrayParams(_) => 89 | JsError("notification parameters must be named") 90 | case ObjectParams(value) => 91 | reads.reads(value) match { 92 | // We do this just to reset the path in the success case. 93 | case JsError(invalid) => JsError(invalid) 94 | case JsSuccess(valid, _) => JsSuccess(valid) 95 | } 96 | } 97 | } 98 | 99 | def write[B <: A](notification: B): JsonRpcNotificationMessage = { 100 | val (method, writes) = 101 | classWrites.getOrElse(notification.getClass, 102 | throw new IllegalArgumentException( 103 | s"No format found for ${notification.getClass}")) 104 | val bWrites = writes.asInstanceOf[OWrites[B]] 105 | JsonRpcNotificationMessage(method, bWrites.writes(notification)) 106 | } 107 | } 108 | 109 | object Message { 110 | 111 | implicit class MessageFormat[A: ClassTag]( 112 | methodAndFormat: (String, Format[A])) { 113 | private[Message] val classTag = implicitly[ClassTag[A]] 114 | private[Message] val (method, format) = methodAndFormat 115 | } 116 | 117 | object MessageFormats { 118 | def apply[A, W[_] <: Writes[_]](messageFormats: MessageFormat[_ <: A]*) 119 | : (Map[String, Reads[_ <: A]], Map[Class[_], (String, W[_ <: A])]) = { 120 | val methods = messageFormats.map(_.method) 121 | require( 122 | methods == methods.distinct, 123 | "Duplicate methods: " + methods.mkString(", ") 124 | ) 125 | val classes = messageFormats.map(_.classTag.runtimeClass) 126 | require( 127 | classes == classes.distinct, 128 | "Duplicate classes: " + classes.mkString(", ") 129 | ) 130 | val reads = messageFormats.map( 131 | messageFormat => 132 | messageFormat.method -> 133 | messageFormat.format) 134 | val writes = messageFormats.map( 135 | messageFormat => 136 | messageFormat.classTag.runtimeClass -> 137 | (messageFormat.method -> messageFormat.format 138 | .asInstanceOf[W[_ <: A]])) 139 | (reads.toMap, writes.toMap) 140 | } 141 | } 142 | 143 | def objectFormat[A](o: A): OFormat[A] = OFormat( 144 | _.validate[JsObject].map(_ => o), 145 | (_: A) => JsObject.empty 146 | ) 147 | 148 | } 149 | -------------------------------------------------------------------------------- /scala-json-rpc/src/test/scala/com/dhpcs/jsonrpc/JsonRpcMessageSpec.scala: -------------------------------------------------------------------------------- 1 | package com.dhpcs.jsonrpc 2 | 3 | import com.dhpcs.jsonrpc.JsonRpcMessage._ 4 | import org.scalatest.FreeSpec 5 | import play.api.libs.json._ 6 | 7 | import scala.collection.immutable.Seq 8 | 9 | class JsonRpcMessageSpec extends FreeSpec { 10 | 11 | "An invalid JsValue" - { 12 | val json = Json.parse("{}") 13 | val jsError = JsError( 14 | "not a valid request, request batch, response, response batch or " + 15 | "notification message") 16 | s"fails to decode with error $jsError" in assert( 17 | Json.fromJson[JsonRpcMessage](json) === jsError 18 | ) 19 | } 20 | 21 | "A JsonRpcRequestMessage" - { 22 | "with an incorrect version" - { 23 | val json = Json.parse( 24 | """ 25 | |{ 26 | | "jsonrpc" : "3.0", 27 | | "method" : "testMethod", 28 | | "params" : { "param1" : "param1", "param2" : "param2" }, 29 | | "id" : 0 30 | |}""".stripMargin 31 | ) 32 | val jsError = JsError(__ \ "jsonrpc", "error.invalid") 33 | s"fails to decode with error $jsError" in assert( 34 | Json.fromJson[JsonRpcRequestMessage](json) === jsError 35 | ) 36 | } 37 | "with version of the wrong type" - { 38 | val json = 39 | Json.parse(""" 40 | |{ 41 | | "jsonrpc" : 2.0, 42 | | "method" : "testMethod", 43 | | "params" : { "param1" : "param1", "param2" : "param2" }, 44 | | "id" : 0 45 | |}""".stripMargin) 46 | val jsError = JsError(__ \ "jsonrpc", "error.expected.jsstring") 47 | s"fails to decode with error $jsError" in assert( 48 | Json.fromJson[JsonRpcRequestMessage](json) === jsError 49 | ) 50 | } 51 | "without a version" - { 52 | val json = Json.parse( 53 | """ 54 | |{ 55 | | "method" : "testMethod", 56 | | "params" : { "param1" : "param1", "param2" : "param2" }, 57 | | "id" : 0 58 | |}""".stripMargin 59 | ) 60 | val jsError = JsError(__ \ "jsonrpc", "error.path.missing") 61 | s"fails to decode with error $jsError" in assert( 62 | Json.fromJson[JsonRpcRequestMessage](json) === jsError 63 | ) 64 | } 65 | "with method of the wrong type" - { 66 | val json = 67 | Json.parse( 68 | """ 69 | |{ 70 | | "jsonrpc" : "2.0", 71 | | "method" : 3.0, 72 | | "params" : { "param1" : "param1", "param2" : "param2" }, 73 | | "id" : 0 74 | |}""".stripMargin 75 | ) 76 | val jsError = JsError(__ \ "method", "error.expected.jsstring") 77 | s"fails to decode with error $jsError" in assert( 78 | Json.fromJson[JsonRpcRequestMessage](json) === jsError 79 | ) 80 | } 81 | "without a method" - { 82 | val json = 83 | Json.parse( 84 | """ 85 | |{ 86 | | "jsonrpc" : "2.0", 87 | | "params" : { "param1" : "param1", "param2" : "param2" }, 88 | | "id" : 0 89 | |}""".stripMargin 90 | ) 91 | val jsError = JsError(__ \ "method", "error.path.missing") 92 | s"fails to decode with error $jsError" in assert( 93 | Json.fromJson[JsonRpcRequestMessage](json) === jsError 94 | ) 95 | } 96 | "with params of the wrong type" - { 97 | val json = Json.parse( 98 | """ 99 | |{ 100 | | "jsonrpc" : "2.0", 101 | | "method" : "testMethod", 102 | | "params" : "params", 103 | | "id" : 0 104 | |}""".stripMargin 105 | ) 106 | val jsError = JsError(__ \ "params", "error.expected.jsobjectorjsarray") 107 | s"fails to decode with error $jsError" in assert( 108 | Json.fromJson[JsonRpcRequestMessage](json) === jsError 109 | ) 110 | } 111 | "without params" - { 112 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 113 | "testMethod", 114 | NoParams, 115 | NumericCorrelationId(1) 116 | ) 117 | val jsonRpcRequestMessageJson = Json.parse( 118 | """ 119 | |{ 120 | | "jsonrpc" : "2.0", 121 | | "method" : "testMethod", 122 | | "id" : 1 123 | |}""".stripMargin 124 | ) 125 | s"decodes to $jsonRpcRequestMessage" in assert( 126 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 127 | jsonRpcRequestMessage) 128 | ) 129 | s"encodes to $jsonRpcRequestMessageJson" in assert( 130 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 131 | ) 132 | } 133 | "without an id" - { 134 | val json = Json.parse( 135 | """ 136 | |{ 137 | | "jsonrpc" : "2.0", 138 | | "method" : "testMethod", 139 | | "params" : { "param1" : "param1", "param2" : "param2" } 140 | |}""".stripMargin 141 | ) 142 | val jsError = JsError(__ \ "id", "error.path.missing") 143 | s"fails to decode with error $jsError" in assert( 144 | Json.fromJson[JsonRpcRequestMessage](json) === jsError 145 | ) 146 | } 147 | "with a params array" - { 148 | "and a null id" - { 149 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 150 | method = "testMethod", 151 | Json.arr( 152 | "param1", 153 | "param2" 154 | ), 155 | NoCorrelationId 156 | ) 157 | val jsonRpcRequestMessageJson = Json.parse( 158 | """ 159 | |{ 160 | | "jsonrpc" : "2.0", 161 | | "method" : "testMethod", 162 | | "params" : [ "param1", "param2" ], 163 | | "id" : null 164 | |}""".stripMargin 165 | ) 166 | s"decodes to $jsonRpcRequestMessage" in assert( 167 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 168 | jsonRpcRequestMessage) 169 | ) 170 | s"encodes to $jsonRpcRequestMessageJson" in assert( 171 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 172 | ) 173 | } 174 | "and a string id" - { 175 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 176 | method = "testMethod", 177 | Json.arr( 178 | "param1", 179 | "param2" 180 | ), 181 | StringCorrelationId("one") 182 | ) 183 | val jsonRpcRequestMessageJson = Json.parse( 184 | """ 185 | |{ 186 | | "jsonrpc" : "2.0", 187 | | "method" : "testMethod", 188 | | "params" : [ "param1", "param2" ], 189 | | "id" : "one" 190 | |}""".stripMargin 191 | ) 192 | s"decodes to $jsonRpcRequestMessage" in assert( 193 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 194 | jsonRpcRequestMessage) 195 | ) 196 | s"encodes to $jsonRpcRequestMessageJson" in assert( 197 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 198 | ) 199 | } 200 | "and a numeric id" - { 201 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 202 | method = "testMethod", 203 | Json.arr( 204 | "param1", 205 | "param2" 206 | ), 207 | NumericCorrelationId(1) 208 | ) 209 | val jsonRpcRequestMessageJson = Json.parse( 210 | """ 211 | |{ 212 | | "jsonrpc" : "2.0", 213 | | "method" : "testMethod", 214 | | "params" : [ "param1", "param2" ], 215 | | "id" : 1 216 | |}""".stripMargin 217 | ) 218 | s"decodes to $jsonRpcRequestMessage" in assert( 219 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 220 | jsonRpcRequestMessage) 221 | ) 222 | s"encodes to $jsonRpcRequestMessageJson" in assert( 223 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 224 | ) 225 | "with a fractional part" - { 226 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 227 | method = "testMethod", 228 | Json.arr( 229 | "param1", 230 | "param2" 231 | ), 232 | NumericCorrelationId(1.1) 233 | ) 234 | val jsonRpcRequestMessageJson = Json.parse( 235 | """{ 236 | | "jsonrpc" : "2.0", 237 | | "method" : "testMethod", 238 | | "params" : [ "param1", "param2" ], 239 | | "id" : 1.1 240 | |}""".stripMargin 241 | ) 242 | s"decodes to $jsonRpcRequestMessage" in assert( 243 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 244 | jsonRpcRequestMessage) 245 | ) 246 | s"encodes to $jsonRpcRequestMessageJson" in assert( 247 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 248 | ) 249 | } 250 | } 251 | } 252 | "with a params object" - { 253 | "and a null id" - { 254 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 255 | method = "testMethod", 256 | Json.obj( 257 | "param1" -> "param1", 258 | "param2" -> "param2" 259 | ), 260 | NoCorrelationId 261 | ) 262 | val jsonRpcRequestMessageJson = Json.parse( 263 | """ 264 | |{ 265 | | "jsonrpc" : "2.0", 266 | | "method" : "testMethod", 267 | | "params" : { "param1" : "param1", "param2" : "param2" }, 268 | | "id" : null 269 | |}""".stripMargin 270 | ) 271 | s"decodes to $jsonRpcRequestMessage" in assert( 272 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 273 | jsonRpcRequestMessage) 274 | ) 275 | s"encodes to $jsonRpcRequestMessageJson" in assert( 276 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 277 | ) 278 | } 279 | "and a string id" - { 280 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 281 | method = "testMethod", 282 | Json.obj( 283 | "param1" -> "param1", 284 | "param2" -> "param2" 285 | ), 286 | StringCorrelationId("one") 287 | ) 288 | val jsonRpcRequestMessageJson = Json.parse( 289 | """ 290 | |{ 291 | | "jsonrpc" : "2.0", 292 | | "method" : "testMethod", 293 | | "params" : { "param1" : "param1", "param2" : "param2" }, 294 | | "id" : "one" 295 | |}""".stripMargin 296 | ) 297 | s"decodes to $jsonRpcRequestMessage" in assert( 298 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 299 | jsonRpcRequestMessage) 300 | ) 301 | s"encodes to $jsonRpcRequestMessageJson" in assert( 302 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 303 | ) 304 | } 305 | "and a numeric id" - { 306 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 307 | method = "testMethod", 308 | Json.obj( 309 | "param1" -> "param1", 310 | "param2" -> "param2" 311 | ), 312 | NumericCorrelationId(1) 313 | ) 314 | val jsonRpcRequestMessageJson = Json.parse( 315 | """ 316 | |{ 317 | | "jsonrpc" : "2.0", 318 | | "method" : "testMethod", 319 | | "params" : { "param1" : "param1", "param2" : "param2" }, 320 | | "id" : 1 321 | |}""".stripMargin 322 | ) 323 | s"decodes to $jsonRpcRequestMessage" in assert( 324 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 325 | jsonRpcRequestMessage) 326 | ) 327 | s"encodes to $jsonRpcRequestMessageJson" in assert( 328 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 329 | ) 330 | "with a fractional part" - { 331 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 332 | method = "testMethod", 333 | Json.obj( 334 | "param1" -> "param1", 335 | "param2" -> "param2" 336 | ), 337 | NumericCorrelationId(1.1) 338 | ) 339 | val jsonRpcRequestMessageJson = Json.parse( 340 | """ 341 | |{ 342 | | "jsonrpc" : "2.0", 343 | | "method" : "testMethod", 344 | | "params" : { "param1" : "param1", "param2" : "param2" }, 345 | | "id" : 1.1 346 | |}""".stripMargin 347 | ) 348 | s"decodes to $jsonRpcRequestMessage" in assert( 349 | Json.fromJson(jsonRpcRequestMessageJson) === JsSuccess( 350 | jsonRpcRequestMessage) 351 | ) 352 | s"encodes to $jsonRpcRequestMessageJson" in assert( 353 | Json.toJson(jsonRpcRequestMessage) === jsonRpcRequestMessageJson 354 | ) 355 | } 356 | } 357 | } 358 | } 359 | 360 | "A JsonRpcRequestMessageBatch" - { 361 | "with no content" - { 362 | val json = Json.parse( 363 | """ 364 | |[ 365 | |]""".stripMargin 366 | ) 367 | val jsError = JsError(__, "error.invalid") 368 | s"fails to decode with error $jsError" in assert( 369 | Json.fromJson[JsonRpcRequestMessageBatch](json) === jsError 370 | ) 371 | } 372 | "with an invalid request" - { 373 | val json = Json.parse( 374 | """ 375 | |[ 376 | | { 377 | | "jsonrpc" : "2.0", 378 | | "params" : { "param1" : "param1", "param2" : "param2" }, 379 | | "id" : 1 380 | | } 381 | |]""".stripMargin 382 | ) 383 | val jsError = JsError(__(0) \ "method", "error.path.missing") 384 | s"fails to decode with error $jsError" in assert( 385 | Json.fromJson[JsonRpcRequestMessageBatch](json) === jsError 386 | ) 387 | } 388 | "with a single request" - { 389 | val jsonRpcRequestMessageBatch = JsonRpcRequestMessageBatch( 390 | Seq( 391 | JsonRpcRequestMessage( 392 | method = "testMethod", 393 | Json.obj( 394 | "param1" -> "param1", 395 | "param2" -> "param2" 396 | ), 397 | NumericCorrelationId(1) 398 | )) 399 | ) 400 | val jsonRpcRequestMessageBatchJson = Json.parse( 401 | """[ 402 | | { 403 | | "jsonrpc" : "2.0", 404 | | "method" : "testMethod", 405 | | "params" : { "param1" : "param1", "param2" : "param2" }, 406 | | "id" : 1 407 | | } 408 | |]""".stripMargin 409 | ) 410 | s"decodes to $jsonRpcRequestMessageBatch" in assert( 411 | Json.fromJson(jsonRpcRequestMessageBatchJson) === JsSuccess( 412 | jsonRpcRequestMessageBatch) 413 | ) 414 | s"encodes to $jsonRpcRequestMessageBatchJson" in assert( 415 | Json 416 | .toJson(jsonRpcRequestMessageBatch) === jsonRpcRequestMessageBatchJson 417 | ) 418 | } 419 | "with an invalid notification" - { 420 | val json = Json.parse( 421 | """ 422 | |[ 423 | | { 424 | | "jsonrpc" : "2.0" 425 | | } 426 | |]""".stripMargin 427 | ) 428 | val jsError = JsError(__(0) \ "method", "error.path.missing") 429 | s"fails to decode with error $jsError" in assert( 430 | Json.fromJson[JsonRpcRequestMessageBatch](json) === jsError 431 | ) 432 | } 433 | "with a single notification" - { 434 | val jsonRpcRequestMessageBatch = JsonRpcRequestMessageBatch( 435 | Seq( 436 | JsonRpcNotificationMessage( 437 | method = "testMethod", 438 | Json.obj( 439 | "param1" -> "param1", 440 | "param2" -> "param2" 441 | ) 442 | ) 443 | ) 444 | ) 445 | val jsonRpcRequestMessageBatchJson = Json.parse( 446 | """ 447 | |[ 448 | | { 449 | | "jsonrpc" : "2.0", 450 | | "method" : "testMethod", 451 | | "params" : { "param1" : "param1", "param2" : "param2" } 452 | | } 453 | |]""".stripMargin 454 | ) 455 | s"decodes to $jsonRpcRequestMessageBatch" in assert( 456 | Json.fromJson(jsonRpcRequestMessageBatchJson) === JsSuccess( 457 | jsonRpcRequestMessageBatch) 458 | ) 459 | s"encodes to $jsonRpcRequestMessageBatchJson" in assert( 460 | Json.toJson(jsonRpcRequestMessageBatch) === 461 | jsonRpcRequestMessageBatchJson 462 | ) 463 | } 464 | } 465 | 466 | "A JsonRpcResponseMessage" - { 467 | "with an incorrect version" - { 468 | val json = Json.parse( 469 | """ 470 | |{ 471 | | "jsonrpc" : "3.0", 472 | | "result" : { "param1" : "param1", "param2" : "param2" }, 473 | | "id" : 0 474 | |}""".stripMargin 475 | ) 476 | val jsError = JsError(__ \ "jsonrpc", "error.invalid") 477 | s"fails to decode with error $jsError" in assert( 478 | Json.fromJson[JsonRpcResponseMessage](json) === jsError 479 | ) 480 | } 481 | "with version of the wrong type" - { 482 | val json = Json.parse( 483 | """ 484 | |{ 485 | | "jsonrpc" :2.0, 486 | | "result" : { "param1" : "param1", "param2" : "param2" }, 487 | | "id" : 0 488 | |}""".stripMargin 489 | ) 490 | val jsError = JsError(__ \ "jsonrpc", "error.expected.jsstring") 491 | s"fails to decode with error $jsError" in assert( 492 | Json.fromJson[JsonRpcResponseMessage](json) === jsError 493 | ) 494 | } 495 | "without a version" - { 496 | val json = Json.parse( 497 | """ 498 | |{ 499 | | "result" : { "param1" : "param1", "param2" : "param2" }, 500 | | "id" : 0 501 | |}""".stripMargin 502 | ) 503 | val jsError = JsError(__ \ "jsonrpc", "error.path.missing") 504 | s"fails to decode with error $jsError" in assert( 505 | Json.fromJson[JsonRpcResponseMessage](json) === jsError 506 | ) 507 | } 508 | "with an error of the wrong type" - { 509 | val json = Json.parse( 510 | """ 511 | |{ 512 | | "jsonrpc" : "2.0", 513 | | "error" : "error", 514 | | "id" : 0 515 | |}""".stripMargin 516 | ) 517 | val jsError = JsError( 518 | Seq( 519 | (__ \ "error" \ "code", 520 | Seq(JsonValidationError("error.path.missing"))), 521 | (__ \ "error" \ "message", 522 | Seq(JsonValidationError("error.path.missing"))) 523 | )) 524 | s"fails to decode with error $jsError" in assert( 525 | Json.fromJson[JsonRpcResponseMessage](json) === jsError 526 | ) 527 | } 528 | "without an error or a result" - { 529 | val json = Json.parse( 530 | """ 531 | |{ 532 | | "jsonrpc" : "2.0", 533 | | "id" : 0 534 | |}""".stripMargin 535 | ) 536 | val jsError = JsError(__ \ "result", "error.path.missing") 537 | s"fails to decode with error $jsError" in assert( 538 | Json.fromJson[JsonRpcResponseMessage](json) === jsError 539 | ) 540 | } 541 | "without an id" - { 542 | val json = Json.parse( 543 | """ 544 | |{ 545 | | "jsonrpc" : "2.0", 546 | | "result" : { "param1" : "param1", "param2" : "param2" } 547 | |}""".stripMargin 548 | ) 549 | val jsError = JsError(__ \ "id", "error.path.missing") 550 | s"fails to decode with error $jsError" in assert( 551 | Json.fromJson[JsonRpcResponseMessage](json) === jsError 552 | ) 553 | } 554 | "with a parse error" - { 555 | val jsonRpcResponseMessage = JsonRpcResponseErrorMessage.parseError( 556 | new Throwable("Boom"), 557 | NumericCorrelationId(1) 558 | ) 559 | val jsonRpcResponseMessageJson = Json.parse( 560 | """ 561 | |{ 562 | | "jsonrpc" : "2.0", 563 | | "error" : { 564 | | "code" : -32700, 565 | | "message" : "Parse error", 566 | | "data" : { 567 | | "meaning" : "Invalid JSON was received by the server.\nAn error occurred on the server while parsing the JSON text.", 568 | | "error" : "Boom" 569 | | } 570 | | }, 571 | | "id" : 1 572 | |}""".stripMargin 573 | ) 574 | s"decodes to $jsonRpcResponseMessage" in assert( 575 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 576 | jsonRpcResponseMessage) 577 | ) 578 | s"encodes to $jsonRpcResponseMessageJson" in assert( 579 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 580 | ) 581 | } 582 | "with an invalid request error" - { 583 | val jsonRpcResponseMessage = JsonRpcResponseErrorMessage.invalidRequest( 584 | error = JsError(__ \ "method", "error.path.missing"), 585 | NumericCorrelationId(1) 586 | ) 587 | val jsonRpcResponseMessageJson = Json.parse( 588 | """ 589 | |{ 590 | | "jsonrpc" : "2.0", 591 | | "error" : { 592 | | "code" : -32600, 593 | | "message" : "Invalid Request", 594 | | "data" : { 595 | | "meaning" : "The JSON sent is not a valid Request object.", 596 | | "error" : { 597 | | "obj.method" : [ { 598 | | "msg" : [ "error.path.missing" ], 599 | | "args" : [] 600 | | } ] 601 | | } 602 | | } 603 | | }, 604 | | "id" : 1 605 | |}""".stripMargin 606 | ) 607 | s"decodes to $jsonRpcResponseMessage" in assert( 608 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 609 | jsonRpcResponseMessage) 610 | ) 611 | s"encodes to $jsonRpcResponseMessageJson" in assert( 612 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 613 | ) 614 | } 615 | "with a method not found error" - { 616 | val jsonRpcResponseMessage = JsonRpcResponseErrorMessage.methodNotFound( 617 | "foo", 618 | NumericCorrelationId(1) 619 | ) 620 | val jsonRpcResponseMessageJson = Json.parse( 621 | """ 622 | |{ 623 | | "jsonrpc" : "2.0", 624 | | "error" : { 625 | | "code" : -32601, 626 | | "message" : "Method not found", 627 | | "data" : { 628 | | "meaning" : "The method does not exist / is not available.", 629 | | "error" : "The method \"foo\" is not implemented." 630 | | } 631 | | }, 632 | | "id" : 1 633 | |}""".stripMargin 634 | ) 635 | s"decodes to $jsonRpcResponseMessage" in assert( 636 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 637 | jsonRpcResponseMessage) 638 | ) 639 | s"encodes to $jsonRpcResponseMessageJson" in assert( 640 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 641 | ) 642 | } 643 | "with an invalid params error" - { 644 | val jsonRpcResponseMessage = JsonRpcResponseErrorMessage.invalidParams( 645 | error = JsError(__ \ "arg1", "error.path.missing"), 646 | NumericCorrelationId(1) 647 | ) 648 | val jsonRpcResponseMessageJson = Json.parse( 649 | """ 650 | |{ 651 | | "jsonrpc" : "2.0", 652 | | "error" : { 653 | | "code" : -32602, 654 | | "message" : "Invalid params", 655 | | "data" : { 656 | | "meaning" : "Invalid method parameter(s).", 657 | | "error" : { 658 | | "obj.arg1" : [ { 659 | | "msg" : [ "error.path.missing" ], 660 | | "args" : [] 661 | | } ] 662 | | } 663 | | } 664 | | }, 665 | | "id" : 1 666 | |}""".stripMargin 667 | ) 668 | s"decodes to $jsonRpcResponseMessage" in assert( 669 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 670 | jsonRpcResponseMessage) 671 | ) 672 | s"encodes to $jsonRpcResponseMessageJson" in assert( 673 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 674 | ) 675 | } 676 | "with an internal error" - { 677 | val jsonRpcResponseMessage = JsonRpcResponseErrorMessage.internalError( 678 | error = None, 679 | NumericCorrelationId(1) 680 | ) 681 | val jsonRpcResponseMessageJson = Json.parse( 682 | """ 683 | |{ 684 | | "jsonrpc" : "2.0", 685 | | "error" : { "code" : -32603, 686 | | "message" : "Internal error", 687 | | "data" : { 688 | | "meaning" : "Internal JSON-RPC error." 689 | | } 690 | | }, 691 | | "id" : 1 692 | |}""".stripMargin 693 | ) 694 | s"decodes to $jsonRpcResponseMessage" in assert( 695 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 696 | jsonRpcResponseMessage) 697 | ) 698 | s"encodes to $jsonRpcResponseMessageJson" in assert( 699 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 700 | ) 701 | } 702 | "with a server error" - { 703 | val jsonRpcResponseMessage = JsonRpcResponseErrorMessage.serverError( 704 | code = JsonRpcResponseErrorMessage.ServerErrorCodeFloor, 705 | error = None, 706 | NumericCorrelationId(1) 707 | ) 708 | val jsonRpcResponseMessageJson = Json.parse( 709 | """ 710 | |{ 711 | | "jsonrpc" : "2.0", 712 | | "error" : { 713 | | "code" : -32099, 714 | | "message" : "Server error", 715 | | "data" : { 716 | | "meaning" : "Something went wrong in the receiving application." 717 | | } 718 | | }, 719 | | "id" : 1 720 | |}""".stripMargin 721 | ) 722 | s"decodes to $jsonRpcResponseMessage" in assert( 723 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 724 | jsonRpcResponseMessage) 725 | ) 726 | s"encodes to $jsonRpcResponseMessageJson" in assert( 727 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 728 | ) 729 | } 730 | "with an application error" - { 731 | val jsonRpcResponseMessage = 732 | JsonRpcResponseErrorMessage.applicationError( 733 | code = -31999, 734 | message = "Boom", 735 | data = None, 736 | NumericCorrelationId(1) 737 | ) 738 | val jsonRpcResponseMessageJson = Json.parse( 739 | """ 740 | |{ 741 | | "jsonrpc" : "2.0", 742 | | "error" : { "code" : -31999, "message" : "Boom" }, 743 | | "id" : 1 744 | |}""".stripMargin 745 | ) 746 | s"decodes to $jsonRpcResponseMessage" in assert( 747 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 748 | jsonRpcResponseMessage) 749 | ) 750 | s"encodes to $jsonRpcResponseMessageJson" in assert( 751 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 752 | ) 753 | } 754 | "with a result" - { 755 | "and a null id" - { 756 | val jsonRpcResponseMessage = JsonRpcResponseSuccessMessage( 757 | Json.obj( 758 | "param1" -> "param1", 759 | "param2" -> "param2" 760 | ), 761 | NoCorrelationId 762 | ) 763 | val jsonRpcResponseMessageJson = Json.parse( 764 | """ 765 | |{ 766 | | "jsonrpc" : "2.0", 767 | | "result" : { "param1" : "param1", "param2" : "param2" }, 768 | | "id" : null 769 | |}""".stripMargin 770 | ) 771 | s"decodes to $jsonRpcResponseMessage" in assert( 772 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 773 | jsonRpcResponseMessage) 774 | ) 775 | s"encodes to $jsonRpcResponseMessageJson" in assert( 776 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 777 | ) 778 | } 779 | "and a string id" - { 780 | val jsonRpcResponseMessage = JsonRpcResponseSuccessMessage( 781 | Json.obj( 782 | "param1" -> "param1", 783 | "param2" -> "param2" 784 | ), 785 | StringCorrelationId("one") 786 | ) 787 | val jsonRpcResponseMessageJson = Json.parse( 788 | """ 789 | |{ 790 | | "jsonrpc" : "2.0", 791 | | "result" : { "param1" : "param1", "param2" : "param2" }, 792 | | "id" : "one" 793 | |}""".stripMargin 794 | ) 795 | s"decodes to $jsonRpcResponseMessage" in assert( 796 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 797 | jsonRpcResponseMessage) 798 | ) 799 | s"encodes to $jsonRpcResponseMessageJson" in assert( 800 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 801 | ) 802 | } 803 | "and a numeric id" - { 804 | val jsonRpcResponseMessage = JsonRpcResponseSuccessMessage( 805 | Json.obj( 806 | "param1" -> "param1", 807 | "param2" -> "param2" 808 | ), 809 | NumericCorrelationId(1) 810 | ) 811 | val jsonRpcResponseMessageJson = Json.parse( 812 | """ 813 | |{ 814 | | "jsonrpc" : "2.0", 815 | | "result" : { "param1" : "param1", "param2" : "param2" }, 816 | | "id" : 1 817 | |}""".stripMargin 818 | ) 819 | s"decodes to $jsonRpcResponseMessage" in assert( 820 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 821 | jsonRpcResponseMessage) 822 | ) 823 | s"encodes to $jsonRpcResponseMessageJson" in assert( 824 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 825 | ) 826 | "with a fractional part" - { 827 | val jsonRpcResponseMessage = JsonRpcResponseSuccessMessage( 828 | Json.obj( 829 | "param1" -> "param1", 830 | "param2" -> "param2" 831 | ), 832 | NumericCorrelationId(1.1) 833 | ) 834 | val jsonRpcResponseMessageJson = Json.parse( 835 | """ 836 | |{ 837 | | "jsonrpc" : "2.0", 838 | | "result" : { "param1" : "param1", "param2" : "param2" }, 839 | | "id" : 1.1 840 | |}""".stripMargin 841 | ) 842 | s"decodes to $jsonRpcResponseMessage" in assert( 843 | Json.fromJson(jsonRpcResponseMessageJson) === JsSuccess( 844 | jsonRpcResponseMessage) 845 | ) 846 | s"encodes to $jsonRpcResponseMessageJson" in assert( 847 | Json.toJson(jsonRpcResponseMessage) === jsonRpcResponseMessageJson 848 | ) 849 | } 850 | } 851 | } 852 | } 853 | 854 | "A JsonRpcResponseMessageBatch" - { 855 | "with no content" - { 856 | val json = Json.parse( 857 | """ 858 | |[ 859 | |]""".stripMargin 860 | ) 861 | val jsError = JsError(__, "error.invalid") 862 | s"fails to decode with error $jsError" in assert( 863 | Json.fromJson[JsonRpcResponseMessageBatch](json) === jsError 864 | ) 865 | } 866 | "with an invalid response" - { 867 | val json = Json.parse( 868 | """ 869 | |[ 870 | | { 871 | | "jsonrpc" : "2.0", 872 | | "id" : 1 873 | | } 874 | |]""".stripMargin 875 | ) 876 | val jsError = JsError(__(0) \ "result", "error.path.missing") 877 | s"fails to decode with error $jsError" in assert( 878 | Json.fromJson[JsonRpcResponseMessageBatch](json) === jsError 879 | ) 880 | } 881 | "with a single response" - { 882 | val jsonRpcResponseMessageBatch = JsonRpcResponseMessageBatch( 883 | Seq( 884 | JsonRpcResponseSuccessMessage( 885 | Json.obj( 886 | "param1" -> "param1", 887 | "param2" -> "param2" 888 | ), 889 | NumericCorrelationId(1) 890 | ) 891 | ) 892 | ) 893 | val jsonRpcResponseMessageBatchJson = Json.parse( 894 | """ 895 | |[ 896 | | { 897 | | "jsonrpc" : "2.0", 898 | | "result" : { "param1" : "param1", "param2" : "param2" }, 899 | | "id" : 1 900 | | } 901 | |]""".stripMargin 902 | ) 903 | s"decodes to $jsonRpcResponseMessageBatch" in assert( 904 | Json.fromJson(jsonRpcResponseMessageBatchJson) === JsSuccess( 905 | jsonRpcResponseMessageBatch) 906 | ) 907 | s"encodes to $jsonRpcResponseMessageBatchJson" in assert( 908 | Json.toJson(jsonRpcResponseMessageBatch) === 909 | jsonRpcResponseMessageBatchJson 910 | ) 911 | } 912 | } 913 | 914 | "A JsonRpcNotificationMessage" - { 915 | "with an incorrect version" - { 916 | val json = Json.parse( 917 | """ 918 | |{ 919 | | "jsonrpc" : "3.0", 920 | | "method" : "testMethod", 921 | | "params" : { "param1" : "param1", "param2" : "param2" } 922 | |}""".stripMargin 923 | ) 924 | val jsError = JsError(__ \ "jsonrpc", "error.invalid") 925 | s"fails to decode with error $jsError" in assert( 926 | Json.fromJson[JsonRpcNotificationMessage](json) === jsError 927 | ) 928 | } 929 | "with version of the wrong type" - { 930 | val json = Json.parse( 931 | """ 932 | |{ 933 | | "jsonrpc" :2.0, 934 | | "method" : "testMethod", 935 | | "params" : { "param1" : "param1", "param2" : "param2" } 936 | |}""".stripMargin 937 | ) 938 | val jsError = JsError(__ \ "jsonrpc", "error.expected.jsstring") 939 | s"fails to decode with error $jsError" in assert( 940 | Json.fromJson[JsonRpcNotificationMessage](json) === jsError 941 | ) 942 | } 943 | "without a version" - { 944 | val json = Json.parse( 945 | """ 946 | |{ 947 | | "method" : "testMethod", 948 | | "params" : { "param1" : "param1", "param2" : "param2" } 949 | |}""".stripMargin 950 | ) 951 | val jsError = JsError(__ \ "jsonrpc", "error.path.missing") 952 | s"fails to decode with error $jsError" in assert( 953 | Json.fromJson[JsonRpcNotificationMessage](json) === jsError 954 | ) 955 | } 956 | "with method of the wrong type" - { 957 | val json = Json.parse( 958 | """ 959 | |{ 960 | | "jsonrpc" : "2.0", 961 | | "method" : 3.0, 962 | | "params" : { "param1" : "param1", "param2" : "param2" } 963 | |}""".stripMargin 964 | ) 965 | val jsError = JsError(__ \ "method", "error.expected.jsstring") 966 | s"fails to decode with error $jsError" in assert( 967 | Json.fromJson[JsonRpcNotificationMessage](json) === jsError 968 | ) 969 | } 970 | "without a method" - { 971 | val json = Json.parse( 972 | """ 973 | |{ 974 | | "jsonrpc" : "2.0", 975 | | "params" : { "param1" : "param1", "param2" : "param2" } 976 | |}""".stripMargin 977 | ) 978 | val jsError = JsError(__ \ "method", "error.path.missing") 979 | s"fails to decode with error $jsError" in assert( 980 | Json.fromJson[JsonRpcNotificationMessage](json) === jsError 981 | ) 982 | } 983 | "with params of the wrong type" - { 984 | val json = Json.parse( 985 | """ 986 | |{ 987 | | "jsonrpc" : "2.0", 988 | | "method" : "testMethod", 989 | | "params" : "params" 990 | |}""".stripMargin 991 | ) 992 | val jsError = JsError(__ \ "params", "error.expected.jsobjectorjsarray") 993 | s"fails to decode with error $jsError" in assert( 994 | Json.fromJson[JsonRpcNotificationMessage](json) === jsError 995 | ) 996 | } 997 | "without params" - { 998 | val jsonRpcNotificationMessage = JsonRpcNotificationMessage( 999 | method = "testMethod", 1000 | NoParams 1001 | ) 1002 | val jsonRpcNotificationMessageJson = Json.parse( 1003 | """ 1004 | |{ 1005 | | "jsonrpc" : "2.0", 1006 | | "method" : "testMethod" 1007 | |}""".stripMargin 1008 | ) 1009 | s"decodes to $jsonRpcNotificationMessage" in assert( 1010 | Json.fromJson(jsonRpcNotificationMessageJson) === JsSuccess( 1011 | jsonRpcNotificationMessage) 1012 | ) 1013 | s"encodes to $jsonRpcNotificationMessageJson" in assert( 1014 | Json.toJson(jsonRpcNotificationMessage) === 1015 | jsonRpcNotificationMessageJson 1016 | ) 1017 | } 1018 | "with a params array" - { 1019 | val jsonRpcNotificationMessage = JsonRpcNotificationMessage( 1020 | method = "testMethod", 1021 | Json.arr( 1022 | "param1", 1023 | "param2" 1024 | ) 1025 | ) 1026 | val jsonRpcNotificationMessageJson = Json.parse( 1027 | """ 1028 | |{ 1029 | | "jsonrpc" : "2.0", 1030 | | "method" : "testMethod", 1031 | | "params" : [ "param1", "param2" ] 1032 | |}""".stripMargin 1033 | ) 1034 | s"decodes to $jsonRpcNotificationMessage" in assert( 1035 | Json.fromJson(jsonRpcNotificationMessageJson) === JsSuccess( 1036 | jsonRpcNotificationMessage) 1037 | ) 1038 | s"encodes to $jsonRpcNotificationMessageJson" in assert( 1039 | Json.toJson(jsonRpcNotificationMessage) === 1040 | jsonRpcNotificationMessageJson 1041 | ) 1042 | } 1043 | "with a params object" - { 1044 | val jsonRpcNotificationMessage = JsonRpcNotificationMessage( 1045 | method = "testMethod", 1046 | Json.obj( 1047 | "param1" -> "param1", 1048 | "param2" -> "param2" 1049 | ) 1050 | ) 1051 | val jsonRpcNotificationMessageJson = Json.parse( 1052 | """ 1053 | |{ 1054 | | "jsonrpc" : "2.0", 1055 | | "method" : "testMethod", 1056 | | "params" : { "param1" : "param1", "param2" : "param2" } 1057 | |}""".stripMargin 1058 | ) 1059 | s"decodes to $jsonRpcNotificationMessage" in assert( 1060 | Json.fromJson(jsonRpcNotificationMessageJson) === JsSuccess( 1061 | jsonRpcNotificationMessage) 1062 | ) 1063 | s"encodes to $jsonRpcNotificationMessageJson" in assert( 1064 | Json.toJson(jsonRpcNotificationMessage) === 1065 | jsonRpcNotificationMessageJson 1066 | ) 1067 | } 1068 | } 1069 | } 1070 | -------------------------------------------------------------------------------- /scala-json-rpc/src/test/scala/com/dhpcs/jsonrpc/MessageCompanionsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.dhpcs.jsonrpc 2 | 3 | import com.dhpcs.jsonrpc.JsonRpcMessage._ 4 | import com.dhpcs.jsonrpc.Message.MessageFormats 5 | import com.dhpcs.jsonrpc.MessageCompanionsSpec._ 6 | import org.scalatest._ 7 | import play.api.libs.functional.syntax._ 8 | import play.api.libs.json.Reads.min 9 | import play.api.libs.json._ 10 | 11 | class MessageCompanionsSpec extends FreeSpec { 12 | 13 | "A Command" - { 14 | "with an invalid method" - { 15 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 16 | method = "invalidMethod", 17 | JsObject.empty, 18 | NumericCorrelationId(1) 19 | ) 20 | val jsError = JsError("unknown method invalidMethod") 21 | s"fails to decode with error $jsError" in assert( 22 | Command.read(jsonRpcRequestMessage) === jsError 23 | ) 24 | } 25 | "of type AddTransactionCommand" - { 26 | "with params of the wrong type" - { 27 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 28 | method = "addTransaction", 29 | JsArray.empty, 30 | NumericCorrelationId(1) 31 | ) 32 | val jsError = JsError(__, "command parameters must be named") 33 | s"fails to decode with error $jsError" in assert( 34 | Command.read(jsonRpcRequestMessage) === jsError 35 | ) 36 | } 37 | "with empty params" - { 38 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 39 | method = "addTransaction", 40 | JsObject.empty, 41 | NumericCorrelationId(1) 42 | ) 43 | val jsError = JsError( 44 | Seq( 45 | (__ \ "to", Seq(JsonValidationError("error.path.missing"))), 46 | (__ \ "from", Seq(JsonValidationError("error.path.missing"))), 47 | (__ \ "value", Seq(JsonValidationError("error.path.missing"))) 48 | ) 49 | ) 50 | s"fails to decode with error $jsError" in assert( 51 | Command.read(jsonRpcRequestMessage) === jsError 52 | ) 53 | } 54 | val addTransactionCommand = AddTransactionCommand( 55 | from = 0, 56 | to = 1, 57 | value = BigDecimal(1000000), 58 | description = Some("Property purchase"), 59 | metadata = Some( 60 | Json.obj( 61 | "property" -> "The TARDIS" 62 | ) 63 | ) 64 | ) 65 | val id = NumericCorrelationId(1) 66 | val jsonRpcRequestMessage = JsonRpcRequestMessage( 67 | method = "addTransaction", 68 | Json.obj( 69 | "from" -> 0, 70 | "to" -> 1, 71 | "value" -> BigDecimal(1000000), 72 | "description" -> "Property purchase", 73 | "metadata" -> Json.obj( 74 | "property" -> "The TARDIS" 75 | ) 76 | ), 77 | NumericCorrelationId(1) 78 | ) 79 | s"decodes to $addTransactionCommand" in assert( 80 | Command.read(jsonRpcRequestMessage) === 81 | JsSuccess(addTransactionCommand) 82 | ) 83 | s"encodes to $jsonRpcRequestMessage" in assert( 84 | Command.write(addTransactionCommand, id) === jsonRpcRequestMessage 85 | ) 86 | } 87 | } 88 | 89 | "A Response of type AddTransactionResponse" - { 90 | val addTransactionResponse = AddTransactionResponse(id = 0) 91 | val id = NumericCorrelationId(1) 92 | val jsonRpcResponseMessage = JsonRpcResponseSuccessMessage( 93 | JsNumber(0), 94 | NumericCorrelationId(1) 95 | ) 96 | val method = "addTransaction" 97 | s"decodes to $addTransactionResponse" in assert( 98 | Response.read(jsonRpcResponseMessage, method) === JsSuccess( 99 | addTransactionResponse) 100 | ) 101 | s"encodes to $jsonRpcResponseMessage" in assert( 102 | Response.write(addTransactionResponse, id) === jsonRpcResponseMessage 103 | ) 104 | } 105 | 106 | "A Notification" - { 107 | "with an invalid method" - { 108 | val jsonRpcNotificationMessage = JsonRpcNotificationMessage( 109 | method = "invalidMethod", 110 | JsObject.empty 111 | ) 112 | val jsError = JsError("unknown method invalidMethod") 113 | s"fails to decode with error $jsError" in assert( 114 | Notification.read(jsonRpcNotificationMessage) === jsError 115 | ) 116 | } 117 | "of type TransactionAddedNotification" - { 118 | "with params of the wrong type" - { 119 | val jsonRpcNotificationMessage = JsonRpcNotificationMessage( 120 | method = "transactionAdded", 121 | JsArray.empty 122 | ) 123 | val jsError = JsError(__, "notification parameters must be named") 124 | s"fails to decode with error $jsError" in assert( 125 | Notification.read(jsonRpcNotificationMessage) === jsError 126 | ) 127 | } 128 | "with empty params" - { 129 | val jsonRpcNotificationMessage = JsonRpcNotificationMessage( 130 | method = "transactionAdded", 131 | JsObject.empty 132 | ) 133 | val jsError = JsError(__ \ "transaction", "error.path.missing") 134 | s"fails to decode with error $jsError" in assert( 135 | Notification.read(jsonRpcNotificationMessage) === jsError 136 | ) 137 | } 138 | val clientJoinedZoneNotification = TransactionAddedNotification( 139 | Transaction( 140 | id = 0, 141 | from = 0, 142 | to = 1, 143 | value = BigDecimal(1000000) 144 | ) 145 | ) 146 | val jsonRpcNotificationMessage = JsonRpcNotificationMessage( 147 | method = "transactionAdded", 148 | params = Json.obj( 149 | "transaction" -> Json.parse( 150 | """{ "id" : 0, "from" : 0, "to" : 1, "value" : 1000000 }""") 151 | ) 152 | ) 153 | s"decodes to $clientJoinedZoneNotification" in assert( 154 | Notification.read(jsonRpcNotificationMessage) === JsSuccess( 155 | clientJoinedZoneNotification) 156 | ) 157 | s"encodes to $jsonRpcNotificationMessage" in assert( 158 | Notification 159 | .write(clientJoinedZoneNotification) === jsonRpcNotificationMessage 160 | ) 161 | } 162 | } 163 | } 164 | 165 | object MessageCompanionsSpec { 166 | 167 | private sealed abstract class Message 168 | 169 | private sealed abstract class Command extends Message 170 | private final case class UpdateAccountCommand(account: Account) 171 | extends Command 172 | private final case class AddTransactionCommand( 173 | from: Int, 174 | to: Int, 175 | value: BigDecimal, 176 | description: Option[String] = None, 177 | metadata: Option[JsObject] = None) 178 | extends Command { 179 | require(value >= 0) 180 | } 181 | 182 | private object AddTransactionCommand { 183 | implicit final val AddTransactionCommandFormat 184 | : Format[AddTransactionCommand] = ( 185 | (JsPath \ "from").format[Int] and 186 | (JsPath \ "to").format[Int] and 187 | (JsPath \ "value").format(min[BigDecimal](0)) and 188 | (JsPath \ "description").formatNullable[String] and 189 | (JsPath \ "metadata").formatNullable[JsObject] 190 | )( 191 | (from, to, value, description, metadata) => 192 | AddTransactionCommand( 193 | from, 194 | to, 195 | value, 196 | description, 197 | metadata 198 | ), 199 | addTransactionCommand => 200 | (addTransactionCommand.from, 201 | addTransactionCommand.to, 202 | addTransactionCommand.value, 203 | addTransactionCommand.description, 204 | addTransactionCommand.metadata) 205 | ) 206 | } 207 | 208 | private object Command extends CommandCompanion[Command] { 209 | override final val CommandFormats = MessageFormats( 210 | "updateAccount" -> Json.format[UpdateAccountCommand], 211 | "addTransaction" -> Json.format[AddTransactionCommand] 212 | ) 213 | } 214 | 215 | private sealed abstract class Response extends Message 216 | private case object UpdateAccountResponse extends Response 217 | private final case class AddTransactionResponse(id: Int) extends Response 218 | 219 | private object Response extends ResponseCompanion[Response] { 220 | override final val ResponseFormats = MessageFormats( 221 | "updateAccount" -> Message.objectFormat(UpdateAccountResponse), 222 | "addTransaction" -> Format[AddTransactionResponse]( 223 | Reads.of[Int].map(AddTransactionResponse), 224 | Writes.of[Int].contramap(_.id) 225 | ) 226 | ) 227 | } 228 | 229 | private sealed abstract class Notification extends Message 230 | private final case class AccountUpdatedNotification(account: Account) 231 | extends Notification 232 | private final case class TransactionAddedNotification( 233 | transaction: Transaction) 234 | extends Notification 235 | 236 | private object Notification extends NotificationCompanion[Notification] { 237 | override final val NotificationFormats = MessageFormats( 238 | "accountUpdated" -> Json.format[AccountUpdatedNotification], 239 | "transactionAdded" -> Json.format[TransactionAddedNotification] 240 | ) 241 | } 242 | 243 | private final case class Account(id: Int, 244 | name: Option[String] = None, 245 | metadata: Option[JsObject] = None) 246 | 247 | private object Account { 248 | implicit final val AccountFormat: Format[Account] = Json.format[Account] 249 | } 250 | 251 | private final case class Transaction(id: Int, 252 | from: Int, 253 | to: Int, 254 | value: BigDecimal, 255 | description: Option[String] = None, 256 | metadata: Option[JsObject] = None) { 257 | require(value >= 0) 258 | } 259 | 260 | private object Transaction { 261 | implicit final val TransactionFormat: Format[Transaction] = ( 262 | (JsPath \ "id").format[Int] and 263 | (JsPath \ "from").format[Int] and 264 | (JsPath \ "to").format[Int] and 265 | (JsPath \ "value").format(min[BigDecimal](0)) and 266 | (JsPath \ "description").formatNullable[String] and 267 | (JsPath \ "metadata").formatNullable[JsObject] 268 | )( 269 | (id, from, to, value, description, metadata) => 270 | Transaction( 271 | id, 272 | from, 273 | to, 274 | value, 275 | description, 276 | metadata 277 | ), 278 | transaction => 279 | (transaction.id, 280 | transaction.from, 281 | transaction.to, 282 | transaction.value, 283 | transaction.description, 284 | transaction.metadata) 285 | ) 286 | } 287 | } 288 | --------------------------------------------------------------------------------