From 100b3fc9719e72071433052bb961d67712a748ff Mon Sep 17 00:00:00 2001 From: Oliver Marienfeld Date: Fri, 19 Jan 2024 12:28:18 +0100 Subject: [PATCH] 118 json improvement (#121) * JSON: Make Json.arr work with and clean up a bit * JSON: Add json"" and jsonArray"" interpolators * Improve Json.obj, Json.arr and documentation * Make asMap and asList convert recursively * Implement Scala-ish JsonPointer * Documentation for JsonPointer * Fix link on README.adoc --- README.adoc | 2 +- pom.xml | 5 + .../lang/scala/testing/VerticleTesting.scala | 2 - vertx-lang-scala/build.sbt | 3 +- vertx-lang-scala/pom.xml | 19 +- .../_docs/coding/working-with-futures.md | 54 +++-- .../_docs/coding/working-with-json.md | 210 ++++++++++++++--- .../scala/io/vertx/lang/scala/json/Json.scala | 64 +++--- .../vertx/lang/scala/json/JsonPointer.scala | 188 +++++++++++++++ .../io/vertx/lang/scala/json/package.scala | 92 ++++++-- .../lang/scala/http/HttpClientTest.scala | 13 +- .../lang/scala/json/ExtensionsSpec.scala | 138 +++++++++++ .../lang/scala/json/JsonPointerSpec.scala | 209 +++++++++++++++++ .../io/vertx/lang/scala/json/JsonSpec.scala | 201 ++++++++++++++++ .../io/vertx/lang/scala/json/JsonTest.scala | 214 ------------------ 15 files changed, 1076 insertions(+), 338 deletions(-) create mode 100644 vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/JsonPointer.scala create mode 100644 vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/ExtensionsSpec.scala create mode 100644 vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonPointerSpec.scala create mode 100644 vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonSpec.scala delete mode 100644 vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonTest.scala diff --git a/README.adoc b/README.adoc index b7e6ceab3..36ba4edf7 100644 --- a/README.adoc +++ b/README.adoc @@ -7,7 +7,7 @@ Vert.x-Scala provides language bindings and helpers for using Scala with Vert.x. Please see the main documentation on the web-site for a full description: -* https://vertx.io/docs/#scala[Web-site documentation] +* http://vert-x3.github.io/vertx-lang-scala/[Web-site documentation] == Structure diff --git a/pom.xml b/pom.xml index aca5e1d5b..7957e84b2 100644 --- a/pom.xml +++ b/pom.xml @@ -76,6 +76,11 @@ scalatest_3 3.3.0-SNAP4 + + org.scalatestplus + scalacheck-1-17_3 + 3.2.17.0 + diff --git a/vertx-lang-scala-test/src/main/scala/io/vertx/lang/scala/testing/VerticleTesting.scala b/vertx-lang-scala-test/src/main/scala/io/vertx/lang/scala/testing/VerticleTesting.scala index 02a44f2a7..49f50d3c2 100644 --- a/vertx-lang-scala-test/src/main/scala/io/vertx/lang/scala/testing/VerticleTesting.scala +++ b/vertx-lang-scala-test/src/main/scala/io/vertx/lang/scala/testing/VerticleTesting.scala @@ -11,8 +11,6 @@ import org.scalatest.flatspec.AsyncFlatSpec import scala.concurrent.Await import scala.concurrent.duration.* import scala.language.postfixOps -import scala.quoted.{Expr, Quotes, Type} -import scala.reflect.ClassTag import scala.util.{Failure, Success} import org.slf4j.Logger import org.slf4j.LoggerFactory diff --git a/vertx-lang-scala/build.sbt b/vertx-lang-scala/build.sbt index eba06e82f..fbfa68e65 100644 --- a/vertx-lang-scala/build.sbt +++ b/vertx-lang-scala/build.sbt @@ -24,7 +24,8 @@ Compile / doc / scalacOptions ++= Seq( "-Ygenerate-inkuire", "-project-version", version.value, "-project-logo", "src/main/markdown/_assets/images/vertx-logo.png", - "-project-footer", s"© $currentYear Eclipse Vert.x™" + "-project-footer", s"© $currentYear Eclipse Vert.x™", + "-project-url", "https://github.com/vert-x3/vertx-lang-scala", ) lazy val root = project diff --git a/vertx-lang-scala/pom.xml b/vertx-lang-scala/pom.xml index bf94430a6..f1cfdde3a 100644 --- a/vertx-lang-scala/pom.xml +++ b/vertx-lang-scala/pom.xml @@ -358,25 +358,14 @@ test-jar test - - - co.helmethair - scalatest-junit-runner - test - - org.junit.jupiter - junit-jupiter-engine - test - - - org.junit.jupiter - junit-jupiter-api + org.scalatest + scalatest_3 test - org.scalatest - scalatest_3 + org.scalatestplus + scalacheck-1-17_3 test diff --git a/vertx-lang-scala/src/main/markdown/_docs/coding/working-with-futures.md b/vertx-lang-scala/src/main/markdown/_docs/coding/working-with-futures.md index 9b62fc4e3..39fd27cd2 100644 --- a/vertx-lang-scala/src/main/markdown/_docs/coding/working-with-futures.md +++ b/vertx-lang-scala/src/main/markdown/_docs/coding/working-with-futures.md @@ -2,32 +2,39 @@ title: Working with Futures --- -Almost every function of the Vert.x API wraps the result `T` of an operation in a `io.vertx.core.Future[T]`. This is very similar to Scala APIs returning`scala.concurrent.Future[T]`. Working with Scala Futures, however, bears some differences: +# Working with Futures + +Almost every function of the Vert.x API wraps the result `T` of an operation in a +`io.vertx.core.Future[T]`. This is very similar to Scala APIs returning +`scala.concurrent.Future[T]`. Working with Scala Futures, however, bears some differences: - _chaining_ Scala Futures can be expressed in an elegant way using `for`-comprehensions - operations like `map` or `filter` require a (`given`) `ExecutionContext` -This page explains how Vert.x Futures can be converted to Scala Futures and how we deal with the `ExecutionContext`. - +This page explains how Vert.x Futures can be converted to Scala Futures and how we deal +with the `ExecutionContext`. -# Implicit Conversion +## Implicit Conversion -The object [[io.vertx.lang.scala.ImplicitConversions]] provides _implicit conversions_. To use them, we need two imports: +The object [[io.vertx.lang.scala.ImplicitConversions]] provides _implicit conversions_. +To use them, we need two imports: ```scala import io.vertx.lang.scala.ImplicitConversions.* import scala.language.implicitConversions ``` -The import `scala.language.implicitConversions` is required to indicate our will to use implicit conversions in the current scope, thus avoiding compiler warnings. +The import `scala.language.implicitConversions` is required to indicate our will to use +implicit conversions in the current scope, thus avoiding compiler warnings. -After that, the Scala compiler applies the conversion automatically if possible. +After that, the Scala compiler applies the conversion automatically if possible. -The following example loads a file from the file system and sends it over the Event Bus. There is an implicit conversion applied here, because +The following example loads a file from the file system and sends it over the Event Bus. +There is an implicit conversion applied here, because 1. loading a file with `vertx.fileSystem.readFile` results in a `io.vertx.core.Future[Buffer]` - 2. the `for`-comprehension requires a `scala.concurrent.Future[_]` - 3. the implicit conversion `vertxFutureToScalaFuture` is in scope + 2. the `for`-comprehension requires a `scala.concurrent.Future[_]` + 3. the implicit conversion `vertxFutureToScalaFuture` is in scope ```scala //{ @@ -47,14 +54,16 @@ final class MyVerticle extends ScalaVerticle: } yield () ``` -As explained in [The Scala 3 Reference](https://docs.scala-lang.org/scala3/reference/contextual/conversions.html), implicit conversions can not always be applied by the compiler. That's when we might want to use an explicit conversion. - +As explained in [The Scala 3 Reference](https://docs.scala-lang.org/scala3/reference/contextual/conversions.html), implicit conversions can not always be applied +by the compiler. That's when we might want to use an explicit conversion. -# Explicit Conversion +## Explicit Conversion -When implicit conversions are not applicable and we still want to use Scala Futures, we can use extension methods. +When implicit conversions are not applicable and we still want to use Scala Futures, we can +use extension methods. -The following example shows usage of the extension method `asScala` which is defined on `io.vertx.core.Future` in the package object [[io.vertx.lang.scala]]: +The following example shows usage of the extension method `asScala` which is defined on +`io.vertx.core.Future` in the package object [[io.vertx.lang.scala]]: ```scala //{ @@ -74,11 +83,15 @@ final class MyVerticle extends ScalaVerticle: ``` -# Working with the `ExecutionContext` +## Working with the `ExecutionContext` -Interaction with Vert.x APIs is normally done in the scope of a Verticle. The base class [[io.vertx.lang.scala.ScalaVerticle]] already has a `given` `ExecutionContext`, so it is provided to any API requiring it as an implicit parameter. This means, as long as we are in the scope of a `ScalaVerticle`, we do not have to deal with the `ExecutionContext`. +Interaction with Vert.x APIs is normally done in the scope of a Verticle. The base class +[[io.vertx.lang.scala.ScalaVerticle]] already has a `given` `ExecutionContext`, so it is +provided to any API requiring it as an implicit parameter. This means, as long as we are +in the scope of a `ScalaVerticle`, we do not have to deal with the `ExecutionContext`. -For rare situations in which it is not possible to access `ScalaVerticle`'s `ExecutionContext`, we can construct one by our own: +For rare situations in which it is not possible to access `ScalaVerticle`'s `ExecutionContext`, +we can construct one by our own: ```scala //{ @@ -91,4 +104,7 @@ import io.vertx.lang.scala.VertxExecutionContext given ec: ExecutionContext = VertxExecutionContext(vertx, vertx.getOrCreateContext()) ``` -The `io.vertx.core.Context` is important for Vert.x in order to properly schedule asynchronous tasks, like accessing a database. Remember: All tasks started on the same Vert.x context, are also running on the same _thread_, which is good as it reduces expensive thread context switching and error-prone sharing of state. \ No newline at end of file +The `io.vertx.core.Context` is important for Vert.x in order to properly schedule +asynchronous tasks, like accessing a database. Remember: All tasks started on the +same Vert.x context, are also running on the same _thread_, which is good as it +reduces expensive thread context switching and error-prone sharing of state. diff --git a/vertx-lang-scala/src/main/markdown/_docs/coding/working-with-json.md b/vertx-lang-scala/src/main/markdown/_docs/coding/working-with-json.md index 0200013ff..6b34ae41d 100644 --- a/vertx-lang-scala/src/main/markdown/_docs/coding/working-with-json.md +++ b/vertx-lang-scala/src/main/markdown/_docs/coding/working-with-json.md @@ -2,59 +2,215 @@ title: Working with JSON --- -# Extensions +# Working with JSON -The package [[io.vertx.lang.scala.json]] contains extensions for `io.vertx.core.json.JsonObject`, providing +JSON is widely used throughout HTTP APIs nowadays and it is also one of the most commonly used data formats +for the Vert.x EventBus. That makes good JSON support essential. This page describes how to construct, read +and alter JSON objects and arrays with the Vert.x for Scala API. - - [[io.vertx.lang.scala.json.package.JsObject.asMap]] - to easily convert from a `JsonObject` to a Scala `Map` +## Builders & Interpolators -# Utilities +Creating `JsonObject` and `JsonArray` instances is easily done using either the builder API in +[[io.vertx.lang.scala.json.Json]] or the custom +[String Interpolator](https://docs.scala-lang.org/scala3/book/string-interpolation.html) in the +package object [[io.vertx.lang.scala.json]]. -The `Json` utility class helps building JSON objects more Scala-like. The following example shows a simple `JsonObject` being created. +### JsonObject + +`JsonObjects` can be built via a builder or via an interpolator: ```scala //{ + import io.vertx.lang.scala.json.Json +import io.vertx.lang.scala.json.json //} -Json.obj( - "foo" -> "foo text", - "bar" -> 3.45d, - "baz" -> false, - "myInt" -> Integer.MAX_VALUE) - -// or... -Json.obj(""" - |{ - | "foo": "foo text", - | "bar": 3.45, - | "baz": false, - | "myInt": 2147483647 - |} +// Json.obj takes... +// ...a vararg of Scala Tuples +val fromVararg = Json.obj( + "foo" -> "foo text", + "bar" -> 3.45d, + "baz" -> false, + "myInt" -> Integer.MAX_VALUE) + +// ...or a Scala Map +val fromMap = Json.obj(Map("foo" -> "foo text", "bar" -> 3.45d)) + +// ...or a String +val fromString = Json.obj( + """ + |{ + | "foo": "foo text", + | "bar": 3.45, + | "baz": false, + | "myInt": 2147483647 + |} """.stripMargin) + +// Additionally, the json interpolator can be used: +val bar = 3.45 +val myInt = 2147483647 +val fromInterpolator = + json""" + { + "foo": "foo text", + "bar": $bar, + "baz": false, + "myInt": $myInt + }""" ``` -Deep nesting can be achieved by subsequent usage of `Json.obj()`: +### JsonArray + +JSON arrays are built similarly: + +```scala +//{ +import collection.immutable.SortedSet +import io.vertx.lang.scala.json.Json.arr +import io.vertx.lang.scala.json.jsonArray +//} +// Json.arr takes... +// a vararg of values +val fromVararg = arr("4", "8", "15", "16", "23", "42") + +// ...or a Seq +val fromSeq = arr(List("108", "216")) + +// ...or a Set +val fromSet = arr(SortedSet(4, 8, 15, 16, 23, 42)) + +// ...or a String +val fromString = arr("[4, 8, 15, 16, 23, 42]") + +// Additionally, the jsonArray interpolator can be used: +val fortytwo = 42 +val fromInterpolator = jsonArray"[4, 8, 15, 16, 23, $fortytwo]" +``` + +### Nesting + +Any `Iterable` can be nested to create a `JsonObject` containing a `JsonArray`. Also any `Map` can be nested +in order to create a `JsonArray` containing `JsonObjects`: + +```scala +//{ +import io.vertx.lang.scala.json.Json.* +//} +// Nesting a JsonArray in a JsonObject +val nestedArray = obj("k1" -> 42, "k2" -> List(3, 2, 1)) +// res1: JsonObject = {"k1":42,"k2":[3,2,1]} + +// ...and vice versa +val nestedObject = arr(1, 2, 3, Map("k1" -> 42, "k2" -> "bar")) +// res2: JsonArray = [1,2,3,{"k1":42,"k2":"bar"}] +``` + +Deep nesting can be achieved by subsequent usage of `Json.obj()` or `Json.arr()`: ```scala //{ import io.vertx.core.json.JsonObject import io.vertx.lang.scala.json.Json +import io.vertx.lang.scala.json.Json.{arr, obj} //} Json.obj( - "webappconf" -> Json.obj( + "webappconf" -> obj( "port" -> 8080, "ssl" -> false, "bridge" -> true, - "some_nested" -> Vector(1, 2, Json.obj("next" -> List(3, 4))), - "some_list" -> Json.arr(1, 2, Vector(3, 4)), + "some_nested" -> Vector(1, 2, obj("next" -> List(3, 4))), + "some_list" -> arr(1, 2, Vector(3, 4)), "inbound_permitted" -> List( - Json.obj( + obj( "address" -> "acme.bar", - "match" -> Json.obj( + "match" -> obj( "action" -> "foo")), - Json.obj( + obj( "address" -> "acme.baz", - "match" -> Json.obj( + "match" -> obj( "action" -> "index"))), "outbound_permitted" -> Array(new JsonObject()))) ``` + +## From JsonObject and JsonArray to Map and List + +Converting from existing `JsonObject` or `JsonArray` instances to Scala `Map` or `List` is supported by +extension methods defined in the package [[io.vertx.lang.scala.json]]: + + - [[io.vertx.lang.scala.json.package.asMap]] - to recursively convert from a `JsonObject` to a Scala `Map` + - [[io.vertx.lang.scala.json.package.asList]] - to recursively convert from a `JsonArray` to a Scala `List` + + +## Json Pointers + +Vert.x for Scala provides the [[io.vertx.lang.scala.json.JsonPointer]], which is a Scala wrapper for +`io.vertx.core.json.pointer.JsonPointer` from core Vert.x. The wrapper's benefits are typesafe as well as +immutable operations. + +To build a `JsonPointer`, use its `apply` method: + +```scala +//{ +import io.vertx.lang.scala.json.JsonPointer +//} +val pointer1 = JsonPointer("/hello/world") +val pointer2 = JsonPointer() // root pointer + .appended("hello") + .appended("world") +``` + +### Querying + +After instantiation, the `query` method can be used to query a value from a `JsonObject` or +`JsonArray`. + +Note: The type argument must be supplied when calling this method to indicate the desired return type: + +```scala +//{ +import io.vertx.lang.scala.json.* +//} +val json = + json"""{ + "hello": { + "world": "¡Hola, mundo!" + }, + "numbers": [4, 8, 15, 16, 23, 42] + }""" +val worldPtr = JsonPointer("/hello/world") +val numbrPtr = JsonPointer("/numbers/5") +val spanish = worldPtr.query[String](json).getOrElse("¡Perdón!") // "¡Hola, mundo!" +val fortytwo = numbrPtr.query[Double](json).getOrElse(0) // 42 +// the following query won't work because the expected return type is wrong +val wontwork = numbrPtr.query[String](json) // None +``` + +### Writing + +The `write` method can be used to add or update values in a `JsonObject` or `JsonArray`. + +Note: For `JsonArrays` values can only be _added_. It is currently impossible to _change_ a value. If +required, create a new array. + +Note: The `write` method will return a new `JsonObject` or `JsonArray`. The original one will be +left untouched. + +```scala +//{ +import io.vertx.lang.scala.json.* +//} +val json = + json"""{ + "hello": { + "world": "¡Hola, mundo!" + }, + "numbers": [4, 8, 15, 16, 23, 42] + }""" +val moonPtr = JsonPointer("/hello/moon") +val firstElement = JsonPointer("/numbers/0") +val arrayPtr = JsonPointer("/numbers") +moonPtr.write(json, "¡Hola, Luna!") // adds "moon": "¡Hola, Luna!" +firstElement.write(json, 0) // adds 0, resulting in [0, 4, 8, 15, 16, 23, 42] +arrayPtr.write(json, Json.arr(0, 8, 15, 16, 23, 42)) // replaces 4 with 0, resulting [0, 8, 15, 16, 23, 42] +``` diff --git a/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/Json.scala b/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/Json.scala index eba814fd1..398f7f245 100644 --- a/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/Json.scala +++ b/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/Json.scala @@ -15,16 +15,13 @@ */ package io.vertx.lang.scala.json -import io.vertx.lang.scala.json.{ JsonArray, JsonObject } +import io.vertx.lang.scala.json.{JsonArray, JsonObject} import java.util import scala.jdk.CollectionConverters.ListHasAsScala /** - * Helper to construct JsonObjects and JsonArrays. - * - * @author Edgar Chan - * @author Joern Bernhardt + * Builder for constructing [[JsonObject JsonObjects]] and [[JsonArray JsonArrays]]. */ object Json: @@ -38,6 +35,7 @@ object Json: /** * Creates a JsonObject from an encoded JSON string. + * * @param json The JSON string. * @return The decoded JsonObject. */ @@ -45,45 +43,33 @@ object Json: /** * Creates an empty JsonArray. - * - * @return An empty JsonArray. */ def arr(): JsonArray = new JsonArray() /** * Creates an empty JsonObject. - * - * @return An empty JsonObject. */ def obj(): JsonObject = new JsonObject() /** - * Constructs a JsonObject from a fieldName -> value pairs. - * - * @param map The scala map that will be converted to a JsonObject - * @return + * Constructs a JsonObject from a [[Map Scala Map]]. */ - def obj(map: Map[String, Any]): JsonObject = obj(map.toSeq: _*) + def obj(map: collection.Map[String, Any]): JsonObject = obj(map.toSeq *) /** - * Constructs a JsonObject from a fieldName -> value pairs. - * - * @param map The scala map that will be converted to a JsonObject - * @return + * Constructs a JsonObject from a [[java.util.Map Java Map]]. */ def obj(map: util.Map[String, Object]): JsonObject = new JsonObject(map) + /** - * Constructs a JsonObject from a fieldName -> value pairs. - * - * @param fields The fieldName -> value pairs - * @return A JsonObject containing the name -> value pairs. + * Constructs a JsonObject from Scala Tuples. */ def obj(fields: (String, Any)*): JsonObject = { val o = new JsonObject() fields.foreach { case (key, l: Array[_]) => o.put(key, listToJsArr(l.toIndexedSeq)) - case (key, l: Seq[_]) => o.put(key, listToJsArr(l)) - case (key, value) => o.put(key, value) + case (key, l: Seq[_]) => o.put(key, listToJsArr(l)) + case (key, value) => o.put(key, value) } o } @@ -95,22 +81,28 @@ object Json: * @return A JsonArray containing the provided elements. */ def arr(fields: Any*): JsonArray = { - val a = new JsonArray() - fields.foreach { - case array: Array[_] => a.add(listToJsArr(array.toIndexedSeq)) - case seq: Seq[_] => a.add(listToJsArr(seq)) - case f => a.add(f) - } - a + if fields.length == 1 then fields(0) match + case array: Array[_] => listToJsArr(array.toIndexedSeq) + case iterable: Iterable[_] => listToJsArr(iterable) + case value => arr().add(value) + else + val a = new JsonArray() + fields.foreach { + case array: Array[_] => a.add(arr(array)) + case map: collection.Map[String, _] => a.add(obj(map)) + case iterable: Iterable[_] => a.add(arr(iterable)) + case f => a.add(f) + } + a } - def arr(list: List[Any]): JsonArray = { - listToJsArr(list) - } + private def listToJsArr(a: Iterable[_]) = Json.arr(a.toSeq *) extension (internal: JsonArray) + + /** + * @deprecated see extension for JsonArray in [[io.vertx.lang.scala.package]] + */ def list: List[Any] = { internal.getList.asScala.toList } - - private def listToJsArr(a: Seq[_]) = Json.arr(a: _*) diff --git a/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/JsonPointer.scala b/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/JsonPointer.scala new file mode 100644 index 000000000..de16f3c1a --- /dev/null +++ b/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/JsonPointer.scala @@ -0,0 +1,188 @@ +package io.vertx.lang.scala.json + +import java.net.URI +import scala.annotation.tailrec +import scala.reflect.Typeable + +private type JJsonPointer = io.vertx.core.json.pointer.JsonPointer + +/** + * Implementation of [[https://tools.ietf.org/html/rfc6901 RFC6901 Json Pointers]]. This + * wraps [[io.vertx.core.json.pointer.JsonPointer]] and makes it behave more Scala-like. + * @param internal the wrapped Vert.x [[io.vertx.core.json.pointer.JsonPointer]] + */ +final case class JsonPointer(private val internal: JJsonPointer): + + /** + * @return `true` if the pointer is a root pointer + */ + def isRootPointer: Boolean = internal.isRootPointer + + /** + * @return `true` if the pointer is local (URI with only fragment) + */ + def isLocalPointer: Boolean = internal.isLocalPointer + + /** + * @return `true` if this pointer is a parent of the given `child`. For instance, + * `"/properties"` pointer is parent pointer of `"/properties/parent"` + */ + def isParent(child: JsonPointer): Boolean = internal.isParent(child.internal) + + override def toString: String = internal.toString + + /** + * Build a URI representation of the JSON Pointer + */ + def toURI: URI = internal.toURI + + /** + * Return the underlying URI without the fragment + */ + def getURIWithoutFragment: URI = internal.getURIWithoutFragment + + /** + * This pointer appended with String tokens. + * + * @param tokens the tokens to append which must not contain escaped characters + */ + def appended(tokens: String*): JsonPointer = + @tailrec def append(ptr: JsonPointer, tokens: String*): JsonPointer = tokens.headOption match + case Some(token) => append(JsonPointer(ptr.internal.append(token)), tokens.tail *) + case None => ptr + + append(JsonPointer(internal.copy()), tokens *) + + /** + * This JsonPointer with the given index appended as reference token. + * + * Note: This function does not mutate this JsonPointer. + */ + def appended(index: Int): JsonPointer = JsonPointer(internal.copy.append(index)) + + /** + * This JsonPointer with the given other JsonPointer appended. + * + * Note: This function does not mutate this JsonPointer. + */ + def appended(other: JsonPointer): JsonPointer = JsonPointer(internal.copy.append(other.internal)) + + /** + * Evaluates to the parent pointer. + */ + def parent: JsonPointer = JsonPointer(this.internal.copy.parent) + + /** + * Query the given `jsonObject`, expecting the result to be of type [[T]]. + * + *

Note: If this pointer is a root pointer ("") and [[T]] is [[JsonObject]], this function returns + * the provided JSON.

+ * + *

Note: JSON numbers will always result in the [[Double]] Scala type. The following JSON to Scala mappings apply: + *

+ *

+ * + * @param jsonObject the JSON to query + * @tparam T the expected result type of the query + * @return `Some[T]` if the requested value exists and has the expected type [[T]], + * otherwise `None` + */ + def query[T: Typeable](jsonObject: JsonObject): Option[T] = internal.queryJson(jsonObject) match + case x: T => Some(x) + case _ => None + + /** + * Write `newElement` to the given `json` using this pointer. The path token "-" is + * handled as append to the end of an array. + * + * Note: This function does not mutate the given `json`. + * + * Note: If this is a root pointer, `newElement` will be returned. + * + * @param json the JSON to write to + * @param newElement the new element to write + * @param createIfMissing creates a new [[JsonObject]] when the parent object key is missing + * @tparam J either a [[JsonObject]] or a [[JsonArray]] + * @return `Some[J]` if the write was successful, `None` if not + */ + def write[J <: JsonObject | JsonArray : Typeable](json: J, newElement: Any, createIfMissing: Boolean = false): Option[J] = + internal.writeJson(json.copy(), newElement, createIfMissing).asInstanceOf[J] match + case o: JsonObject => Some(o) + case a: JsonArray => Some(a) + case _ => None + +end JsonPointer + +object JsonPointer: + /** + * @return an empty `JsonPointer` + */ + def apply(): JsonPointer = JsonPointer(io.vertx.core.json.pointer.JsonPointer.create()) + + /** + * Builds a [[JsonPointer]] from a String. + * + * @param pointer the String representing a valid JSON pointer + * @return the [[JsonPointer]] + * @throws IllegalArgumentException if the given String is not a valid JSON pointer + */ + def apply(pointer: String): JsonPointer = JsonPointer(io.vertx.core.json.pointer.JsonPointer.from(pointer)) + + /** + * Builds a [[JsonPointer]] from a [[java.net.URI]]. + * + * @param pointer - the URI representing a valid JSON pointer + * @return the [[JsonPointer]] + * @throws IllegalArgumentException if the given URI is not a valid JSON pointer + */ + def apply(pointer: URI): JsonPointer = JsonPointer(io.vertx.core.json.pointer.JsonPointer.fromURI(pointer)) + + /** + * Same as [[apply apply()]], for compatibility with Vert.x core. + */ + def create(): JsonPointer = apply() + + /** + * Same as [[apply apply(pointer)]], for compatibility with Vert.x core. + * + * @see [[apply]] + */ + def from(pointer: String): JsonPointer = apply(pointer) + + /** + * Builds a [[JsonPointer]] from a String, returning an [[Option]]. This comes + * in handy in situations when it is unknown if the String contains a valid + * JSON pointer, e.g. if the String is a variable. + * + * @param pointer the String representing a pointer + * @return [[Some Some(JsonPointer)]] if the given String was valid, else [[None]] + */ + def fromOption(pointer: String): Option[JsonPointer] = + try + Some(JsonPointer(io.vertx.core.json.pointer.JsonPointer.from(pointer))) + catch + case _: IllegalArgumentException => None + + /** + * Builds a [[JsonPointer]] from a URI, returning an [[Option]]. This comes + * in handy in situations when it is unknown if the URI contains a valid + * JSON pointer, e.g. if the URI is a variable. + * + * @param uri the uri representing the JSON pointer + * @return [[Some Some(JsonPointer)]] if the given URI was valid, else [[None]] + */ + def fromURIOption(uri: URI): Option[JsonPointer] = + try + Some(JsonPointer(io.vertx.core.json.pointer.JsonPointer.fromURI(uri))) + catch + case _: IllegalArgumentException => None + +end JsonPointer + diff --git a/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/package.scala b/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/package.scala index 685eb69d1..f7fd6c25c 100644 --- a/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/package.scala +++ b/vertx-lang-scala/src/main/scala/io/vertx/lang/scala/json/package.scala @@ -15,33 +15,89 @@ */ package io.vertx.lang.scala -import scala.collection.mutable.Map +import scala.collection.* +import scala.jdk.CollectionConverters.* /** - * @author swilliams - * @author Edgar Chan - * + * Scala extensions for more idiomatic handling of [[io.vertx.core.json.JsonObject]] + * and [[io.vertx.core.json.JsonArray]]. */ -package object json { +package object json: + /** + * Type alias which provides extensions for idiomatic Scala, like [[asList]]. + */ type JsonArray = io.vertx.core.json.JsonArray + /** + * Type alias which provides extensions for idiomatic Scala, like [[asMap]]. + */ type JsonObject = io.vertx.core.json.JsonObject - import scala.language.implicitConversions + extension (internal: JsonObject) + + /** + * Get the underlying Map as an immutable `Map`. Unlike + * Vert.x core's [[io.vertx.core.json.JsonObject.getMap]], this method guarantees + * to convert every contained [[JsonObject]] or [[JsonArray]] into a [[Map]] or [[List]], + * respectively. That's done, however, at the cost of one pass through each contained + * [[JsonObject]] or [[JsonArray]]. + */ + def asMap: Map[String, AnyRef] = Map.from( + for + (key, value) <- internal.getMap.asScala + mappedValue = value match + case j: JsonObject => j.asMap + case a: JsonArray => a.asList + case _ => value + yield (key, mappedValue)) + end extension + + + extension (internal: JsonArray) + + /** + * Get the underlying List as an immutable [[List]]. Unlike + * Vert.x core's [[io.vertx.core.json.JsonArray.getList]], this method guarantees + * to convert every contained [[JsonObject]] or [[JsonArray]] into a [[Map]] or [[List]], + * respectively. That's done, however, at the cost of one pass through each contained + * [[JsonObject]] or [[JsonArray]]. + */ + def asList: List[Any] = List.from( + for + value <- internal.getList.asScala + mappedValue = value match + case j: JsonObject => j.asMap + case a: JsonArray => a.asList + case _ => value + yield mappedValue + ) + end extension + + + extension (sc: StringContext) - implicit def toJsonObject(js: JsObject): JsonObject = js.internal + /** + * Interpolates the given String as a [[JsonObject]]. + */ + def json(args: Any*): JsonObject = { + new JsonObject(interpolated(sc.parts, args)) + } - implicit class JsObject(val internal: JsonObject) extends AnyVal { - import scala.jdk.CollectionConverters._ /** - * Get the underlying Map as `mutable.Map`. This map may contain - * values that are not the types returned by the JsonObject and with - * an unpredictable representation of the value, e.g you might get a - * JSON object as a [[io.vertx.core.json.JsonObject]] or as a Map. - * - * @return the underlying Map - */ - def asMap: Map[String, AnyRef] = internal.getMap.asScala - } + * Interpolates the given String as a [[JsonArray]]. + */ + def jsonArray(args: Any*): JsonArray = { + new JsonArray(interpolated(sc.parts, args)) + } + + end extension + +private def interpolated(stringParts: Seq[String], args: Seq[Any]): String = { + val stringBuilder = StringBuilder() + for + (stringPart, argument) <- stringParts.zipAll(args, "", "") + do + stringBuilder.append(stringPart).append(argument.toString) + stringBuilder.toString.trim } diff --git a/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/http/HttpClientTest.scala b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/http/HttpClientTest.scala index 51ffab2a0..66c83c61e 100644 --- a/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/http/HttpClientTest.scala +++ b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/http/HttpClientTest.scala @@ -2,9 +2,12 @@ package io.vertx.lang.scala.http import io.vertx.core.{Vertx, http} import io.vertx.core.http.HttpMethod.GET -import io.vertx.lang.scala._ +import io.vertx.lang.scala.* +import io.vertx.lang.scala.ImplicitConversions.vertxFutureToScalaFuture import org.scalatest.flatspec.AsyncFlatSpec import org.scalatest.matchers.should.Matchers +import scala.language.implicitConversions + class HttpClientTest extends AsyncFlatSpec, Matchers: @@ -13,10 +16,10 @@ class HttpClientTest extends AsyncFlatSpec, Matchers: val payload = "Hello" val vertx = Vertx.vertx for { - _ <- vertx.createHttpServer.requestHandler(req => req.response.end(payload)).listen(port).asScala - req <- vertx.createHttpClient.request(GET, port, "127.0.0.1", "/").asScala - res <- req.send.asScala - body <- res.body.asScala + _ <- vertx.createHttpServer.requestHandler(_.response.end(payload)).listen(port) + req <- vertx.createHttpClient.request(GET, port, "127.0.0.1", "/") + res <- req.send + body <- res.body assertion = body.toString should equal(payload) } yield assertion } diff --git a/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/ExtensionsSpec.scala b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/ExtensionsSpec.scala new file mode 100644 index 000000000..987871764 --- /dev/null +++ b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/ExtensionsSpec.scala @@ -0,0 +1,138 @@ +package io.vertx.lang.scala.json + +import org.scalatest.Inside +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import io.vertx.core.json.{JsonArray, JsonObject} +import java.lang.Boolean.FALSE + +class ExtensionsSpec extends AnyFunSpec, Matchers, Inside: + describe("JsonObject.asMap extension") { + it("should return a Map representation of some JsonObject") { + val jsonObject = JsonObject.of("foo", "foo text", "optional", true) + jsonObject.asMap("foo") should equal("foo text") + jsonObject.asMap("optional") should equal(true) + } + + it("should convert nested JsonObjects and JsonArrays") { + val jsonObject = JsonObject.of( + "foo", "bar", + "baz", JsonObject.of( + "qux", "qax", + "foobar", "foobaz", + "fooqux", JsonObject.of( + "barfoo", "barbar", + "barbar", "barbaz", + "barqux", JsonArray.of(1, 2, 3, 4) + ))) + val map = jsonObject.asMap + map("foo") should equal("bar") + map("baz") shouldBe a[Map[String, _]] + val bazMap = map("baz").asInstanceOf[Map[String, _]] + bazMap("fooqux") shouldBe a[Map[String, _]] + val fooquxMap = bazMap("fooqux").asInstanceOf[Map[String, _]] + fooquxMap("barqux") shouldBe a[List[_]] + } + } + + + describe("JsonArray.asList extension") { + it("should return a List representation of some JsonArray") { + val jsonArray = JsonArray.of(1, 2, 3) + jsonArray.asList should contain inOrderOnly(1, 2, 3) + } + + it("convert nested JsonObjects and JsonArrays") { + val jsonArray = JsonArray.of(0, + 1, + 2, + JsonArray.of(true, false), + 4, + JsonArray.of(0, 1, JsonArray.of(0, 1, JsonArray.of(0, 1, 2))), + 6, + JsonObject.of("foo", "bar"), + ) + val list = jsonArray.asList + list(3) shouldBe a[List[_]] + list(3).asInstanceOf[List[_]].length should be(2) + list(5) shouldBe a[List[_]] + list(5).asInstanceOf[List[_]](2).asInstanceOf[List[_]](2) shouldBe a[List[_]] + list(7) shouldBe a[Map[String, _]] + } + } + + + describe("json String interpolator") { + it("should be able to construct an empty JsonObject") { + json"{}" should equal(JsonObject()) + } + + it("should be able to construct a flat JsonObject") { + val json = json"""{ "foo": "foo text", "bar": 3.45, "baz": false, "myInt": 23 }""" + + inside(json) { case j: JsonObject => + j.getString("foo") should equal("foo text") + j.getFloat("bar") should equal(3.45f) + j.getBoolean("baz") should be(FALSE) + j.getInteger("myInt") should be(23) + } + } + + it("should interpolate variables") { + val myInt = Int.MaxValue + val json = json"""{ "myInt": $myInt }""" + + inside(json) { case j: JsonObject => + j.getInteger("myInt") should equal(myInt) + } + } + + it("should be able to create nested JsonObjects") { + val json = + json"""{ + "foo": { + "bar": { + "baz": [4, 8, 15, 16, 23, 42] + } + } + }""" + + inside(json) { case j: JsonObject => + j.getJsonObject("foo") + .getJsonObject("bar") + .getJsonArray("baz") should equal(JsonArray("[4, 8, 15, 16, 23, 42]")) + } + } + } + + + describe("jsonArray String interpolator") { + it("should return an empty JsonArray") { + jsonArray"[]" should equal(JsonArray()) + } + + it("be able to construct a flat JsonArray") { + inside(jsonArray"[4, 8, 15, 16, 23, 42]") { case a: JsonArray => + a.asList should contain allElementsOf List(4, 8, 15, 16, 23, 42) + } + } + + it("should be able to construct a JsonArray of JsonObjects") { + val json = + jsonArray"""[{ + "location": "Marseille", + "weather": "fine" + }, + { + "location": "London", + "weather": "rainy" + } + ]""" + inside(json) { case a: JsonArray => + a.getJsonObject(0).getString("location") should equal("Marseille") + a.getJsonObject(1).getString("location") should equal("London") + } + } + } + + diff --git a/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonPointerSpec.scala b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonPointerSpec.scala new file mode 100644 index 000000000..56f623ef9 --- /dev/null +++ b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonPointerSpec.scala @@ -0,0 +1,209 @@ +package io.vertx.lang.scala.json + +import org.scalatest.Inside +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import io.vertx.core.json.{JsonArray, JsonObject} + +import java.net.URI + +class JsonPointerSpec extends AnyFunSpec, Matchers, Inside, ScalaCheckPropertyChecks: + + describe("JsonPointer") { + it("should be constructable from a String") { + JsonPointer("/foo/bar") shouldBe a[JsonPointer] + an[IllegalArgumentException] should be thrownBy JsonPointer("|foo|bar") + } + + it("should be constructable from a String, returning Option for safety") { + JsonPointer.fromOption("/foo/bar") should matchPattern { case Some(_) => } + JsonPointer.fromOption("|foo|bar") should matchPattern { case None => } + } + + it("should be constructable from an URI") { + JsonPointer(URI("/foo/bar")) shouldBe a[JsonPointer] + an[IllegalArgumentException] should be thrownBy JsonPointer(URI("https://vertx.io/docs/vertx-core/java/#_json_pointers")) + } + + it("should be constructable from an URI, returning Option for safety") { + JsonPointer.fromURIOption(URI("/foo/bar")) should matchPattern { case Some(_) => } + JsonPointer.fromURIOption(URI("https://vertx.io/docs/vertx-core/java/#_json_pointers")) should matchPattern { case None => } + } + + it("should be copy-able") { + JsonPointer.fromOption("/foo/bar") match + case Some(orig) => + val copy = orig.copy() + copy should not be theSameInstanceAs(orig) + copy should equal(orig) + case None => fail("should not be None") + } + + it("should return the stringified pointer") { + JsonPointer.fromOption("/foo/bar").map(_.toString) match + case Some(str) => str should be("/foo/bar") + case None => fail("should not be None") + } + + it("should determine if it is a root pointer") { + JsonPointer().isRootPointer should be(true) + JsonPointer("/foo/bar").isRootPointer should be(false) + } + + it("should determine if it is a local pointer") { + JsonPointer("/foo/bar").isLocalPointer should be(true) + JsonPointer(URI("/foo/bar")).isLocalPointer should be(false) + } + + it("should determine if it is the parent of another pointer") { + JsonPointer("/foo").isParent(JsonPointer("/foo/bar")) should be(true) + JsonPointer("/foo").isParent(JsonPointer("/foo")) should be(false) + } + + it("should calculate the parent pointer") { + val pointer = JsonPointer("/foo/bar/baz/qux") + pointer.parent.toString should be("/foo/bar/baz") + pointer.toString should be("/foo/bar/baz/qux") + } + } + + describe("Appending to a JsonPointer") { + val pointer = JsonPointer("/foo/bar") + + it("should append an unescaped String") { + // See RFC 6901, No. 5: + // Note that before processing a JSON string as a JSON Pointer, + // backslash escape sequences must be unescaped + val appended = pointer.appended("baz") + appended.toString should be("/foo/bar/baz") + pointer.toString should be("/foo/bar") + } + + it("should append a sequence of String tokens") { + val appended = pointer.appended("baz", "qux", "qax") + appended.toString should be("/foo/bar/baz/qux/qax") + pointer.toString should be("/foo/bar") + } + + it("should append an index") { + val appended = pointer.appended(23) + appended.toString should be("/foo/bar/23") + pointer.toString should be("/foo/bar") + } + + it("should append another JsonPointer") { + val appended = pointer.appended(JsonPointer("/baz/qux")) + appended.toString should be("/foo/bar/baz/qux") + pointer.toString should be("/foo/bar") + } + } + + describe("Querying") { + val json = JsonObject( + """{ + "int": 123, + "str": "Foo", + "bol": true, + "num": 4.2, + "obj": { "foo": "bar" }, + "arr": [1, 2, 3], + "nul": null + }""") + val queriesAndResults = Table( + ("name", "result", "expectedResult"), + ("query for an Int", JsonPointer("/int").query[Int](json), 123), + ("query for a String", JsonPointer("/str").query[String](json), "Foo"), + ("query for a Boolean", JsonPointer("/bol").query[Boolean](json), true), + ("query for a Double", JsonPointer("/num").query[Double](json), 4.2d), + ("query for a JsonObject", JsonPointer("/obj").query[JsonObject](json), JsonObject("""{"foo":"bar"}""")), + ("query for a JsonArray", JsonPointer("/arr").query[JsonArray](json), JsonArray("""[1,2,3]""")), + ) + forAll(queriesAndResults) { (name, result, expectedResult) => + it(s"$name should result in $expectedResult") { + result should contain(expectedResult) + } + } + + val wrongQueries = Table( + ("name", "result"), + ("query for /int with type String", JsonPointer("/int").query[String](json)), + ("query for /str with type Int", JsonPointer("/str").query[Int](json)), + ("query for /nul", JsonPointer("/nul").query[Any](json)), + ) + forAll(wrongQueries) { (name, result) => + it(s"$name should result in None") { + result should be(None) + } + } + + it("query for root should result in the provided JSON") { + JsonPointer().query[JsonObject](json) should contain(json) + } + } + + describe("Writing") { + val json = JsonObject( + """ + |{ + | "foo": "bar", + | "arr": [1, 2, 3] + |}""".stripMargin) + + it("should create a new JsonObject") { + val expectedJson = JsonObject("""{ "foo": "bar" }""") + JsonPointer().write(JsonObject(), expectedJson) should contain(expectedJson) + } + + it("should create a new JsonArray") { + val expectedJson = JsonArray("[1, 2, 3]") + JsonPointer().write(JsonArray(), expectedJson) should contain(expectedJson) + } + + it("should add a value") { + val expectedJson = JsonObject( + """ + |{ + | "foo": "bar", + | "arr": [1, 2, 3], + | "baz": "qux" + |}""".stripMargin) + JsonPointer("/baz").write(json, "qux") should contain(expectedJson) + } + + it("should replace a value") { + val expectedJson = JsonObject( + """ + |{ + | "foo": "baz", + | "arr": [1, 2, 3] + |} + |""".stripMargin) + JsonPointer("/foo").write(json, "baz") should contain(expectedJson) + } + + it("should append to the end of an array") { + val jsonArray = JsonArray("[1, 2, 3]") + val expectedJson = JsonArray("[1, 2, 3, 4]") + val ptr = JsonPointer("/-") + ptr.write(jsonArray, 4) should contain(expectedJson) + } + + it("should create a JsonObject if missing") { + val ptr = JsonPointer("/obj/baz") + val expectedJson = JsonObject( + """ + |{ + | "foo": "bar", + | "arr": [1, 2, 3], + | "obj": { + | "baz": "qux" + | } + |}""".stripMargin) + ptr.write(json, "qux", true) should contain(expectedJson) + } + + it("should return null if the pointer is wrong") { + JsonPointer("/foo/bar").write(json, "baz") should be(None) + } + } diff --git a/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonSpec.scala b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonSpec.scala new file mode 100644 index 000000000..65e4bd244 --- /dev/null +++ b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonSpec.scala @@ -0,0 +1,201 @@ +/* + * Copyright 2013 the original author or authors. + * + * 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 io.vertx.lang.scala.json + +import io.vertx.core.json.{JsonArray, JsonObject} +import org.scalatest.Inside +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import org.scalatest.prop.TableFor3 +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks.{Table, forAll} + +import java.lang.Boolean.FALSE +import scala.collection.immutable.SortedSet +import scala.collection.mutable + +class JsonSpec extends AnyFlatSpec, Matchers, Inside: + + "Json.obj()" should "construct an empty JsonObject" in { + val jsonObject = Json.obj() + jsonObject.isEmpty should be(true) + jsonObject.encode should equal("{}") + } + + it should "construct a flat JsonObject" in { + val enc = """{"foo":"foo text","bar":3.45,"baz":false,"myInt":2147483647}""" + + val obj: JsonObject = + Json.obj( + "foo" -> "foo text", + "bar" -> 3.45d, + "baz" -> false, + "myInt" -> Integer.MAX_VALUE) + + obj.getString("foo") should equal("foo text") + obj.getDouble("bar") should equal(3.45d) + obj.getBoolean("baz") should equal(false) + obj.getInteger("myInt") should equal(Integer.MAX_VALUE) + obj.encode should equal(enc) + } + + + it should "support at least 3 levels of nesting" in { + val obj = Json.obj( + "webappconf" -> Json.obj( + "port" -> 8080, + "ssl" -> false, + "bridge" -> true, + "some_nested" -> Json.arr(1, 2, Json.obj("next" -> Json.arr(3, 4))), + "some_list" -> Json.arr(1, 2, Json.arr(3, 4)), + "inbound_permitted" -> Json.arr( + Json.obj( + "address" -> "acme.bar", + "match" -> Json.obj("action" -> "foo")), + Json.obj( + "address" -> "acme.baz", + "match" -> Json.obj("action" -> "index")) + ), + "outbound_permitted" -> Json.arr(new JsonObject()))) + obj.encode() should equal(jsonString) + } + + + it should "convert nested List to JsonArray" in { + val obj = Json.obj( + "webappconf" -> Json.obj( + "port" -> 8080, + "ssl" -> false, + "bridge" -> true, + "some_nested" -> List(1, 2, Json.obj("next" -> List(3, 4))), + "some_list" -> List(1, 2, List(3, 4)), + "inbound_permitted" -> List( + Json.obj( + "address" -> "acme.bar", + "match" -> Json.obj("action" -> "foo")), + Json.obj( + "address" -> "acme.baz", + "match" -> Json.obj("action" -> "index")) + ), + "outbound_permitted" -> List(new JsonObject()))) + obj.getJsonObject("webappconf").getJsonArray("some_list").size should be(3) + obj.encode() should equal(jsonString) + } + + it should "convert nested Array to JsonArray" in { + val obj = Json.obj( + "webappconf" -> Json.obj( + "port" -> 8080, + "ssl" -> false, + "bridge" -> true, + "some_nested" -> Array(1, 2, Json.obj("next" -> Array(3, 4))), + "some_list" -> Array(1, 2, Array(3, 4)), + "inbound_permitted" -> Array( + Json.obj( + "address" -> "acme.bar", + "match" -> Json.obj("action" -> "foo")), + Json.obj( + "address" -> "acme.baz", + "match" -> Json.obj("action" -> "index")) + ), + "outbound_permitted" -> Array(new JsonObject()))) + obj.getJsonObject("webappconf").getJsonArray("some_list").size should be(3) + obj.encode() should equal(jsonString) + } + + it should "convert mixed nested Arrays/Lists to JsonArray" in { + val obj = Json.obj( + "webappconf" -> Json.obj( + "port" -> 8080, + "ssl" -> false, + "bridge" -> true, + "some_nested" -> Vector(1, 2, Json.obj("next" -> List(3, 4))), + "some_list" -> Json.arr(1, 2, Vector(3, 4)), + "inbound_permitted" -> List( + Json.obj( + "address" -> "acme.bar", + "match" -> Json.obj("action" -> "foo")), + Json.obj( + "address" -> "acme.baz", + "match" -> Json.obj("action" -> "index"))), + "outbound_permitted" -> Array(new JsonObject()))) + val someList = obj.getJsonObject("webappconf").getJsonArray("some_list") + someList.size should be(3) + someList.getJsonArray(2).size should be(2) + obj.encode() should equal(jsonString) + } + + it should "construct a JsonObject from a mutable Map" in { + val jsonObject = Json.obj(mutable.Map("answer" -> 42)) + jsonObject.encode should equal("""{"answer":42}""") + } + + "Json.arr()" should "construct an empty JsonArray" in { + val jsonArray = Json.arr() + jsonArray.isEmpty should be(true) + jsonArray.encode() should equal("[]") + } + + val iterables: TableFor3[String, JsonArray, String] = Table( + ("name for test", "iterable generator", "expected encoded JSON"), + ("discrete values", Json.arr("f", 3, "b", 7, 35.4f, true), """["f",3,"b",7,35.4,true]"""), + ("a Seq", Json.arr(Seq(1, 2, 3, 4, 5, 6, 7)), "[1,2,3,4,5,6,7]"), + ("a Set", Json.arr(SortedSet(1, 2, 3, 4, 5, 6, 7, 8, 9)), "[1,2,3,4,5,6,7,8,9]"), + ("a mutable Seq", Json.arr(mutable.Seq(1, 2, 3, 4, 5)), "[1,2,3,4,5]"), + ("a mutable Set", Json.arr(mutable.SortedSet(1, 2, 3)), "[1,2,3]"), + ("a Seq containing Seqs", Json.arr(Seq(1, 2, Seq(3, 4), Seq(5, 6))), "[1,2,[3,4],[5,6]]"), + ("a mutable Seq containing a mutable Seq", Json.arr(mutable.Seq(1, mutable.Seq(2, 3))), "[1,[2,3]]"), + ("a mutable Set containing a mutable Set", Json.arr(mutable.Set(1, mutable.Set(2, 3))), "[1,[2,3]]"), + ("values containing a Map", Json.arr(1, 2, 3, Map("k" -> 4)), """[1,2,3,{"k":4}]""") + ) + forAll(iterables) { (name, jsonArray, expectedEncoded) => + it should s"construct a JsonArray from $name" in { + jsonArray.encode should equal(expectedEncoded) + } + } + + private def jsonString = { + """ + |{ + | "webappconf" : { + | "port": 8080, + | "ssl": false, + | "bridge": true, + | "some_nested": [1, 2, { "next": [3, 4] }], + | "some_list": [1, 2, [3, 4]], + | "inbound_permitted": [ + | { + | "address" : "acme.bar", + | "match" : { + | "action" : "foo" + | } + | }, + | + | { + | "address" : "acme.baz", + | "match" : { + | "action" : "index" + | } + | } + | ], + | + | "outbound_permitted": [ + | {} + | ] + | } + |} + """.stripMargin.replaceAll("\\s", "") + } + diff --git a/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonTest.scala b/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonTest.scala deleted file mode 100644 index d02655c43..000000000 --- a/vertx-lang-scala/src/test/scala/io/vertx/lang/scala/json/JsonTest.scala +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2013 the original author or authors. - * - * 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 io.vertx.lang.scala.json - -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Assertions._ - -/** - * @author Edgar Chan - * @author Joern Bernhardt - */ -class JsonTest { - - @Test - def jsonObjectTest(): Unit = { - - val enc = """{"foo":"foo text","bar":3.45,"baz":false,"myInt":2147483647}""" - - val obj: JsonObject = - Json.obj( - "foo" -> "foo text", - "bar" -> 3.45d, - "baz" -> false, - "myInt" -> Integer.MAX_VALUE) - - assertEquals("foo text", obj.getString("foo")) - assertEquals(3.45d, obj.getDouble("bar"), 1e-15) - assertEquals(false, obj.getBoolean("baz")) - assertEquals(Integer.MAX_VALUE, obj.getInteger("myInt")) - assertEquals(enc, obj.encode()) - - } - - @Test - def wrapperConversionTest(): Unit = { - - val obj = Json.obj("foo" -> "foo text", "optional" -> true) - - val wrapped: JsObject = obj - val scalaMapFields: scala.collection.Map[String, AnyRef] = wrapped.asMap - - //Get the original object - val obj2: JsonObject = wrapped - - assertEquals("foo text", scalaMapFields("foo")) - assertEquals(true, obj2.getBoolean("optional")) - - } - - @Test - def jsonArrayTest(): Unit = { - - val enc = """["f",3,"b",7,35.4,true]""" - val array = Json.arr("f", 3, "b", 7, 35.4f, true) - - assertEquals(6, array.size()) - assertEquals(enc, array.encode()) - } -/* - @Test @Ignore("https://groups.google.com/forum/?fromgroups=#!topic/vertx/TJ2B3D_1zrA and https://groups.google.com/forum/?fromgroups=#!topic/vertx/lOmoB96w8hc") - def customObjTest() { - import java.util.Date - - case class Custom(date: Date, other: Boolean) - val info = Custom(new Date(), false) - val obj1 = Json.obj("custom" -> info) - - assertEquals(info, obj1.getValue[Custom]("custom")) - } -*/ - @Test - def nestedObjectsTest(): Unit = { - val obj = - Json.obj( - "webappconf" -> Json.obj( - "port" -> 8080, - "ssl" -> false, - "bridge" -> true, - "some_nested" -> Json.arr(1, 2, Json.obj("next" -> Json.arr(3, 4))), - "some_list" -> Json.arr(1, 2, Json.arr(3, 4)), - "inbound_permitted" -> Json.arr( - Json.obj( - "address" -> "acme.bar", - "match" -> Json.obj( - "action" -> "foo")), - Json.obj( - "address" -> "acme.baz", - "match" -> Json.obj( - "action" -> "index"))), - "outbound_permitted" -> Json.arr( - new JsonObject()))) - - assertEquals(jsonString, obj.encode()) - } - - @Test - def nestedObjectsWithListsTest(): Unit = { - val obj = - Json.obj( - "webappconf" -> Json.obj( - "port" -> 8080, - "ssl" -> false, - "bridge" -> true, - "some_nested" -> List(1, 2, Json.obj("next" -> List(3, 4))), - "some_list" -> List(1, 2, List(3, 4)), - "inbound_permitted" -> List( - Json.obj( - "address" -> "acme.bar", - "match" -> Json.obj( - "action" -> "foo")), - Json.obj( - "address" -> "acme.baz", - "match" -> Json.obj( - "action" -> "index"))), - "outbound_permitted" -> List(new JsonObject()))) - - assertEquals(jsonString, obj.encode()) - } - - @Test - def nestedObjectsWithArraysTest(): Unit = { - val obj = - Json.obj( - "webappconf" -> Json.obj( - "port" -> 8080, - "ssl" -> false, - "bridge" -> true, - "some_nested" -> Array(1, 2, Json.obj("next" -> Array(3, 4))), - "some_list" -> Array(1, 2, Array(3, 4)), - "inbound_permitted" -> Array( - Json.obj( - "address" -> "acme.bar", - "match" -> Json.obj( - "action" -> "foo")), - Json.obj( - "address" -> "acme.baz", - "match" -> Json.obj( - "action" -> "index"))), - "outbound_permitted" -> Array(new JsonObject()))) - - assertEquals(jsonString, obj.encode()) - } - - @Test - def mixedNestedObjectsTest(): Unit = { - val obj = - Json.obj( - "webappconf" -> Json.obj( - "port" -> 8080, - "ssl" -> false, - "bridge" -> true, - "some_nested" -> Vector(1, 2, Json.obj("next" -> List(3, 4))), - "some_list" -> Json.arr(1, 2, Vector(3, 4)), - "inbound_permitted" -> List( - Json.obj( - "address" -> "acme.bar", - "match" -> Json.obj( - "action" -> "foo")), - Json.obj( - "address" -> "acme.baz", - "match" -> Json.obj( - "action" -> "index"))), - "outbound_permitted" -> Array(new JsonObject()))) - - assertEquals(jsonString, obj.encode()) - } - - private def jsonString = { - """ - |{ - | "webappconf" : { - | "port": 8080, - | "ssl": false, - | "bridge": true, - | "some_nested": [1, 2, { "next": [3, 4] }], - | "some_list": [1, 2, [3, 4]], - | "inbound_permitted": [ - | { - | "address" : "acme.bar", - | "match" : { - | "action" : "foo" - | } - | }, - | - | { - | "address" : "acme.baz", - | "match" : { - | "action" : "index" - | } - | } - | ], - | - | "outbound_permitted": [ - | {} - | ] - | } - |} - """.stripMargin.replaceAll("\\s", "") - } - -}