Skip to content

From js any #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 27 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ Implementation is in `scala.json.ast.JValue`
- `scala.json.ast.JObject` is an actual `Map[String,JValue]`. This means that it doesn't handle duplicate keys for a `scala.json.ast.JObject`,
nor does it handle key ordering.
- `scala.json.ast.JArray` is an `Vector`.
- Library does not allow invalid JSON in the representation and hence we can guarantee that a `scala.json.ast.JValue` will
always contain a valid structure that can be serialized/rendered into [JSON](https://en.wikipedia.org/wiki/JSON).
- Note that you can lose precision when using `scala.json.ast.JNumber` in `Scala.js` (see `Scala.js`
- Library does not allow invalid JSON in the representation and hence we can guarantee that a `scala.json.ast.JValue` will
always contain a valid structure that can be serialized/rendered into [JSON](https://en.wikipedia.org/wiki/JSON).
- Note that you can lose precision when using `scala.json.ast.JNumber` in `Scala.js` (see `Scala.js`
section for more info).
- Due to the above, has properly implemented deep equality for all types of `scala.json.ast.JValue`

Expand All @@ -61,33 +61,33 @@ Implementation is in `scala.json.unsafe.JValue`
can be considered valid under the official [JSON spec](https://www.ietf.org/rfc/rfc4627.txt), even if its not considered sane (i.e.
duplicate keys for a `scala.json.ast.unsafe.JObject`).
- Also means it can hold invalid data, due to not doing runtime checks
- Is referentially transparent in regards to `String` -> `scala.json.ast.unsafe.JValue` -> `String` since `scala.json.ast.unsafe.JObject`
- Is referentially transparent in regards to `String` -> `scala.json.ast.unsafe.JValue` -> `String` since `scala.json.ast.unsafe.JObject`
preserves ordering/duplicate keys

## Conversion between scala.json.JValue and scala.json.ast.unsafe.JValue

Any `scala.json.ast.JValue` implements a conversion to `scala.json.ast.unsafe.JValue` with a `toUnsafe` method and vice versa with a
`toStandard` method. These conversion methods have been written to be as fast as possible.

There are some peculiarities when converting between the two AST's. When converting a `scala.json.ast.unsafe.JNumber` to a
`scala.json.ast.JNumber`, it is possible for this to fail at runtime (since the internal representation of
`scala.json.ast.unsafe.JNumber` is a `String` and it doesn't have a runtime check). It is up to the caller on how to handle this error (and when),
There are some peculiarities when converting between the two AST's. When converting a `scala.json.ast.unsafe.JNumber` to a
`scala.json.ast.JNumber`, it is possible for this to fail at runtime (since the internal representation of
`scala.json.ast.unsafe.JNumber` is a `String` and it doesn't have a runtime check). It is up to the caller on how to handle this error (and when),
a runtime check is deliberately avoided on our end for performance reasons.

Converting from a `scala.json.ast.JObject` to a `scala.json.ast.unsafe.JObject` will produce
Converting from a `scala.json.ast.JObject` to a `scala.json.ast.unsafe.JObject` will produce
an `scala.json.ast.unsafe.JObject` with an undefined ordering for its internal `Array`/`js.Array` representation.
This is because a `Map` has no predefined ordering. If you wish to provide ordering, you will either need
to write your own custom conversion to handle this case. Duplicate keys will also be removed for the same reason
in an undefined manner.

Do note that according to the JSON spec, whether to order keys for a `JObject` is not specified. Also note that `Map`
Do note that according to the JSON spec, whether to order keys for a `JObject` is not specified. Also note that `Map`
disregards ordering for equality, however `Array`/`js.Array` equality takes ordering into account.

## .to[T] Conversion

Both `scala.json.ast.JNumber` and `scala.json.ast.unsafe.JNumber` provide conversions using a `.to[T]` method. These methods
Both `scala.json.ast.JNumber` and `scala.json.ast.unsafe.JNumber` provide conversions using a `.to[T]` method. These methods
provide a default fast implementations for converting between different number types (as well
as stuff like `Char[Array]`). You can provide your own implementations of a `.to[T]`
as stuff like `Char[Array]`). You can provide your own implementations of a `.to[T]`
conversion by creating an `implicit val` that implements a JNumberConverter, i.e.

```scala
Expand All @@ -99,11 +99,11 @@ implicit val myNumberConverter = new JNumberConverter[SomeNumberType]{
Then you just need to provide this implementation in scope for usage

## Scala.js
Scala json ast also provides support for [Scala.js](https://github.com/scala-js/scala-js).
There is even a separate `AST` implementation specifically for `Scala.js` with `@JSExport` for the various `JValue` types,
which means you are able to construct a `JValue` in `Javascript`in the rare cases that you may need to do so.
Hence there are added constructors for various `JValue` subtypes, i.e. you can pass in a `Javascript` `array` (i.e. `[]`)
to construct a `JArray`, as well as a constructor for `JObject` that allows you to pass in a standard `Javascript`
Scala json ast also provides support for [Scala.js](https://github.com/scala-js/scala-js).
There is even a separate `AST` implementation specifically for `Scala.js` with `@JSExport` for the various `JValue` types,
which means you are able to construct a `JValue` in `Javascript` in the rare cases that you may need to do so.
Hence there are added constructors for various `JValue` subtypes, i.e. you can pass in a `Javascript` `array` (i.e. `[]`)
to construct a `JArray`, as well as a constructor for `JObject` that allows you to pass in a standard `Javascript`
object with `JValue` as keys (i.e. `{}`).

Examples of constructing various `JValue`'s are given below.
Expand Down Expand Up @@ -132,6 +132,17 @@ var jObjectWithBoolAndNumberAndNull = new scala.json.ast.JObject({
});
```

Alternatively, `JValue` can be constructed directly from standard `Javascript` object using `JValue.fromJsAny`:

```
var jValue = new scala.json.ast.jValue({
"someString" : "string",
"someBool" : true,
"someNumber" : 324324.324,
"nullValue" : null
});
```

The `.value`, `.toJsAny` and `.toStandard`/`.toUnsafe` are also exposed in the Javascript environment.
Note that `.value` may give undefined values if the underlying datastructure doesn't have a Javascript
equivalent
Expand Down
28 changes: 28 additions & 0 deletions js/src/main/scala/scala/json/ast/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,34 @@ sealed abstract class JValue extends Product with Serializable {
def toJsAny: js.Any
}

@JSExport
object JValue {
/**
* Converts a Javascript object/value coming from Javascript to a [[JValue]].
Copy link
Owner

@mdedetrich mdedetrich Jun 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you say Scala.js Javascript object/value rather than just Javascript object/value. This is to clear confusion from Scala.js Javascript versus Javascript alone (which the AST also supports).

Also applies to anywhere else where it happens to be mentioned

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I revered the doc for toJsAny, but I'm not sure I understand the nuance here, isn't it the case that js.Any correspond to both Javascript values and Scala.js Javascript values?

*
* @return
*/
@JSExport
def fromJsAny(json: js.Any): JValue =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this have a return type of Option[JValue]? Not every js.Any can be converted to a JValue.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense 👍

json match {
case v if v == null => JNull
case v if v.isInstanceOf[Boolean] =>
if (v.asInstanceOf[Boolean]) JTrue else JFalse

case v if (v: Any).isInstanceOf[String] =>
JString(v.asInstanceOf[String])

case v: js.Array[js.Any @unchecked] =>
JArray(v.map(fromJsAny).toVector)

case v if js.typeOf(v) == "object" =>
JObject(v.asInstanceOf[js.Dictionary[js.Any]].mapValues(fromJsAny).toMap)

case v if js.typeOf(v) == "number" =>
JNumber(v.toString)
}
}

/** Represents a JSON null value
*
* @author Matthew de Detrich
Expand Down
28 changes: 28 additions & 0 deletions js/src/main/scala/scala/json/ast/unsafe/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,34 @@ sealed abstract class JValue extends Serializable with Product {
def toJsAny: js.Any
}

@JSExport
object JValue {
/**
* Converts a Javascript object/value coming from Javascript to a [[JValue]].
*
* @return
*/
@JSExport
def fromJsAny(json: js.Any): JValue =
json match {
case null => JNull
case v if v.isInstanceOf[Boolean] =>
if (v.asInstanceOf[Boolean]) JTrue else JFalse

case v if (v: Any).isInstanceOf[String] =>
JString(v.asInstanceOf[String])

case v: js.Array[js.Any @unchecked] =>
JArray(v.map(fromJsAny))

case v if js.typeOf(v) == "object" =>
JObject(v.asInstanceOf[js.Dictionary[js.Any]].map { case (k, v) => JField(k, fromJsAny(v)) }.toArray)

case v if js.typeOf(v) == "number" =>
JNumber(v.toString)
}
}

/** Represents a JSON null value
*
* @author Matthew de Detrich
Expand Down
26 changes: 14 additions & 12 deletions js/src/test/scala/specs/JValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,27 @@ object JValue extends TestSuite with UTestScalaCheck {
val tests = TestSuite {
"The JString value should" - {
"equals" - testEquals
"have a bijection with js.Any" - testBijection
}
}

def testEquals =
forAll { jValue: scala.json.ast.JValue =>
// Is there a better way to do this?
val cloned = jValue match {
case scala.json.ast.JNull => scala.json.ast.JNull
case jNumber: scala.json.ast.JNumber =>
scala.json.ast.JNumber(jNumber.value)
case jString: scala.json.ast.JString =>
scala.json.ast.JString(jString.value)
case jArray: scala.json.ast.JArray =>
scala.json.ast.JArray(jArray.value)
case jObject: scala.json.ast.JObject =>
scala.json.ast.JObject(jObject.value)
case jBoolean: scala.json.ast.JBoolean =>
scala.json.ast.JBoolean(jBoolean.get)
case scala.json.ast.JNull => scala.json.ast.JNull
case scala.json.ast.JTrue => scala.json.ast.JTrue
case scala.json.ast.JFalse => scala.json.ast.JFalse
case j: scala.json.ast.JNumber => j.copy()
case j: scala.json.ast.JString => j.copy()
case j: scala.json.ast.JArray => j.copy()
case j: scala.json.ast.JObject => j.copy()
}
jValue == cloned
}.checkUTest()

def testBijection = {
forAll { jValue: scala.json.ast.JValue =>
scala.json.ast.JValue.fromJsAny(jValue.toJsAny) == jValue
}.checkUTest()
}
}
21 changes: 21 additions & 0 deletions js/src/test/scala/specs/unsafe/JValue.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package specs.unsafe

import org.scalacheck.Prop._
import utest._
import Generators._
import specs.UTestScalaCheck

object JValue extends TestSuite with UTestScalaCheck {

val tests = TestSuite {
"The JString value should" - {
"have a bijection with js.Any" - testBijection
}
}

def testBijection = {
forAll { jValue: scala.json.ast.unsafe.JValue =>
scala.json.ast.unsafe.JValue.fromJsAny(jValue.toJsAny) == jValue
}.checkUTest()
}
}