From bb41444cb2988690cfb9bd94a011d862f4cdb95c Mon Sep 17 00:00:00 2001 From: Greg Zoller Date: Tue, 28 Nov 2023 00:33:32 -0600 Subject: [PATCH] JsonConfig initial working --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 1 + TODO.txt | 66 +--- .../src/main/scala/co.blocke/ScalaJack.scala | 4 +- .../scala/co.blocke.scalajack/ScalaJack.scala | 22 +- .../internal/TreeNode.scala | 16 - .../co.blocke.scalajack/json/JsonCodec.scalax | 13 - .../co.blocke.scalajack/json/JsonConfig.scala | 160 +++++++- .../co.blocke.scalajack/json/JsonError.scala | 5 +- .../co.blocke.scalajack/json/package.scala | 14 + .../json/reading/JsonReader.scala | 11 + .../json/writing/JsonCodecMaker.scala | 210 ++++++++++- .../json/writing/JsonOutput.scala | 237 +++++++++++- .../json/writing/JsonWriter.scalax | 111 ------ .../json/writing/JsonWriter2.scalax | 157 -------- .../json/writing/JsonWriter_old.scalax | 56 +-- .../scala/co.blocke.scalajack/run/Play.scala | 48 ++- .../co.blocke.scalajack/run/Record.scala | 6 +- .../json/primitives/JavaPrim.scala | 312 +++++++++++++++ .../json/primitives/Model.scala | 22 +- .../json/primitives/ScalaPrim.scala | 63 ++-- .../json/primitives/Simple.scala | 357 ++++++++++++++++++ 22 files changed, 1386 insertions(+), 505 deletions(-) delete mode 100644 .DS_Store delete mode 100644 src/main/scala/co.blocke.scalajack/internal/TreeNode.scala delete mode 100644 src/main/scala/co.blocke.scalajack/json/JsonCodec.scalax delete mode 100644 src/main/scala/co.blocke.scalajack/json/writing/JsonWriter.scalax delete mode 100644 src/main/scala/co.blocke.scalajack/json/writing/JsonWriter2.scalax create mode 100644 src/test/scala/co.blocke.scalajack/json/primitives/JavaPrim.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/primitives/Simple.scala diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 0e082176d842e2c9234416251cd903c2e8a99a67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!AiqG5S?wSCKRCu1&<3}3u=P}@e*qN0V8@)sYz2bG|fuW+CwSitUu(J_&v_- zZpG4iRIJRv?3>-0+0DL$-3$PT);K-_r~!b3N?3BSSs~;nU6PU=sUQkH$0H0Ngdy~! z$x<{A{6z-n-MJ7!4>IV&m-m-HMX8F=`xZT=Nj^F}d>5s1Wn**8*>bARUGS(T!6+P! z^Hw;#q28rZX*9RP=ql+=I<=iMm5stA>kV~668A9V_BzR8HEF4F7Uw!QFb>Cc+)iz8 zI`zC}U3!h^+Ep;O?Or)AsX9A42_v9mburV&0V zg{^Qu2g58?*#mlM;xfdbpgT{RuSLl$%m6dM46H8$_B3;<>$?Sha%O-T_#p=9e2}Pw zzQw|zK02_`B>*D*MruKuY6;4b7JZ9_LG+*qlZt3kg)K3JNyl+%<9v&SL6Z)`79YZ{ zENq1$^y)aj)Zrj}gWNI$%)lZ8Wz((E{eS*_{lA#RJ!XIz_*V>wN;_z`a7*@XUD_Po vwG#Cnm4xC7gP$pA=&KlG=_=ks)q>-a3`E~zVGu`9_(wp~zzs9-s| - val tpe1 = typeArg1(tpe) - tpe1.asType match - case '[t1] => - val tx = x.asExprOf[List[t1]] - '{ - $out.writeArrayStart() - var l = $tx - while (l ne Nil) { - ${genWriteVal('{ l.head }, tpe1 :: types, isStringified, None, out)} - l = l.tail - } - $out.writeArrayEnd() - } - } - -Need to learn what "withEnocderFor()" does--specifically how $out gets passed in. - - -This is the dark magic.... It creates a Symbol and a method (DefDef) for the given function f. -Not sure how to connect/call these together yet, but in isolation, this should allow us to -generate discrete functions that output a "thing". - - def withEncoderFor[T: Type](methodKey: EncoderMethodKey, arg: Expr[T], out: Expr[JsonWriter]) - (f: (Expr[JsonWriter], Expr[T])=> Expr[Unit]): Expr[Unit] = - Apply(Ref(encodeMethodSyms.getOrElse(methodKey, { - val sym = Symbol.newMethod(Symbol.spliceOwner, "e" + encodeMethodSyms.size, - MethodType(List("x", "out"))(_ => List(TypeRepr.of[T], TypeRepr.of[JsonWriter]), _ => TypeRepr.of[Unit])) - encodeMethodSyms.update(methodKey, sym) - encodeMethodDefs += DefDef(sym, params => { - val List(List(x, out)) = params - Some(f(out.asExprOf[JsonWriter], x.asExprOf[T]).asTerm.changeOwner(sym)) - }) - sym - })), List(arg.asTerm, out.asTerm)).asExprOf[Unit] - - -Here's the EncoderMethodKey thingy: - - case class EncoderMethodKey(tpe: TypeRepr, isStringified: Boolean, discriminatorKeyValue: Option[(String, String)]) - - val encodeMethodSyms = new mutable.HashMap[EncoderMethodKey, Symbol] - val encodeMethodDefs = new mutable.ArrayBuffer[DefDef] - -Not sure if we need all this drama yet or not.... Perhaps we can use this as a simple wrapper around the TypedName. Then if - it needs to store more, the shell is already wired in. \ No newline at end of file +[ ] - Study Jsoniter and how it propagates its write config between compile-time and run-time (usage too) + Ideally we'd love to be able to see cfg at compile-time and geneate accordingly.... Right now, + ScalaJack's config is purely runtime, which is less convenient for us... \ No newline at end of file diff --git a/benchmark/src/main/scala/co.blocke/ScalaJack.scala b/benchmark/src/main/scala/co.blocke/ScalaJack.scala index 16b049f9..84faf749 100644 --- a/benchmark/src/main/scala/co.blocke/ScalaJack.scala +++ b/benchmark/src/main/scala/co.blocke/ScalaJack.scala @@ -16,6 +16,6 @@ object ScalaJackZ: trait ScalaJackWritingBenchmark { @Benchmark - def writeRecordScalaJack = sj[Record].toJson(record) // 677K score - // def writeRecordScalaJack = ScalaJack[Record].toJson(record) // 1.7M score <- faster + // def writeRecordScalaJack = sj[Record].toJson(record) // 677K score + def writeRecordScalaJack = ScalaJack[Record].toJson(record) // 1.7M score <- faster } diff --git a/src/main/scala/co.blocke.scalajack/ScalaJack.scala b/src/main/scala/co.blocke.scalajack/ScalaJack.scala index b2e34006..cebe8c02 100644 --- a/src/main/scala/co.blocke.scalajack/ScalaJack.scala +++ b/src/main/scala/co.blocke.scalajack/ScalaJack.scala @@ -8,11 +8,11 @@ import quoted.Quotes import json.* case class ScalaJack[T](jsonDecoder: reading.JsonDecoder[T], jsonEncoder: JsonCodec[T]): // extends JsonCodec[T] //with YamlCodec with MsgPackCodec - def fromJson(js: String)(using cfg: JsonConfig = JsonConfig()): Either[JsonParseError, T] = + def fromJson(js: String): Either[JsonParseError, T] = jsonDecoder.decodeJson(js) val out = writing.JsonOutput() // let's clear & re-use JsonOutput--avoid re-allocating all the internal buffer space - def toJson(a: T)(using cfg: JsonConfig = JsonConfig()): String = + def toJson(a: T): String = jsonEncoder.encodeValue(a, out.clear()) out.result @@ -22,14 +22,24 @@ object ScalaJack: def apply[A](implicit a: ScalaJack[A]): ScalaJack[A] = a + // ----- Use default JsonConfig inline def sj[T]: ScalaJack[T] = ${ sjImpl[T] } - - def sjImpl[T](using q: Quotes, tt: Type[T]): Expr[ScalaJack[T]] = - import q.reflect.* + def sjImpl[T: Type](using Quotes): Expr[ScalaJack[T]] = + import quotes.reflect.* val classRef = ReflectOnType[T](quotes)(TypeRepr.of[T], true)(using scala.collection.mutable.Map.empty[TypedName, Boolean]) val jsonDecoder = reading.JsonReader.refRead(classRef) - val jsonEncoder = writing.JsonCodecMaker.generateCodecFor(classRef) + val jsonEncoder = writing.JsonCodecMaker.generateCodecFor(classRef, JsonConfig) + + '{ ScalaJack($jsonDecoder, $jsonEncoder) } + // ----- Use given JsonConfig + inline def sj[T](inline cfg: JsonConfig): ScalaJack[T] = ${ sjImplWithConfig[T]('cfg) } + def sjImplWithConfig[T: Type](cfgE: Expr[JsonConfig])(using Quotes): Expr[ScalaJack[T]] = + import quotes.reflect.* + val cfg = summon[FromExpr[JsonConfig]].unapply(cfgE) + val classRef = ReflectOnType[T](quotes)(TypeRepr.of[T], true)(using scala.collection.mutable.Map.empty[TypedName, Boolean]) + val jsonDecoder = reading.JsonReader.refRead(classRef) + val jsonEncoder = writing.JsonCodecMaker.generateCodecFor(classRef, cfg.getOrElse(JsonConfig)) '{ ScalaJack($jsonDecoder, $jsonEncoder) } // refRead[T](classRef) diff --git a/src/main/scala/co.blocke.scalajack/internal/TreeNode.scala b/src/main/scala/co.blocke.scalajack/internal/TreeNode.scala deleted file mode 100644 index 25ecb8d4..00000000 --- a/src/main/scala/co.blocke.scalajack/internal/TreeNode.scala +++ /dev/null @@ -1,16 +0,0 @@ -package co.blocke.scalajack -package internal - -case class TreeNode[P](payload: P, children: List[TreeNode[P]] = Nil): - def addChild(tn: TreeNode[P]) = this.copy(children = this.children :+ tn) - inline def hasChildren = !children.isEmpty - -object TreeNode: - - def inverted[P](tn: TreeNode[P]) = deapthFirst(tn).reverse - def deapthFirst[P](tn: TreeNode[P], acc: List[TreeNode[P]] = Nil): List[TreeNode[P]] = - tn.children.foldLeft(if acc == Nil then List(tn) else acc) { case (soFar, child) => - val nextList = soFar :+ child - if !child.hasChildren then nextList - else deapthFirst(child, nextList) - } diff --git a/src/main/scala/co.blocke.scalajack/json/JsonCodec.scalax b/src/main/scala/co.blocke.scalajack/json/JsonCodec.scalax deleted file mode 100644 index cb15290b..00000000 --- a/src/main/scala/co.blocke.scalajack/json/JsonCodec.scalax +++ /dev/null @@ -1,13 +0,0 @@ -package co.blocke.scalajack -package json - -class JsonCodec[T]: - parent: ScalaJack[T] => - - inline def fromJson( js: String )(using cfg: JsonConfig = JsonConfig()): Either[JsonParseError, T] = - parent.jsonDecoder.decodeJson(js) - - inline def toJson( a: T )(using cfg: JsonConfig = JsonConfig()): String = - parent.jsonEncoder.encode(a, new StringBuilder(), cfg) - - \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json/JsonConfig.scala b/src/main/scala/co.blocke.scalajack/json/JsonConfig.scala index 5f494d0e..c77e6b48 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonConfig.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonConfig.scala @@ -1,24 +1,158 @@ package co.blocke.scalajack package json -case class JsonConfig( - noneAsNull: Boolean = false, - forbidNullsInInput: Boolean = false, - tryFailureHandling: TryOption = TryOption.NO_WRITE, - undefinedFieldHandling: UndefinedValueOption = UndefinedValueOption.THROW_EXCEPTION, - permissivePrimitives: Boolean = false, - writeNonConstructorFields: Boolean = false, +import co.blocke.scala_reflection.TypedName +import co.blocke.scala_reflection.reflect.* +import co.blocke.scala_reflection.reflect.rtypeRefs.* +import scala.quoted.* + +class JsonConfig private[scalajack] ( + val noneAsNull: Boolean, + // forbidNullsInInput: Boolean = false, + // tryFailureHandling: TryOption = TryOption.NO_WRITE, + // undefinedFieldHandling: UndefinedValueOption = UndefinedValueOption.THROW_EXCEPTION, + // permissivePrimitives: Boolean = false, + // writeNonConstructorFields: Boolean = false, // -------------------------- - typeHintLabel: String = "_hint", - typeHintLabelByTrait: Map[String, String] = Map.empty[String, String], // Trait name -> type hint label - typeHintDefaultTransformer: String => String = (v: String) => v, // in case you want something different than class name (simple name re-mapping) - typeHintTransformer: Map[String, Any => String] = Map.empty[String, Any => String], // if you want class-specific control (instance value => String) + val typeHintLabel: String, + val typeHintPolicy: TypeHintPolicy = TypeHintPolicy.SIMPLE_CLASSNAME // -------------------------- - enumsAsIds: Char | List[String] = Nil // Char is '*' for all enums as ids, or a list of fully-qualified class names -) + // enumsAsIds: Option[List[String]] = None // None=no enums as ids, Some(Nil)=all enums as ids, Some(List(...))=specified classes enums as ids +): + def withNoneAsNull(nan: Boolean): JsonConfig = copy(noneAsNull = nan) + def withTypeHintLabel(label: String): JsonConfig = copy(typeHintLabel = label) + def withTypeHintPolicy(hintPolicy: TypeHintPolicy): JsonConfig = copy(typeHintPolicy = hintPolicy) + + private[this] def copy( + noneAsNull: Boolean = noneAsNull, + typeHintLabel: String = typeHintLabel, + typeHintPolicy: TypeHintPolicy = typeHintPolicy + ): JsonConfig = new JsonConfig( + noneAsNull, + typeHintLabel, + typeHintPolicy + ) enum TryOption: case AS_NULL, NO_WRITE, ERR_MSG_STRING, THROW_EXCEPTION enum UndefinedValueOption: case AS_NULL, AS_SYMBOL, THROW_EXCEPTION + +enum TypeHintPolicy: + case SIMPLE_CLASSNAME, SCRAMBLE_CLASSNAME, USE_ANNOTATION + +object JsonConfig + extends JsonConfig( + noneAsNull = false, + typeHintLabel = "_hint", + typeHintPolicy = TypeHintPolicy.SIMPLE_CLASSNAME + ): + import scala.quoted.FromExpr.* + + private[scalajack] given FromExpr[JsonConfig] with { + + def extract[X: FromExpr](name: String, x: Expr[X])(using Quotes): X = + import quotes.reflect.* + summon[FromExpr[X]].unapply(x).getOrElse(throw JsonConfigError(s"Can't parse $name: ${x.show}, tree: ${x.asTerm}")) + + def unapply(x: Expr[JsonConfig])(using Quotes): Option[JsonConfig] = + import quotes.reflect.* + + x match + case '{ + JsonConfig( + $noneAsNullE, + // $forbitNullsInInputE, + // $tryFailureHandlerE, + // $undefinedFieldHandlingE, + // $permissivePrimitivesE, + // $writeNonConstructorFieldsE, + $typeHintLabelE, + $typeHintPolicyE + // $enumsAsIdsE + ) + } => + try + Some( + JsonConfig( + extract("noneAsNull", noneAsNullE), + // extract("forbitNullsInInput", forbitNullsInInputE), + // extract("tryFailureHandler", tryFailureHandlerE), + // extract("undefinedFieldHandling", undefinedFieldHandlingE), + // extract("permissivePrimitives", permissivePrimitivesE), + // extract("writeNonConstructorFields", writeNonConstructorFieldsE), + // extract2[String]("typeHintLabel", x) + extract("typeHintLabel", typeHintLabelE), + extract("typeHintPolicy", typeHintPolicyE) + // extract("enumsAsIds", enumsAsIdsE) + ) + ) + catch { + case x => + println("ERROR: " + x.getMessage) + None + } + case '{ JsonConfig } => Some(JsonConfig) + case '{ ($x: JsonConfig).withNoneAsNull($v) } => Some(x.valueOrAbort.withNoneAsNull(v.valueOrAbort)) + case '{ ($x: JsonConfig).withTypeHintLabel($v) } => Some(x.valueOrAbort.withTypeHintLabel(v.valueOrAbort)) + case '{ ($x: JsonConfig).withTypeHintPolicy($v) } => Some(x.valueOrAbort.withTypeHintPolicy(v.valueOrAbort)) + case z => + println("Z: " + z.show) + None + } + + private[scalajack] given FromExpr[TryOption] with { + def unapply(x: Expr[TryOption])(using Quotes): Option[TryOption] = + import quotes.reflect.* + x match + case '{ TryOption.AS_NULL } => Some(TryOption.AS_NULL) + case '{ TryOption.NO_WRITE } => Some(TryOption.NO_WRITE) + case '{ TryOption.ERR_MSG_STRING } => Some(TryOption.ERR_MSG_STRING) + case '{ TryOption.THROW_EXCEPTION } => Some(TryOption.THROW_EXCEPTION) + } + + private[scalajack] given FromExpr[UndefinedValueOption] with { + def unapply(x: Expr[UndefinedValueOption])(using Quotes): Option[UndefinedValueOption] = + import quotes.reflect.* + x match + case '{ UndefinedValueOption.AS_NULL } => Some(UndefinedValueOption.AS_NULL) + case '{ UndefinedValueOption.AS_SYMBOL } => Some(UndefinedValueOption.AS_SYMBOL) + case '{ UndefinedValueOption.THROW_EXCEPTION } => Some(UndefinedValueOption.THROW_EXCEPTION) + } + + private[scalajack] given FromExpr[TypeHintPolicy] with { + def unapply(x: Expr[TypeHintPolicy])(using Quotes): Option[TypeHintPolicy] = + import quotes.reflect.* + x match + case '{ TypeHintPolicy.SIMPLE_CLASSNAME } => Some(TypeHintPolicy.SIMPLE_CLASSNAME) + case '{ TypeHintPolicy.SCRAMBLE_CLASSNAME } => Some(TypeHintPolicy.SCRAMBLE_CLASSNAME) + case '{ TypeHintPolicy.USE_ANNOTATION } => Some(TypeHintPolicy.USE_ANNOTATION) + } + + /* + Here's how we use Quotes to get default values from a class...def + + // Constructor argument list, preloaded with optional 'None' values and any default values specified + val preloaded = Expr + .ofList(r.fields.map { f => + val scalaF = f.asInstanceOf[ScalaFieldInfoRef] + if scalaF.defaultValueAccessorName.isDefined then + r.refType match + case '[t] => + val tpe = TypeRepr.of[t].widen + val sym = tpe.typeSymbol + val companionBody = sym.companionClass.tree.asInstanceOf[ClassDef].body + val companion = Ref(sym.companionModule) + companionBody + .collect { + case defaultMethod @ DefDef(name, _, _, _) if name.startsWith("$lessinit$greater$default$" + (f.index + 1)) => + companion.select(defaultMethod.symbol).appliedToTypes(tpe.typeArgs).asExpr + } + .headOption + .getOrElse(Expr(null.asInstanceOf[Boolean])) + else if scalaF.fieldRef.isInstanceOf[OptionRef[_]] then Expr(None) + else Expr(null.asInstanceOf[Int]) + }) + + */ diff --git a/src/main/scala/co.blocke.scalajack/json/JsonError.scala b/src/main/scala/co.blocke.scalajack/json/JsonError.scala index b5f127cf..fb85e76c 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonError.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonError.scala @@ -1,7 +1,10 @@ package co.blocke.scalajack package json -class JsonError(msg: String) extends Throwable +class JsonIllegalKeyType(msg: String) extends Throwable(msg) +class JsonNullKeyValue(msg: String) extends Throwable(msg) +class JsonUnsupportedType(msg: String) extends Throwable(msg) +class JsonConfigError(msg: String) extends Throwable(msg) class ParseError(val msg: String) extends Throwable(msg): val show: String = "" diff --git a/src/main/scala/co.blocke.scalajack/json/package.scala b/src/main/scala/co.blocke.scalajack/json/package.scala index 8bf78f50..692942f9 100644 --- a/src/main/scala/co.blocke.scalajack/json/package.scala +++ b/src/main/scala/co.blocke.scalajack/json/package.scala @@ -7,3 +7,17 @@ val BUFFER_EXCEEDED: Char = 7 // Old "BELL" ASCII value, used as a marker when w val END_OF_STRING: Char = 3 inline def lastPart(n: String) = n.split('.').last.stripSuffix("$") + +val random = new scala.util.Random() +def scramble(hash: Int): String = + val last5 = f"$hash%05d".takeRight(5) + val digits = (1 to 5).map(_ => random.nextInt(10)) + if digits(0) % 2 == 0 then s"${last5(0)}${digits(0)}${last5(1)}${digits(1)}${last5(2)}-${digits(2)}${last5(3)}${digits(3)}-${last5(4)}${digits(4)}A" + else s"${digits(0)}${last5(0)}${digits(1)}${last5(1)}${digits(2)}-${last5(2)}${digits(3)}${last5(3)}-${digits(4)}${last5(4)}B" + +def descrambleTest(in: String, hash: Int): Boolean = + val last5 = f"$hash%05d".takeRight(5) + in.last match + case 'A' if in.length == 13 => "" + in(0) + in(2) + in(4) + in(7) + in(10) == last5 + case 'B' if in.length == 13 => "" + in(1) + in(3) + in(6) + in(8) + in(11) == last5 + case _ => false diff --git a/src/main/scala/co.blocke.scalajack/json/reading/JsonReader.scala b/src/main/scala/co.blocke.scalajack/json/reading/JsonReader.scala index d06707fa..a8a12267 100644 --- a/src/main/scala/co.blocke.scalajack/json/reading/JsonReader.scala +++ b/src/main/scala/co.blocke.scalajack/json/reading/JsonReader.scala @@ -18,10 +18,21 @@ import scala.collection.Factory */ object JsonReader: + // Temporary no-op reader... def refRead[T]( ref: RTypeRef[T] )(using q: Quotes, tt: Type[T]): Expr[JsonDecoder[T]] = import quotes.reflect.* + '{ + new JsonDecoder[T] { + def unsafeDecode(in: JsonSource): T = null.asInstanceOf[T] + } + } + + def refRead2[T]( + ref: RTypeRef[T] + )(using q: Quotes, tt: Type[T]): Expr[JsonDecoder[T]] = + import quotes.reflect.* ref match case r: PrimitiveRef => diff --git a/src/main/scala/co.blocke.scalajack/json/writing/JsonCodecMaker.scala b/src/main/scala/co.blocke.scalajack/json/writing/JsonCodecMaker.scala index 0d8e3d5d..1cef76f3 100644 --- a/src/main/scala/co.blocke.scalajack/json/writing/JsonCodecMaker.scala +++ b/src/main/scala/co.blocke.scalajack/json/writing/JsonCodecMaker.scala @@ -5,12 +5,10 @@ package writing import co.blocke.scala_reflection.{RTypeRef, TypedName} import co.blocke.scala_reflection.reflect.rtypeRefs.* import scala.quoted.* -import scala.collection.mutable.Map as MMap -import internal.TreeNode object JsonCodecMaker: - def generateCodecFor[T](ref: RTypeRef[T])(using Quotes)(using tt: Type[T]) = + def generateCodecFor[T](ref: RTypeRef[T], cfg: JsonConfig)(using Quotes)(using tt: Type[T]) = import quotes.reflect.* // Cache generated method Symbols + an array of the generated functions (DefDef) @@ -47,15 +45,43 @@ object JsonCodecMaker: List(arg.asTerm, out.asTerm) ).asExprOf[Unit] - def genFnBody[T](r: RTypeRef[?], aE: Expr[T], out: Expr[JsonOutput])(using Quotes) = + def genFnBody[T](r: RTypeRef[?], aE: Expr[T], out: Expr[JsonOutput], emitDiscriminator: Boolean = false)(using Quotes): Expr[Unit] = r.refType match case '[b] => r match + case t: ArrayRef[?] => + makeFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[e] => + val tin = in.asExprOf[Array[e]] + '{ + $out.startArray() + $tin.foreach { i => + ${ genWriteVal('{ i }, t.elementRef.asInstanceOf[RTypeRef[e]], out) } + } + $out.endArray() + } + } + case t: SeqRef[?] => makeFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => t.elementRef.refType match case '[e] => - val tin = in.asExprOf[Seq[e]] + val tin = if t.isMutable then in.asExprOf[scala.collection.mutable.Seq[e]] else in.asExprOf[Seq[e]] + '{ + $out.startArray() + $tin.foreach { i => + ${ genWriteVal('{ i }, t.elementRef.asInstanceOf[RTypeRef[e]], out) } + } + $out.endArray() + } + } + + case t: SetRef[?] => + makeFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[e] => + val tin = if t.isMutable then in.asExprOf[scala.collection.mutable.Set[e]] else in.asExprOf[Set[e]] '{ $out.startArray() $tin.foreach { i => @@ -79,7 +105,19 @@ object JsonCodecMaker: ${ genWriteVal(fieldValue, f.fieldRef.asInstanceOf[RTypeRef[z]], out) } } } - if eachField.length == 1 then eachField.head + if emitDiscriminator then + val cname = cfg.typeHintPolicy match + case TypeHintPolicy.SIMPLE_CLASSNAME => Expr(lastPart(t.name)) + case TypeHintPolicy.SCRAMBLE_CLASSNAME => + val hash = Expr(lastPart(t.name).hashCode) + '{ scramble($hash) } + case TypeHintPolicy.USE_ANNOTATION => ??? + val withDisc = '{ + $out.label(${ Expr(cfg.typeHintLabel) }) + $out.value($cname) + } +: eachField + Expr.block(withDisc.init, withDisc.last) + else if eachField.length == 1 then eachField.head else Expr.block(eachField.init, eachField.last) } '{ @@ -89,13 +127,65 @@ object JsonCodecMaker: } } + case t: MapRef[?] => + t.elementRef.refType match + case '[k] => + makeFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef2.refType match + case '[v] => + val tin = if t.isMutable then in.asExprOf[scala.collection.mutable.Map[k, v]] else in.asExprOf[Map[k, v]] + '{ + $out.startObject() + $tin.foreach { case (k, v) => + $out.maybeComma() + ${ genWriteVal('{ k }, t.elementRef.asInstanceOf[RTypeRef[k]], out, true) } + $out.colon() + ${ genWriteVal('{ v }, t.elementRef2.asInstanceOf[RTypeRef[v]], out) } + } + $out.endObject() + } + } + + case t: TraitRef[?] => + // classesSeen.put(t.typedName, t) + // val rt = t.expr.asInstanceOf[Expr[TraitRType[T]]] + if !t.isSealed then throw new JsonUnsupportedType("Non-sealed traits are not supported") + if t.childrenAreObject then + // case object -> just write the simple name of the object + val tin = aE.asExprOf[b] + '{ + $out.value($tin.getClass.getName.split('.').last.stripSuffix("$")) + } + else + // So... sealed trait children could be any of those defined for the trait. We need to + // generate functions for each child then a master function that examines $aE and based on + // its value, call the appropriate function to render. + // val beforeKeys = methodSyms.keySet + // t.sealedChildren.foreach { child => + // child.refType match + // case '[c] => + // genFnBody[c](child, aE.asExprOf[c], out) + // } + // Now generate and return the calling function based on runtime type + // Refer to Jsoniter: JsonCodecMaker.scala around line 920 for example how to do this, incl a wildcard, which + // we don't need here. + val cases = t.sealedChildren.map { child => + child.refType match + case '[c] => + val subtype = TypeIdent(TypeRepr.of[c].typeSymbol) + val sym = Symbol.newBind(Symbol.spliceOwner, "t", Flags.EmptyFlags, subtype.tpe) + CaseDef(Bind(sym, Typed(Ref(sym), subtype)), None, genFnBody[c](child, Ref(sym).asExprOf[c], out, true).asTerm) + } :+ CaseDef(Literal(NullConstant()), None, '{ $out.burpNull() }.asTerm) + val matchExpr = Match(aE.asTerm, cases).asExprOf[Unit] + matchExpr + def genWriteVal[T: Type]( aE: Expr[T], ref: RTypeRef[T], - // types: List[TypeRepr], - // isStringified: Boolean, // config option to wrap numbers, boolean, etc in "". Not needed for now... we'll see later... // optWriteDiscriminator: Option[WriteDiscriminator], - out: Expr[JsonOutput] + out: Expr[JsonOutput], + // cfgE: Expr[JsonConfig], + isStringified: Boolean = false // e.g. Map key values. Doesn't apply to stringish values, which are always quotes-wrapped )(using Quotes): Expr[Unit] = val methodKey = MethodKey(ref, false) methodSyms @@ -105,12 +195,96 @@ object JsonCodecMaker: } .getOrElse( ref match - case t: BooleanRef => '{ $out.value(${ aE.asExprOf[Boolean] }) } - case t: IntRef => '{ $out.value(${ aE.asExprOf[Int] }) } - case t: StringRef => '{ $out.value(${ aE.asExprOf[String] }) } - case _ => - println("Gen for: " + ref) - genFnBody(ref, aE, out) + // First cover all primitive and simple types... + case t: BigDecimalRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[scala.math.BigDecimal] }) } + else '{ $out.value(${ aE.asExprOf[scala.math.BigDecimal] }) } + case t: BigIntRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[scala.math.BigInt] }) } + else '{ $out.value(${ aE.asExprOf[scala.math.BigInt] }) } + case t: BooleanRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[Boolean] }) } + else '{ $out.value(${ aE.asExprOf[Boolean] }) } + case t: ByteRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[Byte] }) } + else '{ $out.value(${ aE.asExprOf[Byte] }) } + case t: CharRef => '{ $out.value(${ aE.asExprOf[Char] }) } + case t: DoubleRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[Double] }) } + else '{ $out.value(${ aE.asExprOf[Double] }) } + case t: FloatRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[Float] }) } + else '{ $out.value(${ aE.asExprOf[Float] }) } + case t: IntRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[Int] }) } + else '{ $out.value(${ aE.asExprOf[Int] }) } + case t: LongRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[Long] }) } + else '{ $out.value(${ aE.asExprOf[Long] }) } + case t: ShortRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[Short] }) } + else '{ $out.value(${ aE.asExprOf[Short] }) } + case t: StringRef => '{ $out.value(${ aE.asExprOf[String] }) } + + case t: JBigDecimalRef => + if isStringified then '{ $out.valueStringified(${ aE.asExprOf[java.math.BigDecimal] }) } + else '{ $out.value(${ aE.asExprOf[java.math.BigDecimal] }) } + case t: JBigIntegerRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.math.BigInteger] }) } + else '{ $out.value(${ aE.asExprOf[java.math.BigInteger] }) } + case t: JBooleanRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Boolean] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Boolean] }) } + case t: JByteRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Byte] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Byte] }) } + case t: JCharacterRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Character] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Character] }) } + case t: JDoubleRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Double] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Double] }) } + case t: JFloatRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Float] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Float] }) } + case t: JIntegerRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Integer] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Integer] }) } + case t: JLongRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Long] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Long] }) } + case t: JShortRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Short] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Short] }) } + case t: JNumberRef => + if isStringified then '{ $out.value(${ aE.asExprOf[java.lang.Number] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Number] }) } + + case t: DurationRef => '{ $out.value(${ aE.asExprOf[java.time.Duration] }) } + case t: InstantRef => '{ $out.value(${ aE.asExprOf[java.time.Instant] }) } + case t: LocalDateRef => '{ $out.value(${ aE.asExprOf[java.time.LocalDate] }) } + case t: LocalDateTimeRef => '{ $out.value(${ aE.asExprOf[java.time.LocalDateTime] }) } + case t: LocalTimeRef => '{ $out.value(${ aE.asExprOf[java.time.LocalTime] }) } + case t: MonthDayRef => '{ $out.value(${ aE.asExprOf[java.time.MonthDay] }) } + case t: OffsetDateTimeRef => '{ $out.value(${ aE.asExprOf[java.time.OffsetDateTime] }) } + case t: OffsetTimeRef => '{ $out.value(${ aE.asExprOf[java.time.OffsetTime] }) } + case t: PeriodRef => '{ $out.value(${ aE.asExprOf[java.time.Period] }) } + case t: YearRef => '{ $out.value(${ aE.asExprOf[java.time.Year] }) } + case t: YearMonthRef => '{ $out.value(${ aE.asExprOf[java.time.YearMonth] }) } + case t: ZonedDateTimeRef => '{ $out.value(${ aE.asExprOf[java.time.ZonedDateTime] }) } + case t: ZoneIdRef => '{ $out.value(${ aE.asExprOf[java.time.ZoneId] }) } + case t: ZoneOffsetRef => '{ $out.value(${ aE.asExprOf[java.time.ZoneOffset] }) } + + case t: UUIDRef => '{ $out.value(${ aE.asExprOf[java.util.UUID] }) } + + case t: AliasRef[?] => + t.unwrappedType.refType match + case '[e] => + genWriteVal[e](aE.asInstanceOf[Expr[e]], t.unwrappedType.asInstanceOf[RTypeRef[e]], out) + + // Everything else... + case _ if isStringified => throw new JsonIllegalKeyType("Non-primitive/non-simple types cannot be map keys") + case _ => genFnBody(ref, aE, out) ) // ================================================================ @@ -128,14 +302,12 @@ object JsonCodecMaker: // else genReadVal(rootTpe :: Nil, 'default, cfg.isStringified, false, 'in) // } - def encodeValue(in: T, out: JsonOutput): Unit = ${ - genWriteVal('in, ref, 'out) - } + def encodeValue(in: T, out: JsonOutput): Unit = ${ genWriteVal('in, ref, 'out) } } }.asTerm val neededDefs = // others here??? Refer to Jsoniter file JsonCodecMaker.scala methodDefs val codec = Block(neededDefs.toList, codecDef).asExprOf[JsonCodec[T]] - println(s"Codec: ${codec.show}") + // println(s"Codec: ${codec.show}") codec diff --git a/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala b/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala index dcf0c510..7e05cf37 100644 --- a/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala +++ b/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala @@ -2,6 +2,8 @@ package co.blocke.scalajack package json package writing +import java.time.format.DateTimeFormatter.* + case class JsonOutput(): val internal: StringBuilder = new StringBuilder() @@ -31,33 +33,93 @@ case class JsonOutput(): internal.append(']') comma = true + inline def colon(): Unit = + internal.append(':') + comma = false + inline def maybeComma(): Unit = if comma then internal.append(',') comma = false inline def burpNull(): Unit = + maybeComma() internal.append("null") + comma = true inline def label(s: String): Unit = maybeComma() internal.append("\"" + s + "\":") - inline def label(s: Long): Unit = + // ----------------------- Primitive/Simple type support + + inline def value(v: scala.math.BigDecimal): Unit = maybeComma() - internal.append("\"" + s + "\":") + if v == null then internal.append("null") + else internal.append(v) + comma = true - // ----------------------- Primitive/Simple type support + // Note: data types that are not naturally quotes-wrapped have "Stringified" variants + // (saparate vs a param for speed), for use in Map/Json object keys. + inline def valueStringified(v: scala.math.BigDecimal): Unit = + maybeComma() + if v == null then throw new JsonNullKeyValue("Key values may not be null") + else internal.append("\"" + v + "\"") + comma = true - // TODO: BigDecimal, BigInt and Java equiv. + inline def value(v: scala.math.BigInt): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: scala.math.BigInt): Unit = + maybeComma() + if v == null then throw new JsonNullKeyValue("Key values may not be null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.math.BigDecimal): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.math.BigDecimal): Unit = + maybeComma() + if v == null then throw new JsonNullKeyValue("Key values may not be null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.math.BigInteger): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.math.BigInteger): Unit = + maybeComma() + if v == null then throw new JsonNullKeyValue("Key values may not be null") + else internal.append("\"" + v + "\"") + comma = true inline def value(v: Boolean): Unit = maybeComma() internal.append(v) comma = true + inline def valueStringified(v: Boolean): Unit = + maybeComma() + internal.append("\"" + v + "\"") + comma = true + inline def value(v: Byte): Unit = maybeComma() - internal.append(v) + internal.append(v.toInt) + comma = true + + inline def valueStringified(v: Byte): Unit = + maybeComma() + internal.append("\"" + v + "\"") comma = true inline def value(v: Char): Unit = @@ -70,26 +132,51 @@ case class JsonOutput(): internal.append(v) comma = true + inline def valueStringified(v: Double): Unit = + maybeComma() + internal.append("\"" + v + "\"") + comma = true + inline def value(v: Float): Unit = maybeComma() internal.append(v) comma = true + inline def valueStringified(v: Float): Unit = + maybeComma() + internal.append("\"" + v + "\"") + comma = true + inline def value(v: Int): Unit = maybeComma() internal.append(v) comma = true + inline def valueSringified(v: Int): Unit = + maybeComma() + internal.append("\"" + v + "\"") + comma = true + inline def value(v: Long): Unit = maybeComma() internal.append(v) comma = true + inline def valueStringified(v: Long): Unit = + maybeComma() + internal.append("\"" + v + "\"") + comma = true + inline def value(v: Short): Unit = maybeComma() internal.append(v) comma = true + inline def valueStringified(v: Short): Unit = + maybeComma() + internal.append("\"" + v + "\"") + comma = true + inline def value(v: String): Unit = maybeComma() if v == null then internal.append("null") @@ -102,10 +189,22 @@ case class JsonOutput(): else internal.append(v) comma = true + inline def valueStringified(v: java.lang.Boolean): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + inline def value(v: java.lang.Byte): Unit = maybeComma() if v == null then internal.append("null") - else internal.append(v) + else internal.append(v.toInt) + comma = true + + inline def valueStringified(v: java.lang.Byte): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v.toInt + "\"") comma = true inline def value(v: java.lang.Character): Unit = @@ -120,34 +219,158 @@ case class JsonOutput(): else internal.append(v) comma = true + inline def valueStringified(v: java.lang.Double): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + inline def value(v: java.lang.Float): Unit = maybeComma() if v == null then internal.append("null") else internal.append(v) comma = true + inline def valueStringified(v: java.lang.Float): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + inline def value(v: java.lang.Integer): Unit = maybeComma() if v == null then internal.append("null") else internal.append(v) comma = true + inline def valueStringified(v: java.lang.Integer): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + inline def value(v: java.lang.Long): Unit = maybeComma() if v == null then internal.append("null") else internal.append(v) comma = true + inline def valueStringified(v: java.lang.Long): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + inline def value(v: java.lang.Short): Unit = maybeComma() if v == null then internal.append("null") else internal.append(v) comma = true + inline def valueStringified(v: java.lang.Short): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + inline def value(v: java.lang.Number): Unit = maybeComma() if v == null then internal.append("null") else internal.append(v) comma = true - // TODO: UUID + inline def valueStringified(v: java.lang.Number): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.time.Duration): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.time.Instant): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.time.LocalDate): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v.format(ISO_LOCAL_DATE) + "\"") + comma = true + + inline def value(v: java.time.LocalDateTime): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v.format(ISO_LOCAL_DATE_TIME) + "\"") + comma = true + + inline def value(v: java.time.LocalTime): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v.format(ISO_LOCAL_TIME) + "\"") + comma = true + + inline def value(v: java.time.MonthDay): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.time.OffsetDateTime): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v.format(ISO_OFFSET_DATE_TIME) + "\"") + comma = true + + inline def value(v: java.time.OffsetTime): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v.format(ISO_OFFSET_TIME) + "\"") + comma = true + + inline def value(v: java.time.Period): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.time.Year): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.time.YearMonth): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.time.ZonedDateTime): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v.format(ISO_ZONED_DATE_TIME) + "\"") + comma = true + + inline def value(v: java.time.ZoneId): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.time.ZoneOffset): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true + + inline def value(v: java.util.UUID): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append("\"" + v + "\"") + comma = true diff --git a/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter.scalax b/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter.scalax deleted file mode 100644 index ff2ae12c..00000000 --- a/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter.scalax +++ /dev/null @@ -1,111 +0,0 @@ -package co.blocke.scalajack -package json -package writing - -import co.blocke.scala_reflection.{RTypeRef, TypedName} -import co.blocke.scala_reflection.reflect.rtypeRefs.* -import co.blocke.scala_reflection.rtypes.* -import scala.quoted.* -import scala.collection.mutable.Map as MMap -import scala.util.Failure - -object JsonWriter: - - private def shouldTestForOkToWrite(r: RTypeRef[?]): Boolean = - r match - case _: OptionRef[?] => true - case _: LeftRightRef[?] => true - case _: TryRef[?] => true - case _ => false - - // Tests whether we should write something or not--mainly in the case of Option, or wrapped Option - // Affected types: Option, java.util.Optional, Left/Right, Try/Failure - private def isOkToWrite(a: Any, cfg: JsonConfig) = - a match - case None if !cfg.noneAsNull => false - case o: java.util.Optional[?] if o.isEmpty && !cfg.noneAsNull => false - case Left(None) if !cfg.noneAsNull => false - case Right(None) if !cfg.noneAsNull => false - case Failure(_) if cfg.tryFailureHandling == TryOption.NO_WRITE => false - case _ => true - - def refRead[T]( - ref: RTypeRef[T] - )(using q: Quotes, tt: Type[T]): Expr[(T, JsonOutput, JsonConfig) => String] = - import quotes.reflect.* - '{ (a: T, out: JsonOutput, cfg: JsonConfig) => - ${ refWrite(ref, '{ a }, '{ out }, '{ cfg })(using MMap.empty[TypedName, RTypeRef[?]]) }.result - } - - private def refWrite[T]( - ref: RTypeRef[T], - aE: Expr[T], - outE: Expr[JsonOutput], - cfgE: Expr[JsonConfig], - isMapKey: Boolean = false - )(using classesSeen: MMap[TypedName, RTypeRef[?]])(using q: Quotes, tt: Type[T]): Expr[JsonOutput] = - import quotes.reflect.* - - ref match - case t: BooleanRef => '{ $outE.value($aE) } - case t: IntRef => '{ $outE.value($aE) } - case t: StringRef => '{ $outE.value($aE) } - - case t: SeqRef[?] => - if isMapKey then throw new JsonError("Seq instances cannot be map keys") - t.elementRef.refType match - case '[e] => - val bE = aE.asInstanceOf[Expr[Seq[e]]] // pull type-cast into complie-time for performance gains - '{ - if $aE == null then $outE.burpNull() - else - $outE.startArray() - $bE.foreach { one => - ${ - if shouldTestForOkToWrite(t.elementRef) then // A lot of drama to avoid 1 'if' stmt--it matters for max performance - '{ - if isOkToWrite(one, $cfgE) then ${ refWrite[e](t.elementRef.asInstanceOf[RTypeRef[e]], '{ one }, outE, cfgE) } - } - else refWrite[e](t.elementRef.asInstanceOf[RTypeRef[e]], '{ one }, outE, cfgE) - } - } - $outE.endArray() - } - - case t: ScalaClassRef[?] => - classesSeen.put(t.typedName, t) - val isCase = Expr(t.isCaseClass) - '{ - // Experiment (works!) to generate def fn(in:T, sb: JsonOut). The goal is to pre-calc all the ugly stuff so that the - // macro generated is as pure/fast as possible. - // - // def foo(in: T): Unit = - // ${ - // Expr.ofList(t.fields.map { f => - // '{ println("Field: " + ${ Select.unique('in.asTerm, f.name).asExprOf[Any] }) } - // }) - // } - if $aE == null then $outE.burpNull() - else - $outE.startObject() - ${ - Expr.ofList(t.fields.map { f => - f.fieldRef.refType match - case '[e] => - val fieldValue = Select.unique(aE.asTerm, f.name).asExprOf[e] - val name = Expr(f.name) - if shouldTestForOkToWrite(f.fieldRef) then // A lot of drama to avoid 1 'if' stmt--it matters for max performance - '{ - if isOkToWrite($fieldValue, $cfgE) then - $outE.label($name) - ${ refWrite[e](f.fieldRef.asInstanceOf[RTypeRef[e]], fieldValue, outE, cfgE) } - } - else - '{ - $outE.label($name) - ${ refWrite[e](f.fieldRef.asInstanceOf[RTypeRef[e]], fieldValue, outE, cfgE) } - } - }) - } - $outE.endObject() - } diff --git a/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter2.scalax b/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter2.scalax deleted file mode 100644 index daec147b..00000000 --- a/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter2.scalax +++ /dev/null @@ -1,157 +0,0 @@ -package co.blocke.scalajack -package json -package writing - -import co.blocke.scala_reflection.{RTypeRef, TypedName} -import co.blocke.scala_reflection.reflect.rtypeRefs.* -import scala.quoted.* -import scala.collection.mutable.Map as MMap -import scala.util.Failure - -object JsonWriter2: - - private def shouldTestForOkToWrite(r: RTypeRef[?]): Boolean = - r match - case _: OptionRef[?] => true - case _: LeftRightRef[?] => true - case _: TryRef[?] => true - case _ => false - - // Tests whether we should write something or not--mainly in the case of Option, or wrapped Option - // Affected types: Option, java.util.Optional, Left/Right, Try/Failure - private def isOkToWrite(a: Any, cfg: JsonConfig) = - a match - case None if !cfg.noneAsNull => false - case o: java.util.Optional[?] if o.isEmpty && !cfg.noneAsNull => false - case Left(None) if !cfg.noneAsNull => false - case Right(None) if !cfg.noneAsNull => false - case Failure(_) if cfg.tryFailureHandling == TryOption.NO_WRITE => false - case _ => true - - def refRead[T]( - ref: RTypeRef[T] - )(using q: Quotes, tt: Type[T]): Expr[(T, StringBuilder, JsonConfig) => String] = - import quotes.reflect.* - '{ (a: T, sb: StringBuilder, cfg: JsonConfig) => - ${ refWrite(ref, '{ a }, '{ sb }, '{ cfg })(using MMap.empty[TypedName, RTypeRef[?]]) }.toString - } - - private def refWrite[T]( - ref: RTypeRef[T], - aE: Expr[T], - sbE: Expr[StringBuilder], - cfgE: Expr[JsonConfig], - isMapKey: Boolean = false - )(using classesSeen: MMap[TypedName, RTypeRef[?]])(using q: Quotes, tt: Type[T]): Expr[StringBuilder] = - import quotes.reflect.* - - ref match - case t: PrimitiveRef[?] if t.family == PrimFamily.Stringish => - if t.isNullable then '{ if $aE == null then $sbE.append("null") else $sbE.append("\"" + $aE + "\"") } - else '{ $sbE.append("\"" + $aE + "\"") } - - case t: PrimitiveRef[?] => - val isNullable = Expr(t.isNullable) - if isMapKey then - '{ - if $isNullable && $aE == null then $sbE.append("\"null\"") - else $sbE.append($sbE.append("\"" + $aE + "\"")) - } - else if t.isNullable then '{ if $aE == null then $sbE.append("null") else $sbE.append("\"" + $aE.toString + "\"") } - else '{ $sbE.append($aE.toString) } - - case t: SeqRef[?] => - if isMapKey then throw new JsonError("Seq instances cannot be map keys") - t.elementRef.refType match - case '[e] => - val bE = aE.asInstanceOf[Expr[Seq[e]]] // pull type-cast into complie-time for performance gains - '{ - if $aE == null then $sbE.append("null") - else - $sbE.append('[') - val sbLen = $sbE.length() - - $bE.foreach { one => - ${ - if shouldTestForOkToWrite(t.elementRef) then // A lot of drama to avoid 1 'if' stmt--it matters for max performance - '{ - if isOkToWrite(one, $cfgE) then - ${ refWrite[e](t.elementRef.asInstanceOf[RTypeRef[e]], '{ one }, sbE, cfgE) } - $sbE.append(',') - } - else - '{ - ${ refWrite[e](t.elementRef.asInstanceOf[RTypeRef[e]], '{ one }, sbE, cfgE) } - $sbE.append(',') - } - } - } - if sbLen == $sbE.length() then $sbE.append(']') - else $sbE.setCharAt($sbE.length() - 1, ']') - } - - case t: ScalaClassRef[?] => - classesSeen.put(t.typedName, t) - val isCase = Expr(t.isCaseClass) - '{ - // Experiment (works!) to generate def fn(in:T, sb: JsonOut). The goal is to pre-calc all the ugly stuff so that the - // macro generated is as pure/fast as possible. - // - // def foo(in: T): Unit = - // ${ - // Expr.ofList(t.fields.map { f => - // '{ println("Field: " + ${ Select.unique('in.asTerm, f.name).asExprOf[Any] }) } - // }) - // } - if $aE == null then $sbE.append("null") - else - $sbE.append('{') - val sbLen = $sbE.length() - ${ - Expr.ofList(t.fields.map { f => - f.fieldRef.refType match - case '[e] => - val fieldValue = Select.unique(aE.asTerm, f.name).asExprOf[e] - val name = Expr(f.name) - if shouldTestForOkToWrite(f.fieldRef) then // A lot of drama to avoid 1 'if' stmt--it matters for max performance - '{ - if isOkToWrite($fieldValue, $cfgE) then - $sbE.append("\"" + $name + ":\"") - ${ refWrite[e](f.fieldRef.asInstanceOf[RTypeRef[e]], fieldValue, sbE, cfgE) } - $sbE.append(',') - } - else - '{ - $sbE.append("\"" + $name + ":\"") - ${ refWrite[e](f.fieldRef.asInstanceOf[RTypeRef[e]], fieldValue, sbE, cfgE) } - $sbE.append(',') - } - }) - } - // write out any non-constructor fields (non-case "plain" classes) - if ! $isCase && $cfgE.writeNonConstructorFields then - ${ - Expr.ofList(t.nonConstructorFields.map { f => - f.fieldRef.refType match - case '[e] => - val fieldValue = Select.unique(aE.asTerm, f.getterLabel).asExprOf[e] - val name = Expr(f.name) - if shouldTestForOkToWrite(f.fieldRef) then // A lot of drama to avoid 1 'if' stmt--it matters for max performance - '{ - if isOkToWrite($fieldValue, $cfgE) then - $sbE.append(s"\"$$name\":") - ${ refWrite[e](f.fieldRef.asInstanceOf[RTypeRef[e]], fieldValue, sbE, cfgE) } - $sbE.append(',') - else $sbE - } - else - '{ - $sbE.append(s"\"$$name\":") - ${ refWrite[e](f.fieldRef.asInstanceOf[RTypeRef[e]], fieldValue, sbE, cfgE) } - $sbE.append(',') - } - }) - } - if sbLen == $sbE.length() then $sbE.append('}') - else $sbE.setCharAt($sbE.length() - 1, '}') - } diff --git a/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter_old.scalax b/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter_old.scalax index c3fb4344..a2aae7fb 100644 --- a/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter_old.scalax +++ b/src/main/scala/co.blocke.scalajack/json/writing/JsonWriter_old.scalax @@ -12,32 +12,36 @@ import scala.quoted.staging.* /* TODO: - [*] - Scala non-case class - [*] - Java class (Do I still want to support this???) - [*] - Enum - [*] - Enumeration - [*] - Java Enum - [*] - Java Collections - [*] - Java Map - [*] - Intersection - [*] - Union - [*] - Either - [*] - Object (How???) - [*] - Trait (How???) - [*] - Sealed trait - [*] - sealed abstract class (handle like sealed trait....) - [*] - SelfRef - [*] - Tuple - [*] - Unknown (throw exception) - [*] - Scala 2 (throw exception) - [*] - TypeSymbol (throw exception) - [*] - Value class - - [*] -- correct all the 'if $aE == null...' - [*] -- type hint label mapping - [*] -- type hint value mapping - [*] -- Discontinue use of inTermsOf "open" (non-sealed) trait support (?!) - [*] -- update runtime-size TraitRType handling to match new compile-time code + [*] - Primitive types + [*] - Simple Types + [*] - Seq/Set/Array + [*] - Map + [*] - Scala case class + [ ] - Scala non-case class + [ ] - Java class + [ ] - Enum + [ ] - Enumeration + [ ] - Java Enum + [ ] - Java Collections + [ ] - Java Map + [ ] - Intersection + [ ] - Union + [ ] - Either + [*] - Alias type + [ ] - Object (How???) + [X] - Trait (How???) + [ ] - Sealed Trait + [ ] - Sealed Abstract Class (handle like sealed trait....) + [ ] - SelfRef + [ ] - Tuple + [ ] - Unknown (throw exception) + [ ] - Scala 2 (throw exception) + [ ] - TypeSymbol (throw exception) + [ ] - Value class + + [ ] -- type hint label mapping + [ ] -- type hint value mapping + [ ] -- update runtime-size TraitRType handling to match new compile-time code [ ] -- Streaming JSON write support [ ] -- BigJSON support (eg. multi-gig file) diff --git a/src/main/scala/co.blocke.scalajack/run/Play.scala b/src/main/scala/co.blocke.scalajack/run/Play.scala index 1d5de457..37431f9f 100644 --- a/src/main/scala/co.blocke.scalajack/run/Play.scala +++ b/src/main/scala/co.blocke.scalajack/run/Play.scala @@ -4,52 +4,50 @@ package run import co.blocke.scala_reflection.* import scala.jdk.CollectionConverters.* import scala.reflect.ClassTag +import json.* object RunMe extends App: - given json.JsonConfig = json - .JsonConfig() - .copy(noneAsNull = true) - .copy(writeNonConstructorFields = true) - // .copy(enumsAsIds = '*') + // import scala.util.Random + // val random = new Random() + + // def scramble(hash: Int): String = + // val last5 = f"$hash%05d".takeRight(5) + // val digits = (1 to 5).map(_ => random.nextInt(10)) + // if digits(0) % 2 == 0 then s"${last5(0)}${digits(0)}${last5(1)}${digits(1)}${last5(2)}-${digits(2)}${last5(3)}${digits(3)}-${last5(4)}${digits(4)}A" + // else s"${digits(0)}${last5(0)}${digits(1)}${last5(1)}${digits(2)}-${last5(2)}${digits(3)}${last5(3)}-${digits(4)}${last5(4)}B" try import json.* import ScalaJack.* + implicit val blah: ScalaJack[Foo] = sj[Foo](JsonConfig.withTypeHintPolicy(TypeHintPolicy.SCRAMBLE_CLASSNAME)) + // co.blocke.scalajack.internal.CodePrinter.code { // sj[Record] // } - val v = Foo("Hey", "Boo") - // println(sj[Foo].toJson(v)) - println(sj[Record].toJson(record)) + val v = Foo("Hey", Fish("Bloop", false)) + // val v = Foo("Hey", "Boo") + + println(ScalaJack[Foo].toJson(v)) + // println(sj[Foo](JsonConfig.withTypeHintLabel("bogus")).toJson(v)) // println(sj[Record].toJson(record)) // println("------") // println(sj[Record].fromJson(jsData)) - - // import internal.* - - // val root = TreeNode("1A", List(TreeNode("2A", List(TreeNode("3A", Nil))), TreeNode("2B", List(TreeNode("3B", Nil))), TreeNode("2C", List(TreeNode("3C", Nil))))) - - // val m = Map( - // "1A" -> "Report", - // "2A" -> "Person", - // "2B" -> "Seq[Friend]", - // "2C" -> "Seq[Pet]", - // "3A" -> "Address", - // "3B" -> "Friend", - // "3C" -> "Pet" - // ) - - // println(TreeNode.inverted(root).map(p => m(p.payload))) - catch { case t: Throwable => println(s"BOOM ($t): " + t.getMessage) t.printStackTrace } + + // val s1 = scramble(15) + // val s2 = scramble(394857) + // println(s1) + // println(s2) + // println(descrambleTest(s1, 15)) + // println(descrambleTest(s2, 394857)) diff --git a/src/main/scala/co.blocke.scalajack/run/Record.scala b/src/main/scala/co.blocke.scalajack/run/Record.scala index fc11e620..963fb7d1 100644 --- a/src/main/scala/co.blocke.scalajack/run/Record.scala +++ b/src/main/scala/co.blocke.scalajack/run/Record.scala @@ -37,9 +37,13 @@ case class Record( ) // case class Foo(name: String, maybe: Option[Int], age: Int, expected: String = "nada", gotit: Option[Int] = Some(5)) -case class Foo(name: String, expected: String = "nada") +case class Foo(name: String, a: Animal, expected: String = "nada") // case class Foo(name: String, age: Int, expected: String = "nada") +sealed trait Animal +case class Dog(name: String, numLegs: Int) extends Animal +case class Fish(name: String, isFreshwater: Boolean) extends Animal + val jsData = """{ "person": { diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrim.scala b/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrim.scala new file mode 100644 index 00000000..05a3b009 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrim.scala @@ -0,0 +1,312 @@ +package co.blocke.scalajack +package json +package primitives + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* + +import java.lang.{ + Boolean => JBoolean, + Byte => JByte, + Double => JDouble, + Float => JFloat, + Integer => JInt, + Long => JLong, + Short => JShort +} +import java.math.{ BigDecimal => JBigDecimal, BigInteger => JBigInteger } + +class JavaPrim() extends AnyFunSpec with JsonMatchers: + + describe(colorString("---------------------------\n: Java Primitive Tests :\n---------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("BigDecimal must work") { + val inst = SampleJBigDecimal( + JBigDecimal.ZERO, + JBigDecimal.ONE, + JBigDecimal.TEN, + new JBigDecimal( + "0.1499999999999999944488848768742172978818416595458984375" + ), + null + ) + val js = sj[SampleJBigDecimal].toJson(inst) + js should matchJson("""{"bd1":0,"bd2":1,"bd3":10,"bd4":0.1499999999999999944488848768742172978818416595458984375,"bd5":null}""") + // inst shouldEqual ScalaJack.read[SampleJBigDecimal](js) + } + + it("BigInteger must work") { + val inst = SampleJBigInteger( + JBigInteger.ZERO, + JBigInteger.ONE, + JBigInteger.TEN, + new JBigInteger("-90182736451928374653345"), + new JBigInteger("90182736451928374653345"), + new JBigInteger("0"), + null + ) + val js = sj[SampleJBigInteger].toJson(inst) + js should matchJson("""{"bi1":0,"bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":90182736451928374653345,"bi6":0,"bi7":null}""") + // inst shouldEqual ScalaJack.read[SampleJBigInteger](js) + } + + it("Boolean must work") { + val inst = SampleJBoolean(JBoolean.TRUE, JBoolean.FALSE, true, false, null) + val js = sj[SampleJBoolean].toJson(inst) + js should matchJson("""{"bool1":true,"bool2":false,"bool3":true,"bool4":false,"bool5":null}""") + // inst shouldEqual ScalaJack.read[SampleJBoolean](js) + } + + it("Byte must work") { + val inst = SampleJByte( + JByte.MAX_VALUE, + JByte.MIN_VALUE, + 0.asInstanceOf[Byte], + 64.asInstanceOf[Byte], + null + ) + val js = sj[SampleJByte].toJson(inst) + js should matchJson("""{"b1":127,"b2":-128,"b3":0,"b4":64,"b5":null}""") + // inst shouldEqual ScalaJack.read[SampleJByte](js) + } + + it("Character must work") { + val inst = SampleJChar('Z', '\u20A0', null) + val js = sj[SampleJChar].toJson(inst) + js should matchJson("""{"c1":"Z","c2":"\""" + """u20a0","c3":null}""") + // inst shouldEqual ScalaJack.read[SampleJChar](js) + } + + it("Double must work") { + val inst = SampleJDouble( + JDouble.MAX_VALUE, + JDouble.MIN_VALUE, + 0.0, + -123.4567, + null + ) + val js = sj[SampleJDouble].toJson(inst) + js should matchJson("""{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":0.0,"d4":-123.4567,"d5":null}""") + // inst shouldEqual ScalaJack.read[SampleJDouble](js) + } + + it("Float must work") { + val inst = SampleJFloat( + JFloat.MAX_VALUE, + JFloat.MIN_VALUE, + 0.0F, + -123.4567F, + null + ) + val js = sj[SampleJFloat].toJson(inst) + js should matchJson("""{"f1":3.4028235E38,"f2":1.4E-45,"f3":0.0,"f4":-123.4567,"f5":null}""") + // inst shouldEqual ScalaJack.read[SampleJFloat](js) + } + + it("Integer must work") { + val inst = SampleJInt(JInt.MAX_VALUE, JInt.MIN_VALUE, 0, 123, null) + val js = sj[SampleJInt].toJson(inst) + js should matchJson("""{"i1":2147483647,"i2":-2147483648,"i3":0,"i4":123,"i5":null}""") + // inst shouldEqual ScalaJack.read[SampleJInt](js) + } + + it("Long must work") { + val inst = SampleJLong(JLong.MAX_VALUE, JLong.MIN_VALUE, 0L, 123L, null) + val js = sj[SampleJLong].toJson(inst) + js should matchJson("""{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0,"l4":123,"l5":null}""") + // inst shouldEqual ScalaJack.read[SampleJLong](js) + } + + it("Number must work") { + val inst = SampleJNumber( + JByte.valueOf("-128"), + JByte.valueOf("127"), + JShort.valueOf("-32768"), + JShort.valueOf("32767"), + JInt.valueOf("-2147483648"), + JInt.valueOf("2147483647"), + JLong.valueOf("-9223372036854775808"), + JLong.valueOf("9223372036854755807"), + null, //new JBigInteger("9923372036854755810"), + JByte.valueOf("0"), + JFloat.valueOf("3.4e-038"), + JFloat.valueOf("3.4e+038"), + JDouble.valueOf("1.7e-308"), + JDouble.valueOf("1.7e+308"), + null, //new JBigDecimal("1.8e+308"), + JFloat.valueOf("0.0"), + null + ) + val js = sj[SampleJNumber].toJson(inst) + js should matchJson("""{"n1":-128,"n2":127,"n3":-32768,"n4":32767,"n5":-2147483648,"n6":2147483647,"n7":-9223372036854775808,"n8":9223372036854755807,"n9":null,"n10":0,"n11":3.4E-38,"n12":3.4E38,"n13":1.7E-308,"n14":1.7E308,"n15":null,"n16":0.0,"n17":null}""") + // inst shouldEqual ScalaJack.read[SampleJNumber](js) + } + + it("Short must work") { + val inst = SampleJShort( + JShort.MAX_VALUE, + JShort.MIN_VALUE, + 0.asInstanceOf[Short], + 123.asInstanceOf[Short], + null + ) + val js = sj[SampleJShort].toJson(inst) + js should matchJson("""{"s1":32767,"s2":-32768,"s3":0,"s4":123,"s5":null}""") + // inst shouldEqual ScalaJack.read[SampleJShort](js) + } + } + } + +/* + + //-------------------------------------------------------- + + + test("BigDecimal must break") { + describe("--- Negative Tests ---") + val js = + """{"bd1":0,"bd2":1,"bd3":10,"bd4":"0.1499999999999999944488848768742172978818416595458984375","bd5":null}""".asInstanceOf[JSON] + val msg = + """Expected a Number here + |{"bd1":0,"bd2":1,"bd3":10,"bd4":"0.149999999999999994448884876874217297881841... + |--------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJBigDecimal](js) + } + } + + test("BigInt must break") { + val js = + """{"bi1":"0","bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":90182736451928374653345,"bi6":0,"bi7":null}""".asInstanceOf[JSON] + val msg = + """Expected a Number here + |{"bi1":"0","bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":901827364519... + |-------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJBigInteger](js) + } + } + + test("Boolean must break") { + val js = """{"bool1":true,"bool2":false,"bool3":true,"bool4":"false","bool5":null}""".asInstanceOf[JSON] + val msg = + """Expected a Boolean here + |{"bool1":true,"bool2":false,"bool3":true,"bool4":"false","bool5":null} + |-------------------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJBoolean](js) + } + } + + test("Byte must break") { + val js = """{"b1":127,"b2":-128,"b3":false,"b4":64,"b5":null}""".asInstanceOf[JSON] + val msg = """Expected a Number here + |{"b1":127,"b2":-128,"b3":false,"b4":64,"b5":null} + |-------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJByte](js) + } + } + + test("Char must break") { + val js = """{"c1":"Z","c2":3,"c3":null}""".asInstanceOf[JSON] + val msg = """Expected a String here + |{"c1":"Z","c2":3,"c3":null} + |---------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJChar](js) + } + val js2 = """{"c1":"Z","c2":"","c3":null}""".asInstanceOf[JSON] + val msg2 = """Tried to read a Character but empty string found + |{"c1":"Z","c2":"","c3":null} + |----------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleJChar](js2) + } + } + + test("Double must break") { + val js = + """{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":"0.0","d4":-123.4567,"d5":null}""".asInstanceOf[JSON] + val msg = + """Expected a Number here + |{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":"0.0","d4":-123.4567,"d5":null} + |------------------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJDouble](js) + } + } + + test("Float must break") { + val js = + """{"f1":3.4028235E38,"f2":"1.4E-45","f3":0.0,"f4":-123.4567,"f5":null}""".asInstanceOf[JSON] + val msg = + """Expected a Number here + |{"f1":3.4028235E38,"f2":"1.4E-45","f3":0.0,"f4":-123.4567,"f5":null} + |------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJFloat](js) + } + } + + test("Int must break") { + val js = """{"i1":2147483647,"i2":-2147483648,"i3":false,"i4":123,"i5":null}""".asInstanceOf[JSON] + val msg = + """Expected a Number here + |{"i1":2147483647,"i2":-2147483648,"i3":false,"i4":123,"i5":null} + |---------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJInt](js) + } + val js2 = """{"i1":2147483647,"i2":-2147483648,"i3":0.3,"i4":123,"i5":null}""".asInstanceOf[JSON] + interceptMessage[java.lang.NumberFormatException]("For input string: \"0.3\""){ + sj.read[SampleJInt](js2) + } + } + + test("Long must break") { + val js = + """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":"0","l4":123,"l5":null}""".asInstanceOf[JSON] + val msg = + """Expected a Number here + |...23372036854775807,"l2":-9223372036854775808,"l3":"0","l4":123,"l5":null} + |----------------------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJLong](js) + } + val js2 = + """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123,"l5":null}""".asInstanceOf[JSON] + interceptMessage[java.lang.NumberFormatException]("For input string: \"0.3\""){ + sj.read[SampleJLong](js2) + } + } + + test("Number must break") { + val js = """{"n1":-128,"n2":127,"n3":"-32768","n4":32767,"n5":-2147483648,"n6":2147483647,"n7":-9223372036854775808,"n8":9223372036854755807,"n9":9923372036854755810,"n10":0,"n11":3.4E-38,"n12":3.4E38,"n13":1.7E-308,"n14":1.7E308,"n15":1.8E+308,"n16":0.0,"n17":null}""".asInstanceOf[JSON] + val msg = + """Expected a Number here + |{"n1":-128,"n2":127,"n3":"-32768","n4":32767,"n5":-2147483648,"n6":2147483647... + |-------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJNumber](js) + } + } + + test("Short must break") { + val js = """{"s1":false,"s2":-32768,"s3":0,"s4":123,"s5":null}""".asInstanceOf[JSON] + val msg = """Expected a Number here + |{"s1":false,"s2":-32768,"s3":0,"s4":123,"s5":null} + |------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleJShort](js) + } + val js2 = """{"s1":2.3,"s2":-32768,"s3":0,"s4":123,"s5":null}""".asInstanceOf[JSON] + interceptMessage[java.lang.NumberFormatException]("For input string: \"2.3\""){ + sj.read[SampleJShort](js2) + } + } +*/ \ No newline at end of file diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala b/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala index ae207055..ee156366 100644 --- a/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala +++ b/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala @@ -7,15 +7,6 @@ import java.math.{BigDecimal as JBigDecimal, BigInteger as JBigInteger} import java.time.* import scala.math.* -// === Scala -case class SampleBigDecimal(bd1: BigDecimal, bd2: BigDecimal, bd3: BigDecimal, bd4: BigDecimal, bd5: BigDecimal, bd6: BigDecimal) -case class SampleBigInt(bi1: BigInt, bi2: BigInt, bi3: BigInt, bi4: BigInt) -case class SampleBinary(b1: Array[Byte], b2: Array[Byte]) -case class SampleBoolean(bool1: Boolean, bool2: Boolean) -case class SampleByte(b1: Byte, b2: Byte, b3: Byte, b4: Byte) -case class SampleChar(c1: Char, c2: Char, c3: Char) -case class SampleDouble(d1: Double, d2: Double, d3: Double, d4: Double) - object Size extends Enumeration { val Small, Medium, Large = Value } @@ -44,6 +35,13 @@ case class Plane(numberOfEngines: Int) extends Vehicle case class Ride(wheels: Vehicle) case class Favorite(flavor: Flavor) +// === Scala +case class SampleBigDecimal(bd1: BigDecimal, bd2: BigDecimal, bd3: BigDecimal, bd4: BigDecimal, bd5: BigDecimal, bd6: BigDecimal) +case class SampleBigInt(bi1: BigInt, bi2: BigInt, bi3: BigInt, bi4: BigInt) +case class SampleBoolean(bool1: Boolean, bool2: Boolean) +case class SampleByte(b1: Byte, b2: Byte, b3: Byte, b4: Byte) +case class SampleChar(c1: Char, c2: Char, c3: Char) +case class SampleDouble(d1: Double, d2: Double, d3: Double, d4: Double) case class SampleFloat(f1: Float, f2: Float, f3: Float, f4: Float) case class SampleInt(i1: Int, i2: Int, i3: Int, i4: Int) case class SampleLong(l1: Long, l2: Long, l3: Long, l4: Long) @@ -70,10 +68,16 @@ case class SampleInstant(i1: Instant, i2: Instant, i3: Instant, i4: Instant, i5: case class SampleLocalDateTime(d1: LocalDateTime, d2: LocalDateTime, d3: LocalDateTime, d4: LocalDateTime) case class SampleLocalDate(d1: LocalDate, d2: LocalDate, d3: LocalDate, d4: LocalDate) case class SampleLocalTime(d1: LocalTime, d2: LocalTime, d3: LocalTime, d4: LocalTime, d5: LocalTime, d6: LocalTime) +case class SampleMonthDay(m1: MonthDay, m2: MonthDay) case class SampleOffsetDateTime(o1: OffsetDateTime, o2: OffsetDateTime, o3: OffsetDateTime, o4: OffsetDateTime) case class SampleOffsetTime(o1: OffsetTime, o2: OffsetTime, o3: OffsetTime, o4: OffsetTime) case class SamplePeriod(p1: Period, p2: Period, p3: Period) +case class SampleYear(y1: Year, y2: Year, y3: Year, y4: Year) +case class SampleYearMonth(y1: YearMonth, y2: YearMonth) case class SampleZonedDateTime(o1: ZonedDateTime, o2: ZonedDateTime) +case class SampleZoneId(z1: ZoneId, z2: ZoneId) +case class SampleZoneOffset(z1: ZoneOffset, z2: ZoneOffset) +// TODO: Missing Year, MonthYear, ZoneId, ZoneOffset, others? // === Any primitives case class AnyShell(a: Any) diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrim.scala b/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrim.scala index b0eb163b..e3cdc112 100644 --- a/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrim.scala +++ b/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrim.scala @@ -2,12 +2,11 @@ package co.blocke.scalajack package json package primitives +import ScalaJack.* import co.blocke.scala_reflection.* import scala.math.BigDecimal import java.util.UUID import TestUtil.* -// import munit.* -// import munit.internal.console import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers.* import org.scalatest.* @@ -28,9 +27,9 @@ class ScalaPrim() extends AnyFunSpec with JsonMatchers: null ) - val js = ScalaJack.write(inst) + val js = sj[SampleBigDecimal].toJson(inst) js should matchJson("""{"bd1":123,"bd2":1.23,"bd3":0,"bd4":123.456,"bd5":0.1499999999999999944488848768742172978818416595458984375,"bd6":null}""") - inst shouldEqual ScalaJack.read[SampleBigDecimal](js) + // inst shouldEqual ScalaJack.read[SampleBigDecimal](js) } it("BigInt must work") { @@ -40,88 +39,77 @@ class ScalaPrim() extends AnyFunSpec with JsonMatchers: BigInt(0), null ) - val js = ScalaJack.write(inst) + val js = sj[SampleBigInt].toJson(inst) js should matchJson("""{"bi1":-90182736451928374653345,"bi2":90182736451928374653345,"bi3":0,"bi4":null}""") - inst shouldEqual ScalaJack.read[SampleBigInt](js) - } - - it("Binary must work") { - val inst = SampleBinary( - null, - hexStringToByteArray("e04fd020ea3a6910a2d808002b30309d") - ) - val js = ScalaJack.write(inst) - val inst2 = ScalaJack.read[SampleBinary](js) - js should matchJson("""{"b1":null,"b2":[-32,79,-48,32,-22,58,105,16,-94,-40,8,0,43,48,48,-99]}""") - inst2.b1 shouldEqual null - inst.b2.toList shouldEqual inst2.b2.toList + // inst shouldEqual ScalaJack.read[SampleBigInt](js) } it("Boolean must work (not nullable)") { val inst = SampleBoolean(bool1 = true, bool2 = false) - val js = ScalaJack.write(inst) + val js = sj[SampleBoolean].toJson(inst) js should matchJson("""{"bool1":true,"bool2":false}""") - inst shouldEqual ScalaJack.read[SampleBoolean](js) + // inst shouldEqual ScalaJack.read[SampleBoolean](js) } it("Byte must work (not nullable)") { val inst = SampleByte(Byte.MaxValue, Byte.MinValue, 0, 64) - val js = ScalaJack.write(inst) + val js = sj[SampleByte].toJson(inst) js should matchJson("""{"b1":127,"b2":-128,"b3":0,"b4":64}""") - inst shouldEqual ScalaJack.read[SampleByte](js) + // inst shouldEqual ScalaJack.read[SampleByte](js) } it("Char must work (not nullable)") { val inst = SampleChar(Char.MaxValue, 'Z', '\u20A0') - val js = ScalaJack.write(inst) + val js = sj[SampleChar].toJson(inst) js should matchJson("""{"c1":"\""" + """uffff","c2":"Z","c3":"\""" + """u20a0"}""") - inst shouldEqual ScalaJack.read[SampleChar](js) + // inst shouldEqual ScalaJack.read[SampleChar](js) } it("Double must work (not nullable)") { val inst = SampleDouble(Double.MaxValue, Double.MinValue, 0.0, -123.4567) - val js = ScalaJack.write(inst) + val js = sj[SampleDouble].toJson(inst) js should matchJson("""{"d1":1.7976931348623157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123.4567}""") - inst shouldEqual ScalaJack.read[SampleDouble](js) + // inst shouldEqual ScalaJack.read[SampleDouble](js) } - it("Float must work") { + it("Float must work (not nullable)") { val inst = SampleFloat(Float.MaxValue, Float.MinValue, 0.0f, -123.4567f) - val js = ScalaJack.write(inst) + val js = sj[SampleFloat].toJson(inst) js should matchJson("""{"f1":3.4028235E38,"f2":-3.4028235E38,"f3":0.0,"f4":-123.4567}""") - inst shouldEqual ScalaJack.read[SampleFloat](js) + // inst shouldEqual ScalaJack.read[SampleFloat](js) } it("Int must work (not nullable)") { val inst = SampleInt(Int.MaxValue, Int.MinValue, 0, 123) - val js = ScalaJack.write(inst) + val js = sj[SampleInt].toJson(inst) js should matchJson("""{"i1":2147483647,"i2":-2147483648,"i3":0,"i4":123}""") - inst shouldEqual ScalaJack.read[SampleInt](js) + // inst shouldEqual ScalaJack.read[SampleInt](js) } it("Long must work (not nullable)") { val inst = SampleLong(Long.MaxValue, Long.MinValue, 0L, 123L) - val js = ScalaJack.write(inst) + val js = sj[SampleLong].toJson(inst) js should matchJson("""{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0,"l4":123}""") - inst shouldEqual ScalaJack.read[SampleLong](js) + // inst shouldEqual ScalaJack.read[SampleLong](js) } it("Short must work (not nullable)") { val inst = SampleShort(Short.MaxValue, Short.MinValue, 0, 123) - val js = ScalaJack.write(inst) + val js = sj[SampleShort].toJson(inst) js should matchJson("""{"s1":32767,"s2":-32768,"s3":0,"s4":123}""") - inst shouldEqual ScalaJack.read[SampleShort](js) + // inst shouldEqual ScalaJack.read[SampleShort](js) } it("String must work") { val inst = SampleString("something\b\n\f\r\t☆", "", null) - val js = ScalaJack.write(inst) + val js = sj[SampleString].toJson(inst) // The weird '+' here is to break up the unicode so it won't be interpreted and wreck the test. js should matchJson("""{"s1":"something\b\n\f\r\t\""" + """u2606","s2":"","s3":null}""") - inst shouldEqual ScalaJack.read[SampleString](js) + // inst shouldEqual ScalaJack.read[SampleString](js) } + /* it("UUID must work") { val inst = SampleUUID( null, @@ -298,5 +286,6 @@ class ScalaPrim() extends AnyFunSpec with JsonMatchers: val thrown = the[JsonParseError] thrownBy ScalaJack.read[SampleUUID](js) thrown.getMessage should equal(msg) } + */ } } diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/Simple.scala b/src/test/scala/co.blocke.scalajack/json/primitives/Simple.scala new file mode 100644 index 00000000..a46e9939 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/primitives/Simple.scala @@ -0,0 +1,357 @@ +package co.blocke.scalajack +package json +package primitives + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* +import java.time._ +import java.util.UUID + +class Simple() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-----------------------\n: Simple Type Tests :\n-----------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Duration must work") { + val inst = SampleDuration(Duration.ZERO, Duration.parse("P2DT3H4M"), null) + val js = sj[SampleDuration].toJson(inst) + js should matchJson("""{"d1":"PT0S","d2":"PT51H4M","d3":null}""") + // inst shouldEqual ScalaJack.read[SampleDuration](js) + } + + it("Instant must work") { + val inst = SampleInstant( + Instant.EPOCH, + Instant.MAX, + Instant.MIN, + Instant.parse("2007-12-03T10:15:30.00Z"), + null + ) + val js = sj[SampleInstant].toJson(inst) + js should matchJson("""{"i1":"1970-01-01T00:00:00Z","i2":"+1000000000-12-31T23:59:59.999999999Z","i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""") + // inst shouldEqual ScalaJack.read[SampleInstant](js) + } + + it("LocalDate must work") { + val inst = SampleLocalDate( + LocalDate.MAX, + LocalDate.MIN, + LocalDate.parse("2007-12-03"), + null + ) + val js = sj[SampleLocalDate].toJson(inst) + js should matchJson("""{"d1":"+999999999-12-31","d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""") + // inst shouldEqual ScalaJack.read[SampleLocalDate](js) + } + + it("LocalDateTime must work") { + val inst = SampleLocalDateTime( + LocalDateTime.MAX, + LocalDateTime.MIN, + LocalDateTime.parse("2007-12-03T10:15:30"), + null + ) + val js = sj[SampleLocalDateTime].toJson(inst) + js should matchJson("""{"d1":"+999999999-12-31T23:59:59.999999999","d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null}""") + // inst shouldEqual ScalaJack.read[SampleLocalDateTime](js) + } + + it("LocalTime must work") { + val inst = SampleLocalTime( + LocalTime.MAX, + LocalTime.MIN, + LocalTime.MIDNIGHT, + LocalTime.NOON, + LocalTime.parse("10:15:30"), + null + ) + val js = sj[SampleLocalTime].toJson(inst) + js should matchJson("""{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":"10:15:30","d6":null}""") + // inst shouldEqual ScalaJack.read[SampleLocalTime](js) + } + + it("MonthDay must work") { + val inst = SampleMonthDay( + MonthDay.of(7,1), + null + ) + val js = sj[SampleMonthDay].toJson(inst) + js should matchJson("""{"m1":"--07-01","m2":null}""") + // inst shouldEqual ScalaJack.read[SampleMonthDay](js) + } + + it("OffsetDateTime must work") { + val inst = SampleOffsetDateTime( + OffsetDateTime.MAX, + OffsetDateTime.MIN, + OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), + null + ) + val js = sj[SampleOffsetDateTime].toJson(inst) + js should matchJson("""{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":"-999999999-01-01T00:00:00+18:00","o3":"2007-12-03T10:15:30+01:00","o4":null}""") + // inst shouldEqual ScalaJack.read[SampleOffsetDateTime](js) + } + + it("OffsetTime must work") { + val inst = SampleOffsetTime( + OffsetTime.MAX, + OffsetTime.MIN, + OffsetTime.parse("10:15:30+01:00"), + null + ) + val js = sj[SampleOffsetTime].toJson(inst) + js should matchJson("""{"o1":"23:59:59.999999999-18:00","o2":"00:00:00+18:00","o3":"10:15:30+01:00","o4":null}""") + // inst shouldEqual ScalaJack.read[SampleOffsetTime](js) + } + + it("Period must work") { + val inst = SamplePeriod(Period.ZERO, Period.parse("P1Y2M3D"), null) + val js = sj[SamplePeriod].toJson(inst) + js should matchJson("""{"p1":"P0D","p2":"P1Y2M3D","p3":null}""") + // inst shouldEqual ScalaJack.read[SamplePeriod](js) + } + + it("Year must work") { + val inst = SampleYear(Year.of(Year.MAX_VALUE), Year.of(Year.MIN_VALUE), Year.parse("2020"), null) + val js = sj[SampleYear].toJson(inst) + js should matchJson("""{"y1":"999999999","y2":"-999999999","y3":"2020","y4":null}""") + // inst shouldEqual ScalaJack.read[SampleYear](js) + } + + it("YearMonth must work") { + val inst = SampleYearMonth(YearMonth.of(2020,7), null) + val js = sj[SampleYearMonth].toJson(inst) + js should matchJson("""{"y1":"2020-07","y2":null}""") + // inst shouldEqual ScalaJack.read[SampleYearMonth](js) + } + + it("ZonedDateTime must work") { + val inst = SampleZonedDateTime( + ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), + null + ) + val js = sj[SampleZonedDateTime].toJson(inst) + js should matchJson("""{"o1":"2007-12-03T10:15:30+01:00[Europe/Paris]","o2":null}""") + // inst shouldEqual ScalaJack.read[SampleZonedDateTime](js) + } + + it("ZonedId must work") { + val inst = SampleZoneId( + ZoneId.of("America/Puerto_Rico"), + null + ) + val js = sj[SampleZoneId].toJson(inst) + js should matchJson("""{"z1":"America/Puerto_Rico","z2":null}""") + // inst shouldEqual ScalaJack.read[SampleZoneId](js) + } + + it("ZoneOffset must work") { + val ldt = LocalDateTime.parse("2007-12-03T10:15:30") + val zone = ZoneId.of("Europe/Berlin") + val zoneOffSet = zone.getRules().getOffset(ldt) + val inst = SampleZoneOffset( + null, + zoneOffSet + ) + val js = sj[SampleZoneOffset].toJson(inst) + js should matchJson("""{"z1":null,"z2":"+01:00"}""") + // inst shouldEqual ScalaJack.read[SampleZoneOffset](js) + } + + it("UUID must work") { + val inst = SampleUUID( + null, + UUID.fromString("580afe0d-81c0-458f-9e09-4486c7af0fe9") + ) + val js = sj[SampleUUID].toJson(inst) + js should matchJson("""{"u1":null,"u2":"580afe0d-81c0-458f-9e09-4486c7af0fe9"}""") + // inst shouldEqual ScalaJack.read[SampleUUID](js) + } + } + } + +/* + + + test("Duration must break") { + describe("--- Negative Tests ---") + + val js = """{"d1":"PT0S","d2":21,"d3":null}""".asInstanceOf[JSON] + val msg = """Expected a String here + |{"d1":"PT0S","d2":21,"d3":null} + |------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleDuration](js) + } + val js2 = """{"d1":"PT0S","d2":"bogus","d3":null}""".asInstanceOf[JSON] + val msg2 = """Failed to parse Duration from input 'bogus' + |{"d1":"PT0S","d2":"bogus","d3":null} + |------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleDuration](js2) + } + } + + test("Instant must break") { + val js = + """{"i1":"1970-01-01T00:00:00Z","i2":false,"i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""".asInstanceOf[JSON] + val msg = + """Expected a String here + |{"i1":"1970-01-01T00:00:00Z","i2":false,"i3":"-1000000000-01-01T00:00:00Z","i... + |----------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleInstant](js) + } + val js2 = + """{"i1":"1970-01-01T00:00:00Z","i2":"bogus","i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""".asInstanceOf[JSON] + val msg2 = + """Failed to parse Instant from input 'bogus' + |{"i1":"1970-01-01T00:00:00Z","i2":"bogus","i3":"-1000000000-01-01T00:00:00Z",... + |----------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleInstant](js2) + } + } + + test("LocalDateTime must break") { + val js = + """{"d1":-1,"d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null}""".asInstanceOf[JSON] + val msg = + """Expected a String here + |{"d1":-1,"d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null} + |------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleLocalDateTime](js) + } + val js2 = + """{"d1":"bogus","d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d1":null}""".asInstanceOf[JSON] + val msg2 = + """Failed to parse LocalDateTime from input 'bogus' + |{"d1":"bogus","d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d1... + |------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleLocalDateTime](js2) + } + } + + test("LocalDate must break") { + val js = + """{"d1":-1,"d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""".asInstanceOf[JSON] + val msg = """Expected a String here + |{"d1":-1,"d2":"-999999999-01-01","d3":"2007-12-03","d4":null} + |------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleLocalDate](js) + } + val js2 = + """{"d1":"bogus","d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""".asInstanceOf[JSON] + val msg2 = + """Failed to parse LocalDate from input 'bogus' + |{"d1":"bogus","d2":"-999999999-01-01","d3":"2007-12-03","d4":null} + |------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleLocalDate](js2) + } + } + + test("LocalTime must break") { + val js = + """{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":false,"d6":null}""".asInstanceOf[JSON] + val msg = + """Expected a String here + |...:"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":false,"d6":null} + |----------------------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleLocalTime](js) + } + val js2 = + """{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":"Bogus","d6":null}""".asInstanceOf[JSON] + val msg2 = + """Failed to parse LocalTime from input 'Bogus' + |...0:00","d3":"00:00:00","d4":"12:00:00","d5":"Bogus","d6":null} + |----------------------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleLocalTime](js2) + } + } + + test("OffsetDateTime must break") { + val js = + """{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":2,"o3":"2007-12-03T10:15:30+01:00","o4":null}""".asInstanceOf[JSON] + val msg = + """Expected a String here + |..."+999999999-12-31T23:59:59.999999999-18:00","o2":2,"o3":"2007-12-03T10:15:30... + |----------------------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleOffsetDateTime](js) + } + val js2 = + """{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":"-999999999-01T00:00:00+18:00","o3":"2007-12-03T10:15:30+01:00","o4":null}""".asInstanceOf[JSON] + val msg2 = + """Failed to parse OffsetDateTime from input '-999999999-01T00:00:00+18:00' + |...9999999-18:00","o2":"-999999999-01T00:00:00+18:00","o3":"2007-12-03T10:15:30... + |----------------------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleOffsetDateTime](js2) + } + } + + test("OffsetTime must break") { + val js = + """{"o1":"23:59:59.999999999-18:00","o2":false,"o3":"10:15:30+01:00","o4":null}""".asInstanceOf[JSON] + val msg = + """Expected a String here + |{"o1":"23:59:59.999999999-18:00","o2":false,"o3":"10:15:30+01:00","o4":null} + |--------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleOffsetTime](js) + } + val js2 = + """{"o1":"23:59:59.999999999-18:00","o2":"00:00:00:00+18:00","o3":"10:15:30+01:00","o4":null}""".asInstanceOf[JSON] + val msg2 = + """Failed to parse OffsetTime from input '00:00:00:00+18:00' + |...23:59:59.999999999-18:00","o2":"00:00:00:00+18:00","o3":"10:15:30+01:00","o4... + |----------------------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleOffsetTime](js2) + } + } + + test("Period must break") { + val js = """{"p1":"P0D","p2":5,"p3":null}""".asInstanceOf[JSON] + val msg = """Expected a String here + |{"p1":"P0D","p2":5,"p3":null} + |-----------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SamplePeriod](js) + } + val js2 = """{"p1":"P0D","p2":"bogus","p3":null}""".asInstanceOf[JSON] + val msg2 = """Failed to parse Period from input 'bogus' + |{"p1":"P0D","p2":"bogus","p3":null} + |-----------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SamplePeriod](js2) + } + } + + test("ZonedDateTime must break") { + val js = """{"o1":true,"o2":null}""".asInstanceOf[JSON] + val msg = """Expected a String here + |{"o1":true,"o2":null} + |------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ + sj.read[SampleZonedDateTime](js) + } + val js2 = """{"o1":"2007-12-03T10:15:30+01:00 Earth","o2":null}""".asInstanceOf[JSON] + val msg2 = + """Failed to parse ZonedDateTime from input '2007-12-03T10:15:30+01:00 Earth' + |{"o1":"2007-12-03T10:15:30+01:00 Earth","o2":null} + |--------------------------------------^""".stripMargin + interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ + sj.read[SampleZonedDateTime](js2) + } + } +*/ \ No newline at end of file