-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement conversion from/to native JS objects.
- Loading branch information
Showing
9 changed files
with
512 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
modules/core/.js/src/main/scala/com/github/tarao/record4s/ArrayRecordPlatformSpecific.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.github.tarao.record4s | ||
|
||
import org.getshaka.nativeconverter.NativeConverter | ||
import scala.scalajs.js | ||
|
||
trait ArrayRecordPlatformSpecific { | ||
def fromJS[T <: Tuple](obj: js.Any)(using | ||
nc: NativeConverter[ArrayRecord[T]], | ||
): ArrayRecord[T] = nc.fromNative(obj) | ||
|
||
def fromJSON[T <: Tuple](json: String)(using | ||
nc: NativeConverter[ArrayRecord[T]], | ||
): ArrayRecord[T] = nc.fromJson(json) | ||
|
||
extension [R](record: ArrayRecord[R]) { | ||
def toJS(using NativeConverter[ArrayRecord[R]]): js.Any = record.toNative | ||
|
||
def toJSON(using NativeConverter[ArrayRecord[R]]): String = record.toJson | ||
} | ||
|
||
inline given nativeConverter[R]: NativeConverter[ArrayRecord[R]] = | ||
NativeConverter.derived | ||
} |
88 changes: 88 additions & 0 deletions
88
modules/core/.js/src/main/scala/com/github/tarao/record4s/RecordPlatformSpecific.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package com.github.tarao.record4s | ||
|
||
import org.getshaka.nativeconverter.{NativeConverter, ParseState} | ||
import scala.compiletime.{constValue, erasedValue, summonInline} | ||
import scala.scalajs.js | ||
|
||
trait RecordPlatformSpecific { | ||
def fromJS[R <: %](obj: js.Any)(using nc: NativeConverter[R]): R = | ||
nc.fromNative(obj) | ||
|
||
def fromJSON[R <: %](json: String)(using nc: NativeConverter[R]): R = | ||
nc.fromJson(json) | ||
|
||
extension [R <: %](record: R) { | ||
def toJS(using NativeConverter[R]): js.Any = record.toNative | ||
|
||
def toJSON(using NativeConverter[R]): String = record.toJson | ||
} | ||
|
||
private type ImplicitlyJsAny = | ||
String | Boolean | Byte | Short | Int | Float | Double | Null | js.Any | ||
|
||
private inline def fieldsToNative[Types, Labels]( | ||
record: Map[String, Any], | ||
res: js.Dynamic = js.Object().asInstanceOf[js.Dynamic], | ||
): js.Any = | ||
inline (erasedValue[Types], erasedValue[Labels]) match { | ||
case _: (EmptyTuple, EmptyTuple) => | ||
res | ||
|
||
case _: (tpe *: types, label *: labels) => | ||
val labelStr = constValue[label & String] | ||
val nativeElem = | ||
inline erasedValue[tpe] match { | ||
case _: ImplicitlyJsAny => | ||
record(labelStr).asInstanceOf[js.Any] | ||
case _ => | ||
val nc = summonInline[NativeConverter[tpe]] | ||
val elem = record(labelStr).asInstanceOf[tpe] | ||
nc.toNative(elem) | ||
} | ||
res.updateDynamic(labelStr)(nativeElem) | ||
|
||
fieldsToNative[types, labels](record, res) | ||
} | ||
|
||
private inline def nativeToFields[Types, Labels]( | ||
dict: js.Dictionary[js.Any], | ||
ps: ParseState, | ||
res: Seq[(String, Any)] = Seq.empty, | ||
): Seq[(String, Any)] = | ||
inline (erasedValue[Types], erasedValue[Labels]) match { | ||
case _: (EmptyTuple, EmptyTuple) => | ||
res | ||
|
||
case _: (tpe *: types, label *: labels) => | ||
val nc = summonInline[NativeConverter[tpe]] | ||
val labelStr = constValue[label & String] | ||
val jsElem = dict.getOrElse(labelStr, null) | ||
val elem = nc.fromNative(ps.atKey(labelStr, jsElem)) | ||
nativeToFields[types, labels](dict, ps, res :+ (labelStr, elem)) | ||
} | ||
|
||
private def asDict(ps: ParseState): js.Dictionary[js.Any] = | ||
ps.json match { | ||
case o: js.Object => o.asInstanceOf[js.Dictionary[js.Any]] | ||
case _ => ps.fail("js.Object") | ||
} | ||
|
||
inline given nativeConverter[R <: %](using | ||
r: RecordLike[R], | ||
): NativeConverter[R] = { | ||
type Types = r.ElemTypes | ||
type Labels = r.ElemLabels | ||
|
||
new NativeConverter[R] { | ||
extension (record: R) { | ||
def toNative: js.Any = | ||
fieldsToNative[Types, Labels](r.iterableOf(record).toMap) | ||
} | ||
|
||
def fromNative(ps: ParseState): R = { | ||
val iterable = nativeToFields[Types, Labels](asDict(ps), ps) | ||
Record.newMapRecord[R](iterable) | ||
} | ||
} | ||
} | ||
} |
183 changes: 183 additions & 0 deletions
183
modules/core/.js/src/test/scala/com/github/tarao/record4s/JSArrayRecordSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
package com.github.tarao.record4s | ||
|
||
import scala.scalajs.js | ||
|
||
class JSArrayRecordSpec extends helper.UnitSpec { | ||
describe("ArrayRecord in JS") { | ||
describe("fromJS") { | ||
it("should convert js.Any to ArrayRecord") { | ||
val obj: js.Any = js.Dynamic.literal(name = "tarao", age = 3) | ||
val r = ArrayRecord.fromJS[(("name", String), ("age", Int))](obj) | ||
r shouldStaticallyBe a[ArrayRecord[(("name", String), ("age", Int))]] | ||
r.name shouldBe "tarao" | ||
r.age shouldBe 3 | ||
} | ||
|
||
it("should convert js.Any to nested ArrayRecord") { | ||
val obj: js.Any = js | ||
.Dynamic | ||
.literal( | ||
name = "tarao", | ||
age = 3, | ||
email = js | ||
.Dynamic | ||
.literal( | ||
local = "tarao", | ||
domain = "example.com", | ||
), | ||
) | ||
val r = ArrayRecord.fromJS[ | ||
( | ||
("name", String), | ||
("age", Int), | ||
("email", ArrayRecord[(("local", String), ("domain", String))]), | ||
), | ||
](obj) | ||
r shouldStaticallyBe a[ArrayRecord[ | ||
( | ||
("name", String), | ||
("age", Int), | ||
("email", ArrayRecord[(("local", String), ("domain", String))]), | ||
), | ||
]] | ||
r.name shouldBe "tarao" | ||
r.age shouldBe 3 | ||
r.email.local shouldBe "tarao" | ||
r.email.domain shouldBe "example.com" | ||
} | ||
|
||
it("should ignore extra fields") { | ||
val obj: js.Any = js.Dynamic.literal(name = "tarao", age = 3) | ||
val r = ArrayRecord.fromJS[("name", String) *: EmptyTuple](obj) | ||
r shouldStaticallyBe a[ArrayRecord[("name", String) *: EmptyTuple]] | ||
r.name shouldBe "tarao" | ||
"r.age" shouldNot typeCheck | ||
} | ||
|
||
it("should fill nulls for missing fields") { | ||
val obj: js.Any = js.Dynamic.literal() | ||
val r = ArrayRecord.fromJS[(("name", String), ("age", Int))](obj) | ||
r shouldStaticallyBe a[ArrayRecord[(("name", String), ("age", Int))]] | ||
r.name shouldBe null | ||
r.age shouldBe 0 | ||
} | ||
|
||
it("should throw if an object field is missing") { | ||
val obj: js.Any = js.Dynamic.literal(name = "tarao", age = 3) | ||
a[java.lang.RuntimeException] should be thrownBy | ||
ArrayRecord.fromJS[ | ||
( | ||
("name", String), | ||
("age", Int), | ||
("email", ArrayRecord[(("local", String), ("domain", String))]), | ||
), | ||
](obj) | ||
} | ||
} | ||
|
||
describe("fromJSON") { | ||
it("should convert JSON String to ArrayRecord") { | ||
val json = """{"name":"tarao","age":3}""" | ||
val r = ArrayRecord.fromJSON[(("name", String), ("age", Int))](json) | ||
r shouldStaticallyBe a[ArrayRecord[(("name", String), ("age", Int))]] | ||
r.name shouldBe "tarao" | ||
r.age shouldBe 3 | ||
} | ||
|
||
it("should convert JSON String to nested ArrayRecord") { | ||
val json = | ||
"""{"name":"tarao","age":3,"email":{"local":"tarao","domain":"example.com"}}""" | ||
val r = ArrayRecord.fromJSON[ | ||
( | ||
("name", String), | ||
("age", Int), | ||
("email", ArrayRecord[(("local", String), ("domain", String))]), | ||
), | ||
](json) | ||
r shouldStaticallyBe a[ArrayRecord[ | ||
( | ||
("name", String), | ||
("age", Int), | ||
("email", ArrayRecord[(("local", String), ("domain", String))]), | ||
), | ||
]] | ||
r.name shouldBe "tarao" | ||
r.age shouldBe 3 | ||
r.email.local shouldBe "tarao" | ||
r.email.domain shouldBe "example.com" | ||
} | ||
|
||
it("should ignore extra fields") { | ||
val json = """{"name":"tarao","age":3}""" | ||
val r = ArrayRecord.fromJSON[("name", String) *: EmptyTuple](json) | ||
r shouldStaticallyBe a[ArrayRecord[("name", String) *: EmptyTuple]] | ||
r.name shouldBe "tarao" | ||
"r.age" shouldNot typeCheck | ||
} | ||
|
||
it("should fill nulls for missing fields") { | ||
val json = """{}""" | ||
val r = ArrayRecord.fromJSON[(("name", String), ("age", Int))](json) | ||
r shouldStaticallyBe a[ArrayRecord[(("name", String), ("age", Int))]] | ||
r.name shouldBe null | ||
r.age shouldBe 0 | ||
} | ||
|
||
it("should throw if an object field is missing") { | ||
val json = """{"name":"tarao","age":3}""" | ||
a[java.lang.RuntimeException] should be thrownBy | ||
ArrayRecord.fromJSON[ | ||
( | ||
("name", String), | ||
("age", Int), | ||
("email", ArrayRecord[(("local", String), ("domain", String))]), | ||
), | ||
](json) | ||
} | ||
} | ||
|
||
describe("toJS") { | ||
it("should convert ArrayRecord to js.Any") { | ||
val r = ArrayRecord(name = "tarao", age = 3) | ||
val obj = r.toJS | ||
obj shouldStaticallyBe a[js.Any] | ||
js.JSON.stringify(obj) shouldBe """{"name":"tarao","age":3}""" | ||
} | ||
|
||
it("should convert nested ArrayRecord to js.Any") { | ||
val r = ArrayRecord( | ||
name = "tarao", | ||
age = 3, | ||
email = ArrayRecord( | ||
local = "tarao", | ||
domain = "example.com", | ||
), | ||
) | ||
val obj = r.toJS | ||
obj shouldStaticallyBe a[js.Any] | ||
js.JSON.stringify(obj) shouldBe """{"name":"tarao","age":3,"email":{"local":"tarao","domain":"example.com"}}""" | ||
} | ||
} | ||
|
||
describe("toJSON") { | ||
it("should convert ArrayRecord to JSON String") { | ||
val r = ArrayRecord(name = "tarao", age = 3) | ||
val json = r.toJSON | ||
json shouldBe """{"name":"tarao","age":3}""" | ||
} | ||
|
||
it("should convert nested ArrayRecord to JSON String") { | ||
val r = ArrayRecord( | ||
name = "tarao", | ||
age = 3, | ||
email = ArrayRecord( | ||
local = "tarao", | ||
domain = "example.com", | ||
), | ||
) | ||
val json = r.toJSON | ||
json shouldBe """{"name":"tarao","age":3,"email":{"local":"tarao","domain":"example.com"}}""" | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.