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:
+ *
+ * - `true/false` - `Boolean`
+ * - `number` - `Double`
+ * - `string` - `String`
+ * - `array` - [[JsonArray]]
+ * - `object` - [[JsonObject]]
+ * - `null` - will always result in `None`
+ *
+ *
+ *
+ * @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", "")
- }
-
-}