diff --git a/.mdl/defs/actions.md b/.mdl/defs/actions.md index 78ead08..e6fa1a6 100644 --- a/.mdl/defs/actions.md +++ b/.mdl/defs/actions.md @@ -176,6 +176,7 @@ if [[ "$CI_PULL_REQUEST_VAL" != "false" ]]; then exit 0 fi +SYSTEM=$(uname -s | tr '[:upper:]' '[:lower:]') PROJECT_ROOT="${sys.project-root}" DIST_DIR="${action.build-sjs.dist-dir}" PUBLISH_DIR="${PROJECT_ROOT}/npm-publish" @@ -193,7 +194,11 @@ mkdir -p "$PUBLISH_DIR" cp "${DIST_DIR}"/* "$PUBLISH_DIR"/ cp "${PROJECT_ROOT}/json-sick-scala/npm-template/"* "$PUBLISH_DIR"/ -sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" "$PUBLISH_DIR/package.json" +if [[ "${SYSTEM}" == "darwin" ]]; then + sed -i '' "s/VERSION_PLACEHOLDER/$VERSION/g" "$PUBLISH_DIR/package.json" +else + sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" "$PUBLISH_DIR/package.json" +fi cd "$PUBLISH_DIR" diff --git a/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/ArrayCursorJs.scala b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/ArrayCursorJs.scala new file mode 100644 index 0000000..eb98332 --- /dev/null +++ b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/ArrayCursorJs.scala @@ -0,0 +1,22 @@ +package izumi.sick.eba.cursor + +import scala.scalajs.js.annotation.{JSExportAll, JSExportTopLevel} + +@JSExportTopLevel("ArrayCursor") +@JSExportAll +class ArrayCursorJs(cursor: ArrayCursor) extends SickCursorJs(cursor.asInstanceOf[SickCursor]) { + + def left: ArrayCursorJs = { + new ArrayCursorJs(cursor.left) + } + + def right: ArrayCursorJs = { + new ArrayCursorJs(cursor.right) + } + + def value: SickCursorJs = downIndex(cursor.index) + + def downIndex(index: Int): SickCursorJs = { + new SickCursorJs(cursor.downIndex(index)) + } +} diff --git a/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/ObjectCursorJs.scala b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/ObjectCursorJs.scala new file mode 100644 index 0000000..29a074a --- /dev/null +++ b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/ObjectCursorJs.scala @@ -0,0 +1,7 @@ +package izumi.sick.eba.cursor + +import scala.scalajs.js.annotation.{JSExportAll, JSExportTopLevel} + +@JSExportTopLevel("ObjectCursor") +@JSExportAll +class ObjectCursorJs(cursor: ObjectCursor) extends TopCursorJs(cursor.asInstanceOf[TopCursor]) diff --git a/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/SickCursorJs.scala b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/SickCursorJs.scala new file mode 100644 index 0000000..7be594f --- /dev/null +++ b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/SickCursorJs.scala @@ -0,0 +1,37 @@ +package izumi.sick.eba.cursor + +import scala.scalajs.js +import scala.scalajs.js.annotation.{JSExportAll, JSExportTopLevel} +import scala.scalajs.js.JSConverters._ + +@JSExportTopLevel("SickCursor") +@JSExportAll +class SickCursorJs(cursor: SickCursor) { + def downField(field: String): ObjectCursorJs = { + new ObjectCursorJs(cursor.downField(field)) + } + + def downArray: ArrayCursorJs = { + new ArrayCursorJs(cursor.downArray) + } + + def asNul: js.UndefOr[Null] = cursor.asNul.orUndefined + + def asBool: js.UndefOr[Boolean] = cursor.asBool.orUndefined + + def asByte: js.UndefOr[Byte] = cursor.asByte.orUndefined + + def asShort: js.UndefOr[Short] = cursor.asShort.orUndefined + + def asInt: js.UndefOr[Int] = cursor.asInt.orUndefined + + def asLong: js.UndefOr[Long] = cursor.asLong.orUndefined + + def asBigInt: js.UndefOr[js.BigInt] = cursor.asBigInt.map(v => js.BigInt(v.toString)).orUndefined + + def asFloat: js.UndefOr[Float] = cursor.asFloat.orUndefined + + def asDouble: js.UndefOr[Double] = cursor.asDouble.orUndefined + + def asString: js.UndefOr[String] = cursor.asString.orUndefined +} diff --git a/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/TopCursorJs.scala b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/TopCursorJs.scala new file mode 100644 index 0000000..f1abbf9 --- /dev/null +++ b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/eba/cursor/TopCursorJs.scala @@ -0,0 +1,22 @@ +package izumi.sick.eba.cursor + +import scala.scalajs.js.annotation.{JSExportAll, JSExportTopLevel} + +import scala.scalajs.js +import scala.scalajs.js.JSConverters.* + +@JSExportTopLevel("TopCursor") +@JSExportAll +class TopCursorJs(cursor: TopCursor) extends SickCursorJs(cursor.asInstanceOf[SickCursor]) { + def query(request: String): ObjectCursorJs = { + new ObjectCursorJs(cursor.query(request)) + } + + def getValues: js.Map[String, ObjectCursorJs] = { + cursor.getValues.view.mapValues(new ObjectCursorJs(_)).toMap.toJSMap + } + + def readKey(index: Int): ObjectCursorJs = { + new ObjectCursorJs(cursor.readKey(index)) + } +} diff --git a/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/jsapi/SickJsAPI.scala b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/jsapi/SickJsAPI.scala index 8a5b97e..41fec33 100644 --- a/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/jsapi/SickJsAPI.scala +++ b/json-sick-scala/json-sick/.js/src/main/scala/izumi/sick/jsapi/SickJsAPI.scala @@ -3,7 +3,8 @@ package izumi.sick.jsapi import io.circe.Json import izumi.sick.SICK import izumi.sick.eba.SICKSettings -import izumi.sick.eba.reader.EagerEBAReader +import izumi.sick.eba.cursor.TopCursorJs +import izumi.sick.eba.reader.{EagerEBAReader, IncrementalEBAReader} import izumi.sick.eba.writer.EBAWriter import izumi.sick.model.{SICKWriterParameters, TableWriteStrategy} import izumi.sick.sickcirce.CirceTraverser.* @@ -84,4 +85,17 @@ object SickJsAPI { val bytes = res._1.toArrayUnsafe() bytesToUint8Array(bytes) } + + /** + * Accepts an instance of `Uint8Array` and the rootId, returns a cursor to navigate through the structure + * + * `{ data: { a: 2, b: { c: 3 } } }` + * `const cursor = sickCursorFromUint8Array(uint8Array, "data")` + * `cursor.downField("b").downField("c").asInt` + */ + @JSExportTopLevel("sickCursorFromUint8Array") + def sickCursorFromUint8Array(uint8Array: Uint8Array, rootId: String): TopCursorJs = { + val ebaReader = IncrementalEBAReader.openBytes(uint8ArrayToBytes(uint8Array), eagerOffsets = false) + new TopCursorJs(ebaReader.getCursor(rootId)) + } } diff --git a/json-sick-scala/json-sick/.js/src/test/scala/io/izumi/sick/JsApiTest.scala b/json-sick-scala/json-sick/.js/src/test/scala/io/izumi/sick/JsApiTest.scala index c243faf..e7a4468 100644 --- a/json-sick-scala/json-sick/.js/src/test/scala/io/izumi/sick/JsApiTest.scala +++ b/json-sick-scala/json-sick/.js/src/test/scala/io/izumi/sick/JsApiTest.scala @@ -31,6 +31,14 @@ class JsApiTest extends AnyWordSpec { val circeJson = io.circe.scalajs.convertJsToJson(jsAny).toTry.get assert(circeJson == Json.obj("root1" -> Json.obj("a" -> Json.fromInt(2)), "root2" -> Json.obj("b" -> Json.fromInt(3)))) } + + locally { + val uint8Array = SickJsAPI.encodeObjsToSickUint8Array(js.Dictionary("data" -> js.Dynamic.literal(a = 1, b = 2, c = 3))) + val cursor = SickJsAPI.sickCursorFromUint8Array(uint8Array, "data") + assert(cursor.downField("a").asInt.toOption.contains(1)) + assert(cursor.downField("b").asInt.toOption.contains(2)) + assert(cursor.downField("c").asInt.toOption.contains(3)) + } } } diff --git a/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/ArrayCursor.scala b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/ArrayCursor.scala new file mode 100644 index 0000000..d0cd9a6 --- /dev/null +++ b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/ArrayCursor.scala @@ -0,0 +1,29 @@ +package izumi.sick.eba.cursor + +import izumi.sick.eba.reader.IncrementalEBAReader +import izumi.sick.model.Ref +import izumi.sick.model.RefKind.TArr + +class ArrayCursor(val ref: Ref, val ebaReader: IncrementalEBAReader, val index: Int = 0) extends SickCursor { + private val length = ebaReader.arrTable.readElem(ref.ref).length + + def left: ArrayCursor = { + if (index == 0) throw new ArrayIndexOutOfBoundsException("Can not move left: Index is on first element") + else new ArrayCursor(ref, ebaReader, index - 1) + } + + def right: ArrayCursor = { + if (index == length - 1) throw new ArrayIndexOutOfBoundsException(s"Can not move right: Index is on last element") + else new ArrayCursor(ref, ebaReader, index + 1) + } + + def value: SickCursor = downIndex(index) + + def downIndex(index: Int): SickCursor = { + val newRef = ebaReader.readArrayElementRef(ref, index) + newRef.kind match { + case TArr => new ArrayCursor(newRef, ebaReader) + case _ => new ObjectCursor(newRef, ebaReader) + } + } +} diff --git a/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/ObjectCursor.scala b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/ObjectCursor.scala new file mode 100644 index 0000000..dc537b2 --- /dev/null +++ b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/ObjectCursor.scala @@ -0,0 +1,6 @@ +package izumi.sick.eba.cursor + +import izumi.sick.eba.reader.IncrementalEBAReader +import izumi.sick.model.Ref + +class ObjectCursor(override val ref: Ref, override val ebaReader: IncrementalEBAReader) extends TopCursor(ref, ebaReader) diff --git a/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/SickCursor.scala b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/SickCursor.scala new file mode 100644 index 0000000..3a5c4d8 --- /dev/null +++ b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/SickCursor.scala @@ -0,0 +1,126 @@ +package izumi.sick.eba.cursor + +import izumi.sick.eba.reader.IncrementalEBAReader +import izumi.sick.model.RefKind.* +import izumi.sick.model.{Ref, RefKind} + +abstract class SickCursor { + def ref: Ref + def ebaReader: IncrementalEBAReader + + def downField(field: String): ObjectCursor = { + if (ref.kind != TArr) { + val newRef = ebaReader.readObjectFieldRef(ref, field) + new ObjectCursor(newRef, ebaReader) + } else throw new IllegalArgumentException("Ref has an array kind") + } + + def downArray: ArrayCursor = { + if (ref.kind == TArr) { + new ArrayCursor(ref, ebaReader) + } else throw new IllegalArgumentException("Ref is not an array kind") + } + + def asObjectCursor: ObjectCursor = { + this.asInstanceOf[ObjectCursor] + } + + def asNul: Option[Null] = { + if (ref.kind == TNul) Some(null) + else None + } + + def asBool: Option[Boolean] = { + if (ref.kind == TBit) Some(ref.ref == 1) + else None + } + + def asByte: Option[Byte] = { + if (ref.kind == TByte) Some(ref.ref.toByte) + else None + } + + def asShort: Option[Short] = { + ref.kind match { + case RefKind.TByte => Some(ref.ref.toShort) + case RefKind.TShort => Some(ref.ref.toShort) + case _ => None + } + } + + def asInt: Option[Int] = { + ref.kind match { + case RefKind.TByte => Some(ref.ref.toInt) + case RefKind.TShort => Some(ref.ref.toInt) + case RefKind.TInt => Some(ebaReader.intTable.readElem(ref.ref)) + case _ => None + } + } + + def asLong: Option[Long] = { + ref.kind match { + case RefKind.TByte => Some(ref.ref.toLong) + case RefKind.TShort => Some(ref.ref.toLong) + case RefKind.TInt => Some(ebaReader.intTable.readElem(ref.ref).toLong) + case RefKind.TLng => Some(ebaReader.longTable.readElem(ref.ref)) + case _ => None + } + } + + def asBigInt: Option[BigInt] = { + ref.kind match { + case RefKind.TByte => Some(BigInt.apply(ref.ref.toLong)) + case RefKind.TShort => Some(BigInt.apply(ref.ref.toLong)) + case RefKind.TInt => + Some(BigInt.apply(ebaReader.intTable.readElem(ref.ref).toLong)) + case RefKind.TLng => Some(BigInt.apply(ebaReader.longTable.readElem(ref.ref))) + case RefKind.TBigInt => Some(ebaReader.bigIntTable.readElem(ref.ref)) + case _ => None + } + } + + def asFloat: Option[Float] = { + ref.kind match { + case RefKind.TByte => Some(ref.ref.toFloat) + case RefKind.TShort => Some(ref.ref.toFloat) + case RefKind.TInt => Some(ebaReader.intTable.readElem(ref.ref).toFloat) + case RefKind.TLng => Some(ebaReader.longTable.readElem(ref.ref).toFloat) + case RefKind.TFlt => Some(ebaReader.floatTable.readElem(ref.ref)) + case _ => None + } + } + + def asDouble: Option[Double] = { + ref.kind match { + case RefKind.TByte => Some(ref.ref.toDouble) + case RefKind.TShort => Some(ref.ref.toDouble) + case RefKind.TInt => Some(ebaReader.intTable.readElem(ref.ref).toDouble) + case RefKind.TLng => Some(ebaReader.longTable.readElem(ref.ref).toDouble) + case RefKind.TFlt => Some(ebaReader.floatTable.readElem(ref.ref).toDouble) + case RefKind.TDbl => Some(ebaReader.doubleTable.readElem(ref.ref)) + case _ => None + } + } + + def asBigDec: Option[BigDecimal] = { + ref.kind match { + case RefKind.TByte => Some(BigDecimal.apply(ref.ref.toLong)) + case RefKind.TShort => Some(BigDecimal.apply(ref.ref.toLong)) + case RefKind.TInt => + Some(BigDecimal.apply(ebaReader.intTable.readElem(ref.ref).toLong)) + case RefKind.TLng => + Some(BigDecimal.apply(ebaReader.longTable.readElem(ref.ref))) + case RefKind.TFlt => + Some(BigDecimal.apply(ebaReader.floatTable.readElem(ref.ref).toDouble)) + case RefKind.TDbl => + Some(BigDecimal.apply(ebaReader.doubleTable.readElem(ref.ref))) + case RefKind.TBigDec => Some(ebaReader.bigDecTable.readElem(ref.ref)) + case _ => None + } + } + + def asString: Option[String] = { + if (ref.kind == TStr) Some(ebaReader.strTable.readElem(ref.ref)) + else None + } +} \ No newline at end of file diff --git a/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/TopCursor.scala b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/TopCursor.scala new file mode 100644 index 0000000..529f18b --- /dev/null +++ b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/cursor/TopCursor.scala @@ -0,0 +1,27 @@ +package izumi.sick.eba.cursor + +import izumi.sick.eba.reader.IncrementalEBAReader +import izumi.sick.model.Ref +import izumi.sick.model.RefKind.TObj + +class TopCursor(val ref: Ref, val ebaReader: IncrementalEBAReader) extends SickCursor { + def query(request: String): ObjectCursor = { + new ObjectCursor(ebaReader.queryRef(ref, request)._1, ebaReader) + } + + def getReferences: Map[String, Ref] = { + if (ref.kind == TObj) ebaReader.objTable.readElem(ref.ref).iterator.toMap + else throw new RuntimeException(s"Can not get references for kind ${ref.kind}") + } + + def getValues: Map[String, ObjectCursor] = { + getReferences.view.mapValues(ref => + new ObjectCursor(ref, ebaReader) + ).toMap + } + + def readKey(index: Int): ObjectCursor = { + if (ref.kind == TObj) new ObjectCursor(ebaReader.objTable.readElem(ref.ref).readKey(index)._2, ebaReader) + else throw new RuntimeException(s"Can not read key for kind ${ref.kind}") + } +} diff --git a/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/reader/IncrementalEBAReader.scala b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/reader/IncrementalEBAReader.scala index 71f6fc8..5dafbfe 100644 --- a/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/reader/IncrementalEBAReader.scala +++ b/json-sick-scala/json-sick/src/main/scala/izumi/sick/eba/reader/IncrementalEBAReader.scala @@ -1,5 +1,6 @@ package izumi.sick.eba.reader +import izumi.sick.eba.cursor.TopCursor import izumi.sick.eba.reader.incremental.{IncrementalJValue, IncrementalTableFixed, IncrementalTableVar, OneObjTable} import izumi.sick.eba.writer.codecs.EBACodecs.{DebugTableName, IntCodec, RefCodec, ShortCodec} import izumi.sick.eba.{EBAStructure, SICKSettings} @@ -115,12 +116,23 @@ class IncrementalEBAReader( val roots: collection.Map[String, Ref], ) extends AutoCloseable { + def getCursor(ref: Ref): TopCursor = { + new TopCursor(ref, this) + } + + def getCursor(rootId: String): TopCursor = { + getRoot(rootId) match { + case Some(ref) => new TopCursor(ref, this) + case None => throw new NoSuchElementException(s"Root with id $rootId was not found") + } + } + def getRoot(id: String): Option[Ref] = { roots.get(id) } def query(ref: Ref, path: String): IncrementalJValue = { - query(ref, path.split(".").toList) + query(ref, path.split('.').toList) } def query(ref: Ref, parts: List[String]): IncrementalJValue = { @@ -137,7 +149,7 @@ class IncrementalEBAReader( } def query(jObj: IncrementalJValue.JObj, path: String): IncrementalJValue = { - query(jObj, path.split(".").toList) + query(jObj, path.split('.').toList) } def query(jObj: IncrementalJValue.JObj, parts: List[String]): IncrementalJValue = { @@ -151,7 +163,7 @@ class IncrementalEBAReader( } def queryRef(ref: Ref, path: String): (Ref, Seq[String]) = { - val parts = path.split(".").toList + val parts = path.split('.').toList (queryRef(ref, parts), parts) } diff --git a/json-sick-scala/json-sick/src/test/scala/io/izumi/sick/SickCursorTest.scala b/json-sick-scala/json-sick/src/test/scala/io/izumi/sick/SickCursorTest.scala new file mode 100644 index 0000000..3abfaf9 --- /dev/null +++ b/json-sick-scala/json-sick/src/test/scala/io/izumi/sick/SickCursorTest.scala @@ -0,0 +1,187 @@ +package io.izumi.sick + +import io.circe.jawn.parse +import izumi.sick.SICK +import izumi.sick.eba.reader.IncrementalEBAReader +import izumi.sick.eba.writer.EBAWriter +import izumi.sick.model.RefKind.TObj +import izumi.sick.model.{SICKWriterParameters, TableWriteStrategy} +import org.scalatest.wordspec.AnyWordSpec + +class SickCursorTest extends AnyWordSpec { + + "cursor simple test" in { + val jsonString = """{"data": {"name": "Alice", "age": 30, "city": "NYC"}}""" + val json = parse(jsonString).toTry.get + + val eba = SICK.packJson( + json = json, + name = "user.json", + dedup = true, + dedupPrimitives = true, + avoidBigDecimals = false + ) + + val (bytes, _) = EBAWriter.writeBytes( + eba.index, + SICKWriterParameters(TableWriteStrategy.SinglePassInMemory) + ) + val bytesArray = bytes.toArrayUnsafe() + val reader = + IncrementalEBAReader.openBytes(bytesArray, eagerOffsets = false) + val cursor = reader.getCursor(eba.root) + + assert(cursor.downField("data").downField("name").asString.contains("Alice")) + assert(cursor.downField("data").downField("age").asByte.contains(30)) + assert(cursor.downField("data").downField("city").asString.contains("NYC")) + assert(cursor.downField("data").downField("age").asString.isEmpty) + } + + "cursor support various kinds" in { + val jsonString = + """{ + | "null": null, + | "bit": true, + | "byte": 42, + | "short": 30000, + | "int": 2000000000, + | "long": 9000000000000000000, + | "bigint": 123456789012345678901234567890, + | "float": 1.5, + | "double": 42.4242424242, + | "bigdec": 123.45678901234567890, + | "string": "test", + | "object": { + | "field": "test field" + | } + |}""".stripMargin + val json = parse(jsonString).toTry.get + + val eba = SICK.packJson( + json = json, + name = "user.json", + dedup = true, + dedupPrimitives = true, + avoidBigDecimals = false + ) + + val (bytes, _) = EBAWriter.writeBytes( + eba.index, + SICKWriterParameters(TableWriteStrategy.SinglePassInMemory) + ) + val bytesArray = bytes.toArrayUnsafe() + val reader = + IncrementalEBAReader.openBytes(bytesArray, eagerOffsets = false) + val cursor = reader.getCursor(eba.root) + cursor.downField("null").asNul + assert(cursor.downField("bit").asBool.contains(true)) + assert(cursor.downField("byte").asByte.contains(42)) + assert(cursor.downField("short").asShort.contains(30000)) + assert(cursor.downField("int").asInt.contains(2000000000)) + assert(cursor.downField("int").asLong.contains(2000000000L)) + assert(cursor.downField("long").asLong.contains(9000000000000000000L)) + assert( + cursor.downField("bigint").asBigInt.contains(BigInt.apply( + "123456789012345678901234567890" + )) + ) + assert(cursor.downField("float").asFloat.contains(1.5.toFloat)) + assert(cursor.downField("float").asDouble.contains(1.5)) + assert(cursor.downField("double").asDouble.contains(42.4242424242d)) + assert( + cursor.downField("bigdec").asBigDec.contains(BigDecimal.apply( + "123.45678901234567890" + )) + ) + assert(cursor.downField("string").asString.contains("test")) + assert(cursor.downField("object").downField("field").asString.contains("test field")) + } + + "cursor support arrays" in { + val jsonString = + """{ + | "array" : [ + | "one", + | "two", + | "three" + | ], + | "objects" : [ + | {"name": "Alice", "age": 30, "city": "NYC"}, + | {"name": "Bob", "age": 42, "city": "Chicago"} + | ] + |}""".stripMargin + val json = parse(jsonString).toTry.get + + val eba = SICK.packJson( + json = json, + name = "user.json", + dedup = true, + dedupPrimitives = true, + avoidBigDecimals = false + ) + + val (bytes, _) = EBAWriter.writeBytes( + eba.index, + SICKWriterParameters(TableWriteStrategy.SinglePassInMemory) + ) + val bytesArray = bytes.toArrayUnsafe() + val reader = + IncrementalEBAReader.openBytes(bytesArray, eagerOffsets = false) + val cursor = reader.getCursor(eba.root) + assert(cursor.downField("array").downArray.downIndex(0).asString.contains("one")) + assert( + cursor + .downField("objects") + .downArray + .downIndex(1) + .downField("name") + .asString.contains("Bob") + ) + + val arrayCursor = cursor.downField("array").downArray + + assert(arrayCursor.value.asString.contains("one")) + assert(arrayCursor.right.value.asString.contains("two")) + assert(arrayCursor.right.right.value.asString.contains("three")) + assert(arrayCursor.right.left.value.asString.contains("one")) + } + + "cursor support queries" in { + val jsonString = + """{ + | "data": { + | "person": {"name": "Alice", "age": 30, "city": "NYC"}, + | "numbers" : [ + | "one", + | "two", + | "three" + | ] + | } + |}""".stripMargin + val json = parse(jsonString).toTry.get + + val eba = SICK.packJson( + json = json, + name = "user.json", + dedup = true, + dedupPrimitives = true, + avoidBigDecimals = false + ) + + val (bytes, _) = EBAWriter.writeBytes( + eba.index, + SICKWriterParameters(TableWriteStrategy.SinglePassInMemory) + ) + val bytesArray = bytes.toArrayUnsafe() + val reader = + IncrementalEBAReader.openBytes(bytesArray, eagerOffsets = false) + val cursor = reader.getCursor(eba.root) + assert(cursor.query("data.person").ref.kind == TObj) + assert(cursor.query("data.person.name").asString.contains("Alice")) + + assert(cursor.query("data.person").getReferences.size == 3) + assert(cursor.query("data.person").getValues.get("name").exists(_.asString.contains("Alice"))) + + assert(cursor.query("data.person").readKey(1).asInt.contains(30)) + } +} diff --git a/json-sick-scala/npm-template/README.md b/json-sick-scala/npm-template/README.md index e31ca81..41351d8 100644 --- a/json-sick-scala/npm-template/README.md +++ b/json-sick-scala/npm-template/README.md @@ -67,6 +67,10 @@ Accepts dictionary where keys are root names and values are strings that parse i Accepts dictionary where keys are root names and values are Uint8Arrays containing valid UTF-8 text that parse into JSON, returns a SICK-encoded binary Uint8Array. +### `sickCursorFromUint8Array(uint8Array: Uint8Array, rootId: string): TopCursor` + +Accepts an instance of `Uint8Array` and the rootId, returns a cursor to navigate through the structure. + ## License BSD-2-Clause diff --git a/json-sick-scala/npm-template/test.js b/json-sick-scala/npm-template/test.js index 7863f46..9ad3402 100644 --- a/json-sick-scala/npm-template/test.js +++ b/json-sick-scala/npm-template/test.js @@ -5,13 +5,14 @@ import { encodeObjToSickUint8Array, encodeObjsToSickUint8Array, encodeJSONStringsToSickUint8Array, - encodeJSONBytesToSickUint8Array + encodeJSONBytesToSickUint8Array, + sickCursorFromUint8Array } from "./json-sick-2.13-fullOpt.js"; test("Encode/Decode obj test", t => { - const encoded = encodeObjToSickUint8Array("data", { a: 2, b: { c: 3 } }); + const encoded = encodeObjToSickUint8Array("data", { a: 2, b: { c: 3 , d: true} }); const decoded = decodeSickUint8Array(encoded); - t.deepEqual(decoded, { data: { a: 2, b: { c: 3 } } }); + t.deepEqual(decoded, { data: { a: 2, b: { c: 3 , d: true} } }); }) test("Encode/Decode JSON strings test", t => { @@ -27,4 +28,39 @@ test("Encode/Decode multiple objects test", t => { }); const decoded = decodeSickUint8Array(multiEncoded); t.deepEqual(decoded, { data: { a: 2 }, data1: { b: 3 } }); +}) + +test("Sick Cursors test", t => { + const encoded = encodeObjToSickUint8Array("data", + { + nul: null, + bit: true, + byte: 42, + int: 30000, + bigint: 123456789012345678901234567890, + double: 3.14, + bigdec: 123.45678901234567890, + string: "test", + object: { + life: 42, + person: { name: "Alice", age: 30, city: "NYC" } + }, + arr: ["a", "b", "c"] + }); + const cursor = sickCursorFromUint8Array(encoded, "data"); + + t.is(cursor.downField("nul").asNul, null); + t.true(cursor.downField("bit").asBool); + t.is(cursor.downField("byte").asByte, 42); + t.is(cursor.downField("int").asInt, 30000); + t.is(cursor.downField("bigint").asBigInt, 123456789012345680000000000000n); + t.is(cursor.downField("double").asDouble, 3.14); + t.is(cursor.downField("string").asString, "test"); + t.is(cursor.downField("object").downField("life").asInt, 42); + + t.is(cursor.query("object.person.name").asString, "Alice"); + t.is(cursor.downField("object").getValues.get("person").downField("name").asString, "Alice"); + + t.is(cursor.downField("arr").downArray.right.value.asString, "b"); + t.is(cursor.downField("arr").downArray.downIndex(2).asString, "c"); }) \ No newline at end of file