Skip to content
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

Support Scala Native #826

Merged
merged 11 commits into from
May 4, 2023
20 changes: 15 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def playJsonMimaSettings = Seq(
mimaPreviousArtifacts := ((crossProjectPlatform.?.value, previousStableVersion.value) match {
case _ if isScala3.value => Set.empty // no releases for Scala 3 yet
case (Some(JSPlatform), Some("2.8.1")) => Set.empty
case (Some(NativePlatform), _) => Set.empty // no release for Scala Native yet
case (_, Some(previousVersion)) =>
val stableVersion = if (previousVersion.startsWith("2.10.0-RC")) "2.9.2" else previousVersion
Set(organization.value %%% moduleName.value % stableVersion)
Expand Down Expand Up @@ -128,14 +129,16 @@ lazy val root = project
.aggregate(
`play-jsonJS`,
`play-jsonJVM`,
`play-jsonNative`,
`play-functionalJS`,
`play-functionalJVM`,
`play-functionalNative`,
`play-json-joda`
)
.settings(commonSettings)
.settings(publish / skip := true)

lazy val `play-json` = crossProject(JVMPlatform, JSPlatform)
lazy val `play-json` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Full)
.in(file("play-json"))
.enablePlugins(Omnidoc, Playdoc)
Expand All @@ -145,6 +148,11 @@ lazy val `play-json` = crossProject(JVMPlatform, JSPlatform)
("org.scala-js" %%% "scalajs-java-securerandom" % "1.0.0").cross(CrossVersion.for3Use2_13),
)
)
.nativeSettings(
libraryDependencies ++= Seq(
"org.typelevel" %%% "jawn-parser" % "1.4.0"
)
)
.settings(
commonSettings ++ playJsonMimaSettings ++ Def.settings(
libraryDependencies ++= (
Expand Down Expand Up @@ -234,7 +242,8 @@ lazy val `play-json` = crossProject(JVMPlatform, JSPlatform)
)
.dependsOn(`play-functional`)

lazy val `play-jsonJS` = `play-json`.js
lazy val `play-jsonJS` = `play-json`.js
lazy val `play-jsonNative` = `play-json`.native

lazy val `play-jsonJVM` = `play-json`.jvm
.settings(
Expand Down Expand Up @@ -267,16 +276,17 @@ lazy val `play-json-joda` = project
)
.dependsOn(`play-jsonJVM`)

lazy val `play-functional` = crossProject(JVMPlatform, JSPlatform)
lazy val `play-functional` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.in(file("play-functional"))
.settings(
commonSettings ++ playJsonMimaSettings
)
.enablePlugins(Omnidoc)

lazy val `play-functionalJVM` = `play-functional`.jvm
lazy val `play-functionalJS` = `play-functional`.js
lazy val `play-functionalJVM` = `play-functional`.jvm
lazy val `play-functionalJS` = `play-functional`.js
lazy val `play-functionalNative` = `play-functional`.native

lazy val benchmarks = project
.in(file("benchmarks"))
Expand Down
113 changes: 113 additions & 0 deletions play-json/js-native/src/main/scala/StaticBindingNonJvm.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2009-2021 Lightbend Inc. <https://www.lightbend.com>
*/

package play.api.libs.json

import java.io.InputStreamReader

private[json] object StaticBindingNonJvm {

/** Parses a [[JsValue]] from a stream (assuming UTF-8). */
def parseJsValue(stream: java.io.InputStream): JsValue = {
var in: InputStreamReader = null

try {
in = new java.io.InputStreamReader(stream, "UTF-8")
val acc = new StringBuilder()
val buf = Array.ofDim[Char](1024)

@annotation.tailrec
def read(): String = {
val r = in.read(buf, 0, 1024)

if (r == 1024) {
acc ++= buf
read()
} else if (r > 0) {
acc ++= buf.slice(0, r)
read()
} else acc.result()
}

StaticBinding.parseJsValue(read())
} catch {
case err: Throwable => throw err
} finally {
if (in != null) in.close()
}
}

def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String =
fromJs(jsValue, escapeNonASCII, 0, _ => "")

def prettyPrint(jsValue: JsValue): String =
fromJs(
jsValue,
false,
0,
{ l =>
0.until(l * 2).map(_ => ' ').mkString
},
newline = true,
fieldValueSep = " : ",
arraySep = ("[ ", ", ", " ]")
)

def toBytes(jsValue: JsValue): Array[Byte] =
generateFromJsValue(jsValue, false).getBytes("UTF-8")

def fromJs(
jsValue: JsValue,
escapeNonASCII: Boolean,
ilevel: Int,
indent: Int => String,
newline: Boolean = false,
fieldValueSep: String = ":",
arraySep: (String, String, String) = ("[", ",", "]")
): String = {
def str = jsValue match {
case JsNull => "null"
case JsString(s) => StaticBinding.fromString(s, escapeNonASCII)
case JsNumber(n) => n.toString
case JsTrue => "true"
case JsFalse => "false"

case JsArray(items) => {
val il = ilevel + 1

items
.map(fromJs(_, escapeNonASCII, il, indent, newline, fieldValueSep, arraySep))
.mkString(arraySep._1, arraySep._2, arraySep._3)
}

case JsObject(fields) => {
val il = ilevel + 1
val (before, after) = if (newline) {
s"\n${indent(il)}" -> s"\n${indent(ilevel)}}"
} else indent(il) -> "}"

fields
.map { case (k, v) =>
@inline def key = StaticBinding.fromString(k, escapeNonASCII)
@inline def value = fromJs(v, escapeNonASCII, il, indent, newline, fieldValueSep, arraySep)

s"$before$key$fieldValueSep$value"
}
.mkString("{", ",", after)
}
}

str
}

def escapeStr(s: String): String = s.flatMap { c =>
val code = c.toInt

if (code > 31 && code < 127 /* US-ASCII */ ) String.valueOf(c)
else {
def hexCode = code.toHexString.reverse.padTo(4, '0').reverse
'\\' +: s"u${hexCode.toUpperCase}"
}
}
}
110 changes: 7 additions & 103 deletions play-json/js/src/main/scala/StaticBinding.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

package play.api.libs.json

import java.io.InputStreamReader

import scalajs.js
import js.JSON

Expand All @@ -16,106 +14,22 @@ object StaticBinding {
parseJsValue(new String(data, "UTF-8"))

/** Parses a [[JsValue]] from a stream (assuming UTF-8). */
def parseJsValue(stream: java.io.InputStream): JsValue = {
var in: InputStreamReader = null

try {
in = new java.io.InputStreamReader(stream, "UTF-8")
val acc = new StringBuilder()
val buf = Array.ofDim[Char](1024)

@annotation.tailrec
def read(): String = {
val r = in.read(buf, 0, 1024)

if (r == 1024) {
acc ++= buf
read()
} else if (r > 0) {
acc ++= buf.slice(0, r)
read()
} else acc.result()
}

parseJsValue(read())
} catch {
case err: Throwable => throw err
} finally {
if (in != null) in.close()
}
}
def parseJsValue(stream: java.io.InputStream): JsValue =
StaticBindingNonJvm.parseJsValue(stream)

/** Parses a [[JsValue]] from a string content. */
def parseJsValue(input: String): JsValue =
anyToJsValue(JSON.parse(input))

def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String =
fromJs(jsValue, escapeNonASCII, 0, _ => "")

def prettyPrint(jsValue: JsValue): String =
fromJs(
jsValue,
false,
0,
{ l =>
0.until(l * 2).map(_ => ' ').mkString
},
newline = true,
fieldValueSep = " : ",
arraySep = ("[ ", ", ", " ]")
)

def toBytes(jsValue: JsValue): Array[Byte] =
generateFromJsValue(jsValue, false).getBytes("UTF-8")

// ---

private def fromJs(
jsValue: JsValue,
escapeNonASCII: Boolean,
ilevel: Int,
indent: Int => String,
newline: Boolean = false,
fieldValueSep: String = ":",
arraySep: (String, String, String) = ("[", ",", "]")
): String = {
def str = jsValue match {
case JsNull => "null"
case JsString(s) => fromString(s, escapeNonASCII)
case JsNumber(n) => n.toString
case JsTrue => "true"
case JsFalse => "false"
StaticBindingNonJvm.generateFromJsValue(jsValue, escapeNonASCII)

case JsArray(items) => {
val il = ilevel + 1
def prettyPrint(jsValue: JsValue): String = StaticBindingNonJvm.prettyPrint(jsValue)

items
.map(fromJs(_, escapeNonASCII, il, indent, newline, fieldValueSep, arraySep))
.mkString(arraySep._1, arraySep._2, arraySep._3)
}
def toBytes(jsValue: JsValue): Array[Byte] = StaticBindingNonJvm.toBytes(jsValue)

case JsObject(fields) => {
val il = ilevel + 1
val (before, after) = if (newline) {
s"\n${indent(il)}" -> s"\n${indent(ilevel)}}"
} else indent(il) -> "}"

fields
.map { case (k, v) =>
@inline def key = fromString(k, escapeNonASCII)
@inline def value = fromJs(v, escapeNonASCII, il, indent, newline, fieldValueSep, arraySep)

s"$before$key$fieldValueSep$value"
}
.mkString("{", ",", after)
}
}

str
}

@inline private def fromString(s: String, escapeNonASCII: Boolean): String =
if (!escapeNonASCII) JSON.stringify(s, null) else escapeStr(JSON.stringify(s, null))
@inline private[json] def fromString(s: String, escapeNonASCII: Boolean): String =
if (!escapeNonASCII) JSON.stringify(s, null) else StaticBindingNonJvm.escapeStr(JSON.stringify(s, null))

private def anyToJsValue(raw: Any): JsValue = raw match {
case null => JsNull
Expand All @@ -136,14 +50,4 @@ object StaticBinding {

case _ => sys.error(s"Unexpected JS value: $raw")
}

private def escapeStr(s: String): String = s.flatMap { c =>
val code = c.toInt

if (code > 31 && code < 127 /* US-ASCII */ ) String.valueOf(c)
else {
def hexCode = code.toHexString.reverse.padTo(4, '0').reverse
'\\' +: s"u${hexCode.toUpperCase}"
}
}
}
1 change: 0 additions & 1 deletion play-json/js/src/test/scala/JsonSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package play.api.libs.json

import play.api.libs.json.Json._

import org.scalatest._
import org.scalatest.matchers.must.Matchers
import org.scalatest.wordspec.AnyWordSpec

Expand Down
58 changes: 58 additions & 0 deletions play-json/native/src/main/scala/StaticBinding.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2009-2021 Lightbend Inc. <https://www.lightbend.com>
Copy link
Member

Choose a reason for hiding this comment

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

I figure this is needed to ensure linting works on the new rules. But this isn't really associated with Lightbend anymore. @mkurz we should visit revamping the requirements on copyrights and file headers.

Copy link
Member

Choose a reason for hiding this comment

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

I will do that in a upcoming pr.

*/

package play.api.libs.json

import org.typelevel.jawn

object StaticBinding {

private implicit object JsValueFacade extends jawn.Facade.SimpleFacade[JsValue] {
final def jfalse: JsValue = JsFalse
final def jnull: JsValue = JsNull
final def jnum(s: CharSequence, decIndex: Int, expIndex: Int): JsValue = JsNumber(
new java.math.BigDecimal(s.toString)
)
final def jstring(s: CharSequence): JsValue = JsString(s.toString)
final def jtrue: JsValue = JsTrue

final def jarray(vs: List[JsValue]): JsValue = JsArray(vs)
final def jobject(vs: Map[String, JsValue]): JsValue = JsObject(vs)
}

/** Parses a [[JsValue]] from raw data (assuming UTF-8). */
def parseJsValue(data: Array[Byte]): JsValue =
new jawn.ByteArrayParser[JsValue](data).parse()

/** Parses a [[JsValue]] from a string content. */
def parseJsValue(input: String): JsValue =
jawn.Parser.parseUnsafe[JsValue](input)

/** Parses a [[JsValue]] from a stream (assuming UTF-8). */
def parseJsValue(stream: java.io.InputStream): JsValue =
StaticBindingNonJvm.parseJsValue(stream)

def generateFromJsValue(jsValue: JsValue, escapeNonASCII: Boolean): String =
StaticBindingNonJvm.generateFromJsValue(jsValue, escapeNonASCII)

def prettyPrint(jsValue: JsValue): String = StaticBindingNonJvm.prettyPrint(jsValue)

def toBytes(jsValue: JsValue): Array[Byte] = StaticBindingNonJvm.toBytes(jsValue)

@inline private[json] def fromString(s: String, escapeNonASCII: Boolean): String = {
def escaped(c: Char) = c match {
case '\b' => "\\b"
case '\f' => "\\f"
case '\n' => "\\n"
case '\r' => "\\r"
case '\t' => "\\t"
case '\\' => "\\\\"
case '\"' => "\\\""
case c => c.toString
}
val stringified = if (s == null) "null" else s""""${s.flatMap(escaped)}""""
cchantep marked this conversation as resolved.
Show resolved Hide resolved
if (!escapeNonASCII) stringified else StaticBindingNonJvm.escapeStr(stringified)
}

}
Loading