Skip to content

Commit

Permalink
Rudimentary read working
Browse files Browse the repository at this point in the history
  • Loading branch information
Greg Zoller committed Oct 23, 2023
1 parent d11003e commit 766f55a
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 1 deletion.
19 changes: 19 additions & 0 deletions src/main/scala/co.blocke.scalajack/ScalaJack.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package co.blocke.scalajack

import co.blocke.scala_reflection.TypedName
import co.blocke.scala_reflection.reflect.ReflectOnType
import co.blocke.scala_reflection.reflect.rtypeRefs.ClassRef
import scala.quoted.*
import quoted.Quotes
import json.*
Expand All @@ -19,3 +20,21 @@ object ScalaJack:
val sb = new StringBuilder()
$fn($t, sb, $cfg).toString
}

inline def read[T](js: String)(using cfg: JsonConfig = JsonConfig()): T = ${ readImpl[T]('js, 'cfg) }

def readImpl[T: Type](js: Expr[String], cfg: Expr[JsonConfig])(using q: Quotes): Expr[T] =
import quotes.reflect.*

val classRef = ReflectOnType[T](quotes)(TypeRepr.of[T], true)(using scala.collection.mutable.Map.empty[TypedName, Boolean]).asInstanceOf[ClassRef[T]]
val parseTable = JsonReader.classParseMap[T](classRef)
val instantiator = JsonReader.classInstantiator[T](classRef)
'{ // run-time
val parser = JsonParser($js)
val classFieldMap = $parseTable(parser) // Map[String, JsonConfig => Either[ParseError, ?]]
parser.expectClass[T]($cfg, classFieldMap, $instantiator) match
case Right(v) => v
case Left(t) =>
println("BOOM: " + t.msg)
throw t
}
1 change: 1 addition & 0 deletions src/main/scala/co.blocke.scalajack/json/JsonConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package json

