Skip to content

Commit

Permalink
Merge pull request #826 from lolgab/support-scala-native
Browse files Browse the repository at this point in the history
Support Scala Native
  • Loading branch information
mergify[bot] authored May 4, 2023
2 parents fb8974f + f05f1ce commit cf5d178
Show file tree
Hide file tree
Showing 13 changed files with 278 additions and 112 deletions.
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
File renamed without changes.
File renamed without changes.
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>
*/

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)}""""
if (!escapeNonASCII) stringified else StaticBindingNonJvm.escapeStr(stringified)
}

}
Loading

0 comments on commit cf5d178

Please sign in to comment.