-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Greg Zoller
committed
Oct 23, 2023
1 parent
d11003e
commit 766f55a
Showing
6 changed files
with
347 additions
and
1 deletion.
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
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
157 changes: 157 additions & 0 deletions
157
src/main/scala/co.blocke.scalajack/json/JsonParser.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,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
114
src/main/scala/co.blocke.scalajack/json/JsonReader.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,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 | ||
} | ||
|
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
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