Skip to content

Instantly share code, notes, and snippets.

@mjsmith707
Created September 25, 2024 06:10
Show Gist options
  • Save mjsmith707/81908f7523b380a00697f0dd81b75ca8 to your computer and use it in GitHub Desktop.
Save mjsmith707/81908f7523b380a00697f0dd81b75ca8 to your computer and use it in GitHub Desktop.
package org.msmith;
import com.github.plokhotnyuk.jsoniter_scala.core.JsonWriter;
import java.util.Arrays;
public class CharArrayJsoniterWriter {
static void encode(byte[] str, JsonWriter out) {
readField(str, 0, out);
}
private static int readField(byte[] str, int pos, JsonWriter out) {
byte c = str[pos];
if (c == '"') {
return readString(str, pos, false, out);
} else if (c == 't' || c == 'f') {
return readBoolean(str, pos, out);
} else if (c >= '0' && c <= '9' || c == '-') {
return readNumeric(str, pos, out);
} else if (c == '{') {
return readObject(str, pos, out);
} else if (c == '[') {
return readArray(str, pos, out);
} else {
throw new RuntimeException("Failed to read field at pos=%d, char=%s".formatted(pos, str[pos]));
}
}
private static int readString(byte[] str, int pos, boolean isKey, JsonWriter out) {
// find opposite enclosing string
boolean escaped = false;
for (int i = pos+1; i<str.length; i++) {
if (escaped) {
escaped = false;
continue;
}
if (str[i] == '\\') {
escaped = true;
continue;
}
if (str[i] == '"') {
if (isKey) {
// read a string then write it out since there's no writeRawKey
String s = new String(str, pos+1, i-(pos+1));
out.writeKey(s);
} else {
byte[] slice = Arrays.copyOfRange(str, pos, i+1);
out.writeRawVal(slice);
}
return i+1;
}
}
throw new RuntimeException("Failed to find terminating quote in json string at position %d".formatted(pos));
}
private static int readObject(byte[] str, int pos, JsonWriter out) {
out.writeObjectStart();
// read k/v pairs
while(pos < str.length) {
// read key
pos = readString(str, pos+1, true, out);
if (pos < str.length && str[pos] == ':') {
// read value
pos = readField(str, pos+1, out);
} else {
throw new RuntimeException("Failed to find key : in json object at position %d".formatted(pos));
}
if (str[pos] == '}') {
out.writeObjectEnd();
return pos+1;
} else if (str[pos] != ',') {
throw new RuntimeException("Failed to find object separator , in json object at position %d".formatted(pos));
}
}
throw new RuntimeException("Failed to find terminating brace in json object at position %d".formatted(pos));
}
private static int readNumeric(byte[] str, int pos, JsonWriter out) {
for (int i=pos; i<str.length; i++) {
if (str[i] == ',' || str[i] == '}' || str[i] == ']') {
byte[] slice = Arrays.copyOfRange(str, pos, i);
out.writeRawVal(slice);
return i;
}
}
throw new RuntimeException("Failed to read numeric field at position %d".formatted(pos));
}
private static int readBoolean(byte[] str, int pos, JsonWriter out) {
if (str[pos] == 't') {
out.writeVal(true);
return pos+4;
} else if (str[pos] == 'f') {
out.writeVal(false);
return pos+5;
} else {
throw new RuntimeException("Failed to read boolean at pos %d".formatted(pos));
}
}
private static int readArray(byte[] str, int pos, JsonWriter out) {
out.writeArrayStart();
// read values
while(pos < str.length) {
// read field
pos = readField(str, pos+1, out);
if (str[pos] == ']') {
out.writeArrayEnd();
return pos+1;
} else if (str[pos] != ',') {
throw new RuntimeException("Failed to find array separator , in json object at position %d".formatted(pos));
}
}
throw new RuntimeException("Failed to find terminating brace in json object at position %d".formatted(pos));
}
}
package org.msmith
import com.github.plokhotnyuk.jsoniter_scala.core.{JsonReader, JsonValueCodec, JsonWriter, ReaderConfig, readFromArray, writeToString}
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
/**
* This is a wrapper class around some type T and it's JSON representation in bytes
* The motive behind this is to only deserialize as type T (which may be expensive) if necessary
* while otherwise using the byte representation for serialization in common cases where we're just
* receiving a GET request -> db -> http response (i.e. shuffling bytes from the db to http)
* @param jsonBytes JSON byte representation of T
* @param underlyingT Class representation of T
* @param c JsonValueCodec for T
* @tparam T The wrapped object type
*/
class LazyJson[T] private (
private val jsonBytes: Option[Array[Byte]],
private val underlyingT: Option[T],
private val readerConfig: Option[ReaderConfig]
)(implicit
private val c: JsonValueCodec[T]) {
require(jsonBytes.isDefined || underlyingT.isDefined, "LazyJson must have either bytes or the reified class")
lazy val underlying: T = {
underlyingT.isDefined match {
case true =>
underlyingT.get
case false =>
readerConfig.fold(readFromArray[T](jsonBytes.get)) { rc =>
readFromArray[T](jsonBytes.get, rc)
}
}
}
}
object LazyJson {
/**
* This should only be used by database layer decoders on json/jsonb columns
* @param jsonBytes
* @param c
* @tparam T
* @return
*/
def apply[T](
jsonBytes: Array[Byte]
)(implicit
c: JsonValueCodec[T],
readerConfig: ReaderConfig
): LazyJson[T] = {
new LazyJson[T](Some(jsonBytes), None, Some(readerConfig))
}
/**
* General constructor for T
* @param t
* @param c
* @tparam T
* @return
*/
def apply[T](
t: T
)(implicit
c: JsonValueCodec[T]
): LazyJson[T] = {
new LazyJson[T](None, Some(t), None)
}
/**
* Generates a wrapper codec for this type T
* The codec will always decode as T internally (for validation purposes)
* while always serializing the jsonBytes when available (for performance)
* @param c
* @tparam T
* @return
*/
def codec[T](implicit c: JsonValueCodec[T]): JsonValueCodec[LazyJson[T]] = {
new JsonValueCodec[LazyJson[T]] {
override def decodeValue(in: JsonReader, default: LazyJson[T]): LazyJson[T] = {
// always decode to the concrete type as it's validated
val t = c.decodeValue(in, c.nullValue)
LazyJson(t)
}
override def encodeValue(x: LazyJson[T], out: JsonWriter): Unit = {
// encode the json bytes if present, otherwise encode the class itself
x.jsonBytes match {
case Some(bytes) =>
CharArrayJsoniterWriter.encode(bytes, out)
case None =>
c.encodeValue(x.underlyingT.get, out)
}
}
override def nullValue: LazyJson[T] = null
}
}
}
case class MyTestClass(str: String, i: Int, b: Boolean, obj: Map[String, String], arr: List[String])
object MyTestClass {
implicit val codec: JsonValueCodec[MyTestClass] = {
JsonCodecMaker.make
}
implicit val lazyCodec: JsonValueCodec[LazyJson[MyTestClass]] = {
LazyJson.codec
}
}
class TestSpec extends AnyFlatSpec with Matchers {
behavior of "serialization test"
implicit val readerConfig: ReaderConfig = ReaderConfig
val testClass = MyTestClass("hello", 1234, true, Map("a" -> "b"), List("a", "b", "c"))
val asBytes = writeToString(testClass).getBytes
it should "skip deserialization and write the bytes directly to the JsonWriter" in {
val lazyClass = LazyJson[MyTestClass](asBytes)
val result = writeToString(lazyClass)
println(result)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment