diff --git a/src/main/scala/co.blocke.scalajack/json/JsonParser.scala b/src/main/scala/co.blocke.scalajack/json/JsonParser.scala index b17b9834..890315ac 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonParser.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonParser.scala @@ -5,153 +5,220 @@ 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 - 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 - i += 4 - Right(true) - case 'f' if i+4 - 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] = +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 do 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, ie no escaped/special chars) + def expectLabel: Either[ParseError, String] = + jsChars(i) match + case '"' => + i += 1 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 + while i < max && jsChars(i) != '"' do 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, p: JsonParser): 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, p: JsonParser): 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, p: JsonParser): 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 expectString(cfg: JsonConfig, p: JsonParser): Either[ParseError, String] = + nullCheck match + case true if cfg.forbidNullsInInput => Left(ParseError(s"Forbidden 'null' value received at position [$i]")) + case true => Right(null.asInstanceOf[String]) + case false => + jsChars(i) match + case '"' => + i += 1 + val mark = i // save position in case we need complex string parse + var captured: Option[String] = None + while i < max && jsChars(i) != '"' do + jsChars(i) match + case '\\' => // oops! special char found--do slow string parse + i = mark + captured = Some(_expectString) + case _ => i += 1 + i += 1 + Right(captured.getOrElse(js.substring(mark, i - 1))) + case x => Left(ParseError(s"Unexpected character '$x' where beginning of label expected at position [$i]")) + + def expectOption[T](cfg: JsonConfig, expectElement: (JsonConfig, JsonParser) => Either[ParseError, T]): Either[ParseError, Option[T]] = + nullCheck match + case false => expectElement(cfg, this).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, JsonParser) => 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, this) + _ = acc.addOne(el) + _ <- expectComma + } yield el) match + case Left(_) if jsChars(i) == ']' => 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 + 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, JsonParser) => 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, this) + _ = fields.put(fieldLabel, fieldValue) + _ <- expectComma + } yield fieldValue) match + case Left(_) if jsChars(i) == '}' => 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 \ No newline at end of file + done = Some(Right(instantiator(fields.toMap))) // instantiate the class here!!! + case Left(e) => + done = Some(Left(e)) + case Right(_) => + done.get + + // Slower String parsing that handles special escaped chars + def _expectString = + val builder = new java.lang.StringBuilder() + while i < max && jsChars(i) != '"' do + if jsChars(i) == '\\' then { + jsChars(i + 1) match { + case '"' => + builder.append('\"') + i += 2 + + case '\\' => + builder.append('\\') + i += 2 + + case 'b' => + builder.append('\b') + i += 2 + + case 'f' => + builder.append('\f') + i += 2 + + case 'n' => + builder.append('\n') + i += 2 + + case 'r' => + builder.append('\r') + i += 2 + + case 't' => + builder.append('\t') + i += 2 + + case 'u' => + val hexEncoded = js.substring(i + 2, i + 6) + val unicodeChar = Integer.parseInt(hexEncoded, 16).toChar + builder.append(unicodeChar.toString) + i += 6 + + case c => + builder.append(c) + i += 2 + } + } else { + builder.append(jsChars(i)) + i += 1 + } + builder.toString diff --git a/src/main/scala/co.blocke.scalajack/json/JsonReader.scala b/src/main/scala/co.blocke.scalajack/json/JsonReader.scala index b3eb8e95..b90f2bde 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonReader.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonReader.scala @@ -3,112 +3,104 @@ 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.{Clazzes, RTypeRef, TypedName} import co.blocke.scala_reflection.rtypes.* import co.blocke.scala_reflection.Liftables.TypedNameToExpr import scala.quoted.* +import scala.collection.Factory 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) + def expectInt(): Either[ParseError, Long] = Right(1L) +import scala.collection.immutable.* +case class Blah(msg: String, stuff: Array[List[String]]) //, age: Int, isOk: Boolean) object JsonReader: - def classInstantiator[T:Type]( ref: ClassRef[T] )(using Quotes): Expr[Map[String, ?] => T] = + 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)), ""), - ref.fields.map{f => - f.fieldRef.refType match - case '[t] => - '{ fieldMap(${Expr(f.name)}).asInstanceOf[t] }.asTerm - } - ) - tree.asExpr.asExprOf[T] - } + '{ (fieldMap: Map[String, ?]) => + ${ + val tree = Apply( + Select.unique(New(TypeIdent(sym)), ""), + 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 - } + def refFn[T: Type](ref: RTypeRef[T])(using Quotes): Expr[(JsonConfig, JsonParser) => Either[ParseError, T]] = + import Clazzes.* + import quotes.reflect.* + ref match + case t: PrimitiveRef[?] if t.name == BOOLEAN_CLASS => + '{ (j: JsonConfig, p: JsonParser) => p.expectBoolean(j, p).map(_.asInstanceOf[T]) } + case t: PrimitiveRef[?] if t.name == BYTE_CLASS => + '{ (j: JsonConfig, p: JsonParser) => p.expectLong(j, p).map(_.toByte.asInstanceOf[T]) } + case t: PrimitiveRef[?] if t.name == CHAR_CLASS => + '{ (j: JsonConfig, p: JsonParser) => + p.expectString(j, p) + .flatMap(s => + s.toArray.headOption match + case Some(c) => Right(c.asInstanceOf[T]) + case None => Left(ParseError(s"Cannot convert value '$s' into a Char.")) + ) + } + case t: PrimitiveRef[?] if t.name == DOUBLE_CLASS => + '{ (j: JsonConfig, p: JsonParser) => p.expectDouble(j, p).map(_.asInstanceOf[T]) } + case t: PrimitiveRef[?] if t.name == FLOAT_CLASS => + '{ (j: JsonConfig, p: JsonParser) => p.expectDouble(j, p).map(_.toFloat.asInstanceOf[T]) } + case t: PrimitiveRef[?] if t.name == INT_CLASS => + '{ (j: JsonConfig, p: JsonParser) => p.expectLong(j, p).map(_.toInt.asInstanceOf[T]) } + case t: PrimitiveRef[?] if t.name == LONG_CLASS => + '{ (j: JsonConfig, p: JsonParser) => p.expectLong(j, p).map(_.asInstanceOf[T]) } + case t: PrimitiveRef[?] if t.name == SHORT_CLASS => + '{ (j: JsonConfig, p: JsonParser) => p.expectLong(j, p).map(_.toShort.asInstanceOf[T]) } + case t: PrimitiveRef[T] if t.name == STRING_CLASS => + '{ (j: JsonConfig, p: JsonParser) => p.expectString(j, p).map(_.asInstanceOf[T]) } + + case t: SeqRef[T] => + t.refType match + case '[s] => + t.elementRef.refType match + case '[e] => + val subFn = refFn[e](t.elementRef.asInstanceOf[RTypeRef[e]]).asInstanceOf[Expr[(JsonConfig, JsonParser) => Either[ParseError, e]]] + '{ (j: JsonConfig, p: JsonParser) => + p.expectList[e](j, $subFn).map(_.to(${ Expr.summon[Factory[e, T]].get })) // Convert List to whatever the target type should be + } + case t: ArrayRef[T] => + t.refType match + case '[s] => + t.elementRef.refType match + case '[e] => + val subFn = refFn[e](t.elementRef.asInstanceOf[RTypeRef[e]]).asInstanceOf[Expr[(JsonConfig, JsonParser) => Either[ParseError, e]]] + '{ (j: JsonConfig, p: JsonParser) => + p.expectList[e](j, $subFn).map(_.to(${ Expr.summon[Factory[e, T]].get })) // Convert List to whatever the target type should be + } + + def classParseMap[T: Type](ref: ClassRef[T])(using Quotes): Expr[JsonParser => Map[String, (JsonConfig, JsonParser) => Either[ParseError, ?]]] = + import Clazzes.* + '{ (parser: JsonParser) => + val daList = ${ + val fieldList = ref.fields.map(f => + f.fieldRef.refType match + case '[m] => + val fn = refFn[m](f.fieldRef.asInstanceOf[RTypeRef[m]]) + '{ + ${ Expr(f.name) } -> $fn + } + ) + Expr.ofList(fieldList) + } + daList.toMap + } diff --git a/src/main/scala/co.blocke.scalajack/run/Play.scala b/src/main/scala/co.blocke.scalajack/run/Play.scala index 85e861e9..4dfc6277 100644 --- a/src/main/scala/co.blocke.scalajack/run/Play.scala +++ b/src/main/scala/co.blocke.scalajack/run/Play.scala @@ -21,15 +21,16 @@ object RunMe extends App: .JsonConfig() try - println("RESULT: " + ScalaJack.read[json.Blah]("""{"msg":"Greg","isOk":true,"age":57}""")) + println("RESULT: " + ScalaJack.read[json.Blah]("""{"msg":"Greg\nZoller", "stuff": [["a","b","c"],["x","y","z"]] }""")) + // 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)) + // 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) }