Created
September 25, 2024 06:10
-
-
Save mjsmith707/81908f7523b380a00697f0dd81b75ca8 to your computer and use it in GitHub Desktop.
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
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