case class JsonConfig(
noneAsNull: Boolean = false,
forbidNullsInInput: Boolean = false,
tryFailureHandling: TryOption = TryOption.NO_WRITE,
// --------------------------
typeHintLabel: String = "_hint",
Expand Down
157 changes: 157 additions & 0 deletions src/main/scala/co.blocke.scalajack/json/JsonParser.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package co.blocke.scalajack
package json

import scala.util.*

case class ParseError(msg: String) extends Throwable

case class JsonParser( js: String ):

private val jsChars: Array[Char] = js.toCharArray
private var i = 0
private val max: Int = jsChars.length

// Housekeeping
//------------------------------
@inline def nullCheck: Boolean =
val res = i+3<max && jsChars(i)=='n' && jsChars(i+1)=='u' && jsChars(i+2)=='l' && jsChars(i+3)=='l'
if res then i += 4
res
@inline def eatWhitespace: Either[ParseError,Unit] =
while (i < max && jsChars(i).isWhitespace) i += 1
Right(())
@inline def expectComma: Either[ParseError,Unit] = // Note: this consumes whitespace before/after the ','
for {
_ <- eatWhitespace
r <- if jsChars(i) == ',' then
i += 1
eatWhitespace
Right(())
else
Left(ParseError(s"Expected comma at position [$i]"))
} yield r
@inline def expectColon: Either[ParseError,Unit] = // Note: this consumes whitespace before/after the ':'
for {
_ <- eatWhitespace
r <- if jsChars(i) == ':' then
i += 1
eatWhitespace
Right(())
else
Left(ParseError(s"Expected colon at position [$i]"))
} yield r

// JSON label (i.e. object key, which is always a simple string)
def expectLabel: Either[ParseError,String] =
jsChars(i) match
case '"' =>
i += 1
val mark = i
while (i < max && jsChars(i) != '"') i += 1
i += 1
Right(js.substring(mark, i-1))
case x => Left(ParseError(s"Unexpected character '$x' where beginning of label expected at position [$i]"))


// Data Types
//------------------------------
def expectBoolean(cfg: JsonConfig): Either[ParseError,Boolean] =
jsChars(i) match
case 't' if i+3<max && jsChars(i+1)=='r' && jsChars(i+2)=='u' && jsChars(i+3)=='e' =>
i += 4
Right(true)
case 'f' if i+4<max && jsChars(i+1)=='a' && jsChars(i+2)=='l' && jsChars(i+3)=='s' && jsChars(i+3)=='e' =>
i += 5
Right(false)
case x => Left(ParseError(s"Unexpected character '$x' where beginning of boolean value expected at position [$i]"))

def expectLong(cfg: JsonConfig): Either[ParseError,Long] =
val mark = i
var done = false
while !done do
jsChars(i) match
case c if (c >= '0' && c <= '9') || c == '-' => i += 1
case _ => done = true
Try( js.substring(mark,i).toLong ) match
case Success(g) => Right(g)
case Failure(f) =>
val msg = if mark == i then
s"""Int/Long expected but couldn't parse from "${jsChars(i)}" at position [$i]"""
else
s"""Int/Long expected but couldn't parse from "${js.substring(mark,i)}" at position [$i]"""
i = mark
Left(ParseError(msg))

def expectDouble(cfg: JsonConfig): Either[ParseError,Double] =
val mark = i
var done = false
while !done do
jsChars(i) match
case c if (c >= '0' && c <= '9') || c == '-' || c == '.' || c == 'e' || c == 'E' || c == '+' => i += 1
case _ => done = true
Try( js.substring(mark,i-1).toDouble ) match
case Success(g) => Right(g)
case Failure(_) =>
val msg = if mark == i then
s"Float/Double expected but couldn't parse from \"${jsChars(i)}\" at position [$i]"
else
s"Float/Double expected but couldn't parse from \"${js.substring(mark,i-1)}\" at position [$i]"
i = mark
Left(ParseError(msg))

def expectOption[T](cfg: JsonConfig, expectElement: JsonConfig=>Either[ParseError,T]): Either[ParseError, Option[T]] =
nullCheck match
case false => expectElement(cfg).map(t => Some(t))
case true if cfg.noneAsNull => Right(None)
case true if cfg.forbidNullsInInput => Left(ParseError(s"Forbidden 'null' value received at position [$i]"))
case true => Right(Some(null.asInstanceOf[T]))

def expectList[T]( cfg: JsonConfig, expectElement: JsonConfig=>Either[ParseError,T]): Either[ParseError,List[T]] =
if jsChars(i) != '[' then Left(ParseError(s"Beginning of list expected at position [$i]"))
else
i += 1
eatWhitespace
val acc = scala.collection.mutable.ListBuffer.empty[T]
var done: Option[Either[ParseError,List[T]]] = None
while done.isEmpty do
(for {
el <- expectElement(cfg)
_ = acc.addOne(el)
_ <- expectComma
} yield el) match
case Left(_) if jsChars(i) == ']' =>
i += 1
eatWhitespace
done = Some(Right(acc.toList))
case Left(e) =>
done = Some(Left(e))
case Right(_) =>
done.get

def expectClass[T](
cfg: JsonConfig,
fieldMap: Map[String, (JsonConfig) => Either[ParseError, ?]],
instantiator: Map[String, ?] => T
): Either[ParseError,T] =
if jsChars(i) != '{' then Left(ParseError(s"Beginning of object expected at position [$i]"))
else
i += 1
eatWhitespace
var done: Option[Either[ParseError,T]] = None
val fields = scala.collection.mutable.HashMap.empty[String,Any]
while done.isEmpty do
(for {
fieldLabel <- expectLabel
_ <- expectColon
fieldValue <- fieldMap(fieldLabel)(cfg)
_ = fields.put(fieldLabel, fieldValue)
_ <- expectComma
} yield fieldValue) match
case Left(_) if jsChars(i) == '}' =>
i += 1
eatWhitespace
done = Some(Right( instantiator(fields.toMap) )) // instantiate the class here!!!
case Left(e) =>
done = Some(Left(e))
case Right(_) =>
done.get
114 changes: 114 additions & 0 deletions src/main/scala/co.blocke.scalajack/json/JsonReader.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package co.blocke.scalajack
package json

import co.blocke.scala_reflection.reflect.rtypeRefs.*
import co.blocke.scala_reflection.reflect.*
import co.blocke.scala_reflection.{RTypeRef, TypedName, Clazzes}
import co.blocke.scala_reflection.rtypes.*
import co.blocke.scala_reflection.Liftables.TypedNameToExpr
import scala.quoted.*
import co.blocke.scala_reflection.RType
import scala.jdk.CollectionConverters.*
import java.util.concurrent.ConcurrentHashMap
import scala.util.{Failure, Success}


case class ParserRead():
def expectInt(): Either[ParseError,Long] = Right(1L)

case class Blah(msg: String, age: Int, isOk: Boolean)


object JsonReader:

def classInstantiator[T:Type]( ref: ClassRef[T] )(using Quotes): Expr[Map[String, ?] => T] =
import quotes.reflect.*
val sym = TypeRepr.of[T].classSymbol.get
'{
(fieldMap: Map[String, ?]) =>
${
val tree = Apply(Select.unique(New(TypeIdent(sym)), "<init>"),
ref.fields.map{f =>
f.fieldRef.refType match
case '[t] =>
'{ fieldMap(${Expr(f.name)}).asInstanceOf[t] }.asTerm
}
)
tree.asExpr.asExprOf[T]
}
}

def classParseMap[T:Type]( ref: ClassRef[T] )(using Quotes): Expr[JsonParser => Map[String, JsonConfig=>Either[ParseError, ?]]] =
'{
(parser: JsonParser) =>
val daList = ${
val fieldList = ref.fields.map(f => f.fieldRef match
case t: PrimitiveRef[?] if t.name == Clazzes.CHAR_CLASS =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>
for {
strVal <- parser.expectLabel
charVal = strVal.toArray.headOption match
case Some(c) => Right(c)
case None => ParseError(s"Cannot convert value '$strVal' into a Char.")
} yield charVal
}
}
case t: PrimitiveRef[?] if t.family == PrimFamily.Stringish =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>parser.expectLabel}
}
case t: PrimitiveRef[?] if t.name == Clazzes.INT_CLASS =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>
for {
longVal <- parser.expectLong(j)
intVal = longVal.toInt
} yield intVal
}
}
case t: PrimitiveRef[?] if t.name == Clazzes.SHORT_CLASS =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>
for {
longVal <- parser.expectLong(j)
intVal = longVal.toShort
} yield intVal
}
}
case t: PrimitiveRef[?] if t.name == Clazzes.BYTE_CLASS =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>
for {
longVal <- parser.expectLong(j)
intVal = longVal.toByte
} yield intVal
}
}
case t: PrimitiveRef[?] if t.family == PrimFamily.Longish =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>parser.expectLong(j)}
}
case t: PrimitiveRef[?] if t.name == Clazzes.FLOAT_CLASS =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>
for {
longVal <- parser.expectDouble(j)
intVal = longVal.toFloat
} yield intVal
}
}
case t: PrimitiveRef[?] if t.family == PrimFamily.Doublish =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>parser.expectDouble(j)}
}
case t: PrimitiveRef[?] if t.family == PrimFamily.Boolish =>
'{
${Expr(f.name)} -> {(j:JsonConfig)=>parser.expectBoolean(j)}
}
)
Expr.ofList(fieldList)
}
daList.asInstanceOf[List[(String, JsonConfig=>Either[ParseError, ?])]].toMap
}

2 changes: 1 addition & 1 deletion src/main/scala/co.blocke.scalajack/json/JsonWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ object JsonWriter:
import quotes.reflect.*

rtRef match
case rt: PrimitiveRef[?] if rt.isStringish =>
case rt: PrimitiveRef[?] if rt.family == PrimFamily.Stringish =>
'{ (a: T, sb: StringBuilder, cfg: JsonConfig) =>
sb.append('"')
sb.append(a.toString)
Expand Down
55 changes: 55 additions & 0 deletions src/main/scala/co.blocke.scalajack/run/Play.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,37 @@ import co.blocke.scala_reflection.*

object RunMe extends App:

val js = """[[123,-456],[394,-2]]"""

val parser = json.JsonParser(js)

/*
val f = () => parser.expectLong
val f2 = () => parser.expectList[Long](() => parser.expectLong)
val r = parser.expectList[List[Long]](f2)
println("R: " + r)
*/

given json.JsonConfig = json
.JsonConfig()

try
println("RESULT: " + ScalaJack.read[json.Blah]("""{"msg":"Greg","isOk":true,"age":57}"""))
catch {
case t: Throwable => println("BOOM: " + t.getMessage)
}

val t0 = System.currentTimeMillis()
for i <- (0 to 10000) do ScalaJack.read[json.Blah]("""{"msg":"Greg","isOk":true,"age":57}""")
val t1 = System.currentTimeMillis()
println("TIME: " + (t1 - t0))

// inline def read[T](js: String)(using cfg: JsonConfig = JsonConfig()): T = ${ readImpl[T]('js, 'cfg) }

// def expectList[T]( expectElement: ()=>Either[ParseError,T]): Either[ParseError,List[T]] =

/*
val p = Person("Greg", 57, List(false, true, true), Colors.Blue, "Fred".asInstanceOf[BigName])
val d = Dog("Fido", 4, 2, Some(Dog("Mindy", 4, 0, None)))
Expand Down Expand Up @@ -39,3 +70,27 @@ object RunMe extends App:
val t1 = System.currentTimeMillis()
println("TIME: " + (t1 - t0))
*/

/*
case SomeRef =>
// Compile-Time
// * Quotes
// * Class details
// Build field parse map:
Map(
"name" -> ()=>expectString()
"lists" -> ()=>expectList(()=>expectString())
)
'{
// Runtime
// * json
// * cfg
}
*/

0 comments on commit 766f55a

Please sign in to comment.