From 3b9a37f18dbf92cecae24528e8d61c8b2ad10c3d Mon Sep 17 00:00:00 2001 From: Greg Zoller Date: Sun, 31 Mar 2024 00:19:21 -0500 Subject: [PATCH] fine tuning either and option --- build.sbt | 2 +- .../json/JsonCodecMaker.scala | 93 ++++++---- .../co.blocke.scalajack/json/JsonConfig.scala | 36 +--- .../json/reading/JsonSource.scala | 51 +++--- .../json/reading/Numbers.scala | 6 +- .../json/writing/AnyWriter.scala | 2 - .../json/writing/JsonOutput.scala | 5 + .../scala/co.blocke.scalajack/run/Play.scala | 26 ++- .../co.blocke.scalajack/run/Record.scala | 2 +- .../json/collections/MapSpec.scala | 30 +-- .../json/misc/LRSpec.scala | 171 +++++++++--------- .../co.blocke.scalajack/json/misc/Model.scala | 1 + .../json/misc/OptionSpec.scala | 49 ++++- .../json/primitives/ScalaPrimSpec.scala | 4 +- 14 files changed, 263 insertions(+), 215 deletions(-) diff --git a/build.sbt b/build.sbt index 441eac88..12a6de43 100644 --- a/build.sbt +++ b/build.sbt @@ -35,7 +35,7 @@ lazy val root = project Test / parallelExecution := false, scalafmtOnCompile := !isCI, libraryDependencies ++= Seq( - "co.blocke" %% "scala-reflection" % "2.0.3", + "co.blocke" %% "scala-reflection" % "fixOptionUnit_ea63ce", //"2.0.3", "org.apache.commons" % "commons-text" % "1.11.0", "io.github.kitlangton" %% "neotype" % "0.0.9", "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.24.5-SNAPSHOT", diff --git a/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala b/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala index 59419e1a..75b56595 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala @@ -10,7 +10,7 @@ import reading.JsonSource import scala.jdk.CollectionConverters.* import scala.quoted.* import scala.reflect.ClassTag -import scala.annotation.switch +import scala.annotation.{switch, tailrec} import scala.collection.Factory import scala.util.{Failure, Success, Try} import dotty.tools.dotc.ast.Trees.EmptyTree @@ -176,7 +176,6 @@ object JsonCodecMaker: $prefix $out.burpNull() } - case TryPolicy.NO_WRITE => '{ () } case TryPolicy.ERR_MSG_STRING => '{ $prefix @@ -193,13 +192,6 @@ object JsonCodecMaker: t.rightRef.refType match case '[rt] => cfg.eitherLeftHandling match - case EitherLeftPolicy.NO_WRITE => - '{ - if $tin == null then $out.burpNull() - $tin match - case Left(_) => () - case Right(v) => ${ _maybeWrite[rt](prefix, '{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } - } case EitherLeftPolicy.AS_NULL => '{ if $tin == null then $out.burpNull() @@ -231,10 +223,12 @@ object JsonCodecMaker: '{ if $tin == null then $out.burpNull() $tin match - case Left(v) => ${ _maybeWrite[lt](prefix, '{ v.asInstanceOf[lt] }, t.leftRef.asInstanceOf[RTypeRef[lt]], out, cfg) } - case Right(v) => ${ _maybeWrite[rt](prefix, '{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } + case Left(v) => + ${ _maybeWrite[lt](prefix, '{ v.asInstanceOf[lt] }, t.leftRef.asInstanceOf[RTypeRef[lt]], out, cfg) } + case Right(v) => + ${ _maybeWrite[rt](prefix, '{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } } - case t: LeftRightRef[?] if !cfg.noneAsNull && t.lrkind != LRKind.EITHER && (t.leftRef.isInstanceOf[OptionRef[_]] || t.rightRef.isInstanceOf[OptionRef[_]]) => + case t: LeftRightRef[?] if t.lrkind != LRKind.EITHER => t.refType match case '[e] => t.rightRef.refType match @@ -243,14 +237,14 @@ object JsonCodecMaker: case '[lt] => val tin = aE.asExprOf[e] '{ - if $tin == None then () + if $tin == null then $out.burpNull() else $out.mark() scala.util.Try { ${ _maybeWrite[rt](prefix, '{ $tin.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } } match case scala.util.Success(_) => () - case scala.util.Failure(_) => + case scala.util.Failure(f) => $out.revert() ${ _maybeWrite[lt](prefix, '{ $tin.asInstanceOf[lt] }, t.leftRef.asInstanceOf[RTypeRef[lt]], out, cfg) } } @@ -616,15 +610,6 @@ object JsonCodecMaker: case Right(v) => ${ genWriteVal[rt]('{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, inTuple = inTuple) } } - case EitherLeftPolicy.NO_WRITE => - '{ - if $tin == null then $out.burpNull() - else - $tin match - case Left(v) => () - case Right(v) => - ${ genWriteVal[rt]('{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, inTuple = inTuple) } - } case EitherLeftPolicy.ERR_MSG_STRING => '{ if $tin == null then $out.burpNull() @@ -671,7 +656,6 @@ object JsonCodecMaker: cfg.tryFailureHandling match case _ if inTuple => '{ $out.burpNull() } case TryPolicy.AS_NULL => '{ $out.burpNull() } - case TryPolicy.NO_WRITE => '{ () } case TryPolicy.ERR_MSG_STRING => '{ $out.value("Try Failure with msg: " + v.getMessage()) } case TryPolicy.THROW_EXCEPTION => '{ throw v } } @@ -860,6 +844,16 @@ object JsonCodecMaker: // --------------------------------------------------------------------------------------------- + def lrHasOptionChild(lr: LeftRightRef[?]): String = + lr.rightRef match + case t: OptionRef[?] => "r" + case t: LeftRightRef[?] => "r" + lrHasOptionChild(t) + case _ => + lr.leftRef match + case t: OptionRef[?] => "l" + case t: LeftRightRef[?] => "l" + lrHasOptionChild(t) + case _ => "" + def genDecFnBody[T: Type](r: RTypeRef[?], in: Expr[JsonSource])(using Quotes): Expr[Unit] = import quotes.reflect.* @@ -905,11 +899,31 @@ object JsonCodecMaker: // if dvMembers.isEmpty then (ValDef(sym, Some(oneField.fieldRef.unitVal.asTerm)), caseDef, fieldSymRef) if dvMembers.isEmpty then // no default... required? Not if Option/Optional, or a collection - oneField.fieldRef match { - case _: OptionRef[?] => // not required - case _ => required = required | math.pow(2, oneField.index).toInt // required + val unitVal = oneField.fieldRef match { + case _: OptionRef[?] => + oneField.fieldRef.unitVal.asTerm // not required + case r: LeftRightRef[?] if r.lrkind == LRKind.EITHER => // maybe required + val optionRecipe = lrHasOptionChild(r) + if optionRecipe.length == 0 then + required = required | math.pow(2, oneField.index).toInt // required + oneField.fieldRef.unitVal.asTerm + else + val recipeE = Expr(optionRecipe) + '{ + $recipeE.foldRight(None: Any)((c, acc) => if c == 'r' then Right(acc) else Left(acc)).asInstanceOf[f] + }.asTerm + case r: LeftRightRef[?] => // maybe required + val optionRecipe = lrHasOptionChild(r) + if optionRecipe.length == 0 then // no Option children -> required + required = required | math.pow(2, oneField.index).toInt // required + oneField.fieldRef.unitVal.asTerm + else // at least one Option child -> optional + '{ None }.asTerm + case _ => + required = required | math.pow(2, oneField.index).toInt // required + oneField.fieldRef.unitVal.asTerm } - (ValDef(sym, Some(oneField.fieldRef.unitVal.asTerm)), caseDef, fieldSymRef) + (ValDef(sym, Some(unitVal)), caseDef, fieldSymRef) else val methodSymbol = dvMembers.head val dvSelectNoTArgs = Ref(companionModule).select(methodSymbol) @@ -1010,14 +1024,14 @@ object JsonCodecMaker: '{ $in.expectString() match case null => - $in.retract() - $in.retract() - $in.retract() - $in.retract() + $in.backspace() + $in.backspace() + $in.backspace() + $in.backspace() throw JsonParseError("Char value cannot be null", $in) case "" => - $in.retract() - $in.retract() + $in.backspace() + $in.backspace() throw JsonParseError("Char value expected but empty string found in json", $in) case c => c.charAt(0) }.asExprOf[T] @@ -1086,8 +1100,8 @@ object JsonCodecMaker: val c = $in.expectString() if c == null then null else if c.length == 0 then - $in.retract() - $in.retract() + $in.backspace() + $in.backspace() throw JsonParseError("Character value expected but empty string found in json", $in) else java.lang.Character.valueOf(c.charAt(0)) }.asExprOf[T] @@ -1257,10 +1271,11 @@ object JsonCodecMaker: case Success(rval) => Right(rval) case Failure(f) => + $in.revertToMark() scala.util.Try(${ genReadVal[l](t.leftRef.asInstanceOf[RTypeRef[l]], in, inTuple) }) match case Success(lval) => Left(lval) case Failure(_) => - $in.retract() + $in.backspace() throw JsonParseError("Failed to read either side of Either type", $in) }.asExprOf[T] @@ -1271,13 +1286,15 @@ object JsonCodecMaker: t.rightRef.refType match case '[r] => '{ + $in.mark() scala.util.Try(${ genReadVal[l](t.leftRef.asInstanceOf[RTypeRef[l]], in, true) }) match case Success(lval) => lval case Failure(f) => + $in.revertToMark() scala.util.Try(${ genReadVal[r](t.rightRef.asInstanceOf[RTypeRef[r]], in, true) }) match case Success(rval) => rval case Failure(_) => - $in.retract() + $in.backspace() throw JsonParseError("Failed to read either side of Union type", $in) }.asExprOf[T] /* diff --git a/src/main/scala/co.blocke.scalajack/json/JsonConfig.scala b/src/main/scala/co.blocke.scalajack/json/JsonConfig.scala index 24845fb2..4952ed9d 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonConfig.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonConfig.scala @@ -8,11 +8,9 @@ import scala.quoted.* class JsonConfig private[scalajack] ( val noneAsNull: Boolean, - // val forbidNullsInInput: Boolean = false, val tryFailureHandling: TryPolicy, val eitherLeftHandling: EitherLeftPolicy, // val undefinedFieldHandling: UndefinedValueOption = UndefinedValueOption.THROW_EXCEPTION, - // val _allowQuotedPrimitives: Boolean, val writeNonConstructorFields: Boolean, // -------------------------- val typeHintLabel: String, @@ -31,7 +29,6 @@ class JsonConfig private[scalajack] ( def withEnumsAsIds(asIds: Option[List[String]]): JsonConfig = copy(enumsAsIds = asIds) def suppressEscapedStrings(): JsonConfig = copy(_suppressEscapedStrings = true) def suppressTypeHints(): JsonConfig = copy(_suppressTypeHints = true) - // def allowQuotedPrimitives(): JsonConfig = copy(_allowQuotedPrimitives = true) private[this] def copy( noneAsNull: Boolean = noneAsNull, @@ -43,12 +40,10 @@ class JsonConfig private[scalajack] ( enumsAsIds: Option[List[String]] = enumsAsIds, _suppressEscapedStrings: Boolean = _suppressEscapedStrings, _suppressTypeHints: Boolean = _suppressTypeHints - // _allowQuotedPrimitives: Boolean = _allowQuotedPrimitives ): JsonConfig = new JsonConfig( noneAsNull, tryFailureHandling, eitherLeftHandling, - // _allowQuotedPrimitives, writeNonConstructorFields, typeHintLabel, typeHintPolicy, @@ -58,10 +53,10 @@ class JsonConfig private[scalajack] ( ) enum TryPolicy: - case AS_NULL, NO_WRITE, ERR_MSG_STRING, THROW_EXCEPTION + case AS_NULL, ERR_MSG_STRING, THROW_EXCEPTION enum EitherLeftPolicy: - case AS_VALUE, AS_NULL, NO_WRITE, ERR_MSG_STRING, THROW_EXCEPTION + case AS_VALUE, AS_NULL, ERR_MSG_STRING, THROW_EXCEPTION // enum UndefinedValueOption: // case AS_NULL, AS_SYMBOL, THROW_EXCEPTION @@ -72,9 +67,8 @@ enum TypeHintPolicy: object JsonConfig extends JsonConfig( noneAsNull = false, - tryFailureHandling = TryPolicy.NO_WRITE, - eitherLeftHandling = EitherLeftPolicy.NO_WRITE, - // _allowQuotedPrimitives = false writeNonConstructorFields = true, + tryFailureHandling = TryPolicy.AS_NULL, + eitherLeftHandling = EitherLeftPolicy.AS_VALUE, writeNonConstructorFields = false, typeHintLabel = "_hint", typeHintPolicy = TypeHintPolicy.SIMPLE_CLASSNAME, @@ -94,7 +88,6 @@ object JsonConfig .withTypeHintLabel(${ Expr(x.typeHintLabel) }) .withTypeHintPolicy(${ Expr(x.typeHintPolicy) }) .withEnumsAsIds(${ Expr(x.enumsAsIds) }) - // .allowQuotedPrimitives(${ Expr(x._allowQuotedPrimitives) }) val jc2 = ${ if x.noneAsNull then '{ jc.withNoneAsNull() } else '{ jc } @@ -124,12 +117,9 @@ object JsonConfig case '{ JsonConfig( $noneAsNullE, - // $forbitNullsInInputE, $tryFailureHandlerE, $eitherLeftHandlerE, - // $allowQuotedPrimitivesE, // $undefinedFieldHandlingE, - // $permissivePrimitivesE, $writeNonConstructorFieldsE, $typeHintLabelE, $typeHintPolicyE, @@ -142,14 +132,9 @@ object JsonConfig Some( JsonConfig( extract("noneAsNull", noneAsNullE), - // extract("forbitNullsInInput", forbitNullsInInputE), extract("tryFailureHandler", tryFailureHandlerE), extract("eitherLeftHandler", eitherLeftHandlerE), - // extract("_allowQuotedPrimitives", allowQuotedPrimtiviesE), - // extract("undefinedFieldHandling", undefinedFieldHandlingE), - // extract("permissivePrimitives", permissivePrimitivesE), extract("writeNonConstructorFields", writeNonConstructorFieldsE), - // extract2[String]("typeHintLabel", x) extract("typeHintLabel", typeHintLabelE), extract("typeHintPolicy", typeHintPolicyE), extract("enumsAsIds", enumsAsIdsE), @@ -162,11 +147,10 @@ object JsonConfig println("ERROR: " + x.getMessage) None } - case '{ JsonConfig } => Some(JsonConfig) - case '{ ($x: JsonConfig).withNoneAsNull() } => Some(x.valueOrAbort.withNoneAsNull()) - case '{ ($x: JsonConfig).withTryFailureHandling($v) } => Some(x.valueOrAbort.withTryFailureHandling(v.valueOrAbort)) - case '{ ($x: JsonConfig).withEitherLeftHandling($v) } => Some(x.valueOrAbort.withEitherLeftHandling(v.valueOrAbort)) - // case '{ ($x: JsonConfig).allowQuotedPrimitives() } => Some(x.valueOrAbort.allowQuotedPrimities()) + case '{ JsonConfig } => Some(JsonConfig) + case '{ ($x: JsonConfig).withNoneAsNull() } => Some(x.valueOrAbort.withNoneAsNull()) + case '{ ($x: JsonConfig).withTryFailureHandling($v) } => Some(x.valueOrAbort.withTryFailureHandling(v.valueOrAbort)) + case '{ ($x: JsonConfig).withEitherLeftHandling($v) } => Some(x.valueOrAbort.withEitherLeftHandling(v.valueOrAbort)) case '{ ($x: JsonConfig).withWriteNonConstructorFields($v) } => Some(x.valueOrAbort.withWriteNonConstructorFields(v.valueOrAbort)) case '{ ($x: JsonConfig).withTypeHintLabel($v) } => Some(x.valueOrAbort.withTypeHintLabel(v.valueOrAbort)) case '{ ($x: JsonConfig).withTypeHintPolicy($v) } => Some(x.valueOrAbort.withTypeHintPolicy(v.valueOrAbort)) @@ -180,7 +164,6 @@ object JsonConfig x match case TryPolicy.AS_NULL => '{ TryPolicy.AS_NULL } case TryPolicy.ERR_MSG_STRING => '{ TryPolicy.ERR_MSG_STRING } - case TryPolicy.NO_WRITE => '{ TryPolicy.NO_WRITE } case TryPolicy.THROW_EXCEPTION => '{ TryPolicy.THROW_EXCEPTION } } @@ -190,7 +173,6 @@ object JsonConfig case EitherLeftPolicy.AS_VALUE => '{ EitherLeftPolicy.AS_VALUE } case EitherLeftPolicy.AS_NULL => '{ EitherLeftPolicy.AS_NULL } case EitherLeftPolicy.ERR_MSG_STRING => '{ EitherLeftPolicy.ERR_MSG_STRING } - case EitherLeftPolicy.NO_WRITE => '{ EitherLeftPolicy.NO_WRITE } case EitherLeftPolicy.THROW_EXCEPTION => '{ EitherLeftPolicy.THROW_EXCEPTION } } @@ -207,7 +189,6 @@ object JsonConfig import quotes.reflect.* x match case '{ TryPolicy.AS_NULL } => Some(TryPolicy.AS_NULL) - case '{ TryPolicy.NO_WRITE } => Some(TryPolicy.NO_WRITE) case '{ TryPolicy.ERR_MSG_STRING } => Some(TryPolicy.ERR_MSG_STRING) case '{ TryPolicy.THROW_EXCEPTION } => Some(TryPolicy.THROW_EXCEPTION) } @@ -218,7 +199,6 @@ object JsonConfig x match case '{ EitherLeftPolicy.AS_VALUE } => Some(EitherLeftPolicy.AS_VALUE) case '{ EitherLeftPolicy.AS_NULL } => Some(EitherLeftPolicy.AS_NULL) - case '{ EitherLeftPolicy.NO_WRITE } => Some(EitherLeftPolicy.NO_WRITE) case '{ EitherLeftPolicy.ERR_MSG_STRING } => Some(EitherLeftPolicy.ERR_MSG_STRING) case '{ EitherLeftPolicy.THROW_EXCEPTION } => Some(EitherLeftPolicy.THROW_EXCEPTION) } diff --git a/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala b/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala index 92ed70de..cbc8b0d9 100644 --- a/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala +++ b/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala @@ -27,9 +27,10 @@ case class JsonSource(js: CharSequence): inline def here = js.charAt(i) - inline def retract() = i -= 1 + inline def backspace() = i -= 1 inline def mark() = _mark = i + inline def revertToMark() = i = _mark inline def captureMark(): String = js.subSequence(_mark, i).toString @tailrec @@ -49,7 +50,7 @@ case class JsonSource(js: CharSequence): i += 1 if !(b == ' ' || b == '\n' || b == '\t' || (b | 0x4) == '\r') then if b != t then - retract() + backspace() throw JsonParseError(s"Expected '$t' here", this) else () else expectToken(t) @@ -59,7 +60,7 @@ case class JsonSource(js: CharSequence): readChars(JsonSource.ull, "null") true else - retract() + backspace() false // Enum... @@ -67,7 +68,7 @@ case class JsonSource(js: CharSequence): def expectEnum(): Int | String = readToken() match case t if t >= '0' && t <= '9' => - retract() + backspace() expectInt() case t if t == '"' => val endI = parseString(i) @@ -108,8 +109,8 @@ case class JsonSource(js: CharSequence): else throw new JsonParseError(s"Expected object field name but found '$tt'", this) else if t == '}' then None else - retract() - throw new JsonParseError(s"Expected ',' or '}' but found $t", this) + backspace() + throw new JsonParseError(s"Expected ',' or '}' but found '$t'", this) final def parseObjectKey(fieldNameMatrix: StringMatrix): Int = // returns index of field name or -1 if not found var fi: Int = 0 @@ -136,7 +137,7 @@ case class JsonSource(js: CharSequence): val value = vf() parseMap[K, V](kf, vf, acc + (key -> value), false) case _ if isFirst => - retract() + backspace() val key = kf() expectToken(':') val value = vf() @@ -147,23 +148,21 @@ case class JsonSource(js: CharSequence): // ======================================================= @tailrec - final private def addAllArray[E](s: scala.collection.mutable.ListBuffer[E], f: () => E): scala.collection.mutable.ListBuffer[E] = + final private def addAllArray[E](s: scala.collection.mutable.ListBuffer[E], f: () => E, isFirst: Boolean): scala.collection.mutable.ListBuffer[E] = if i == max then throw JsonParseError("Unexpected end of buffer", this) - s.addOne(f()) val tt = readToken() - if tt == ']' then - retract() - s - else if tt != ',' then throw JsonParseError(s"Expected ',' or ']' got '$tt'", this) - else addAllArray(s, f) + if tt == ']' then s + else if !isFirst && tt != ',' then throw JsonParseError(s"Expected ',' or ']' got '$tt'", this) + else + if isFirst then backspace() + s.addOne(f()) + addAllArray(s, f, false) def expectArray[E](f: () => E): scala.collection.mutable.ListBuffer[E] = val t = readToken() if t == '[' then val seq = scala.collection.mutable.ListBuffer.empty[E] - skipWS() - addAllArray(seq, f) - i += 1 + addAllArray(seq, f, true) seq else if t == 'n' then readChars(JsonSource.ull, "null") @@ -285,7 +284,7 @@ case class JsonSource(js: CharSequence): readChars(JsonSource.ull, "null") null.asInstanceOf[java.lang.Boolean] else - retract() + backspace() throw JsonParseError(s"Expected 'true', 'false', or null here", this) // Characters... @@ -324,12 +323,12 @@ case class JsonSource(js: CharSequence): def expectFloat(): Float = val result = UnsafeNumbers.float_(this, false, 32) - retract() + backspace() result def expectDouble(): Double = val result = UnsafeNumbers.double_(this, false, 64) - retract() + backspace() result def expectNumberOrNull(): String = @@ -341,7 +340,7 @@ case class JsonSource(js: CharSequence): readChars(JsonSource.ull, "null") null else - retract() + backspace() throw new JsonParseError("Expected a numerical value or null here", this) def expectInt(): Int = @@ -351,7 +350,7 @@ case class JsonSource(js: CharSequence): b = readChar() s = 0 if b < '0' || b > '9' then - retract() + backspace() throw JsonParseError("Non-numeric character found when integer value expected", this) var x = '0' - b while { b = readChar(); b >= '0' && b <= '9' } do @@ -364,14 +363,14 @@ case class JsonSource(js: CharSequence): x -= s if (s & x) == -2147483648 then throw JsonParseError("Integer value overflow", this) if (b | 0x20) == 'e' || b == '.' then - retract() + backspace() throw JsonParseError("Decimal digit 'e' or '.' found when integer value expected", this) - retract() + backspace() x def expectLong(): Long = val result = UnsafeNumbers.long_(this, false) - retract() + backspace() result // Skip things... @@ -399,7 +398,7 @@ case class JsonSource(js: CharSequence): @tailrec final def skipNumber(): Unit = - if !isNumber(readChar()) then retract() + if !isNumber(readChar()) then backspace() else skipNumber() @tailrec diff --git a/src/main/scala/co.blocke.scalajack/json/reading/Numbers.scala b/src/main/scala/co.blocke.scalajack/json/reading/Numbers.scala index aafeda44..0dc05c95 100644 --- a/src/main/scala/co.blocke.scalajack/json/reading/Numbers.scala +++ b/src/main/scala/co.blocke.scalajack/json/reading/Numbers.scala @@ -589,7 +589,7 @@ object UnsafeNumbers { } if !isDigit(current) then - in.retract() + in.backspace() throw JsonParseError("Unexpected character in Int/Long value: " + current.toChar, in) var accum: Long = 0L @@ -632,7 +632,7 @@ object UnsafeNumbers { while i < len do { current = in.readChar() if current != s(i) then - in.retract() + in.backspace() throw JsonParseError("Unexpected character in Int/Long value: " + current.toChar, in) i += 1 } @@ -817,7 +817,7 @@ object UnsafeNumbers { } if sig < 0 then - in.retract() + in.backspace() throw JsonParseError("Malformed Float/Double/BigDecimal", in) // no significand if current == 'E' || current == 'e' then exp = int_(in, consume) diff --git a/src/main/scala/co.blocke.scalajack/json/writing/AnyWriter.scala b/src/main/scala/co.blocke.scalajack/json/writing/AnyWriter.scala index 45303ead..ecb3243a 100644 --- a/src/main/scala/co.blocke.scalajack/json/writing/AnyWriter.scala +++ b/src/main/scala/co.blocke.scalajack/json/writing/AnyWriter.scala @@ -144,7 +144,6 @@ object AnyWriter: case Failure(e) => cfg.tryFailureHandling match case TryPolicy.AS_NULL => Some("null") - case TryPolicy.NO_WRITE => None case TryPolicy.ERR_MSG_STRING => Some("Try Failure with msg: " + e.getMessage()) case TryPolicy.THROW_EXCEPTION => throw e @@ -152,7 +151,6 @@ object AnyWriter: cfg.eitherLeftHandling match case EitherLeftPolicy.AS_VALUE => Some(v) case EitherLeftPolicy.AS_NULL => Some("null") - case EitherLeftPolicy.NO_WRITE => None case EitherLeftPolicy.ERR_MSG_STRING => Some("Left Error: " + v.toString) case EitherLeftPolicy.THROW_EXCEPTION => throw new JsonEitherLeftError("Left Error: " + v.toString) case Some(v) => isOkToWrite(v, cfg) 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 60c2cecb..108c39ef 100644 --- a/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala +++ b/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala @@ -14,6 +14,7 @@ case class JsonOutput(): private var comma: Boolean = false private var savePoint: Int = 0 + private var saveComma: Boolean = false def result = internal.result @@ -21,13 +22,16 @@ case class JsonOutput(): internal.clear() comma = false savePoint = 0 + saveComma = false this def mark() = savePoint = internal.length + saveComma = comma def revert() = // delete everything after the set savePoint internal.setLength(savePoint) + comma = saveComma inline def startObject(): Unit = maybeComma() @@ -60,6 +64,7 @@ case class JsonOutput(): internal.append("null") comma = true + // Problem: for unions, if left fails to write, comma is not reset to true for the attempt to write right side inline def label(s: String): Unit = maybeComma() internal.append('"') diff --git a/src/main/scala/co.blocke.scalajack/run/Play.scala b/src/main/scala/co.blocke.scalajack/run/Play.scala index 07a0adb7..0abdb81f 100644 --- a/src/main/scala/co.blocke.scalajack/run/Play.scala +++ b/src/main/scala/co.blocke.scalajack/run/Play.scala @@ -74,11 +74,25 @@ object RunMe extends App: // val c: Pizza = ScalaJack[Pizza].fromJson("\"READY\"") // println("Pizza: " + c) - opaque type OnOff = Boolean - opaque type Counter = Short - - val m: Map[OnOff, Counter] = Map(true.asInstanceOf[OnOff] -> 1.asInstanceOf[Counter]) - val n: Map[Boolean, Short] = m.asInstanceOf[Map[Boolean, Short]] - println(n) + implicit val blah: ScalaJack[Decide] = sjCodecOf[Decide] + // 012345678 + val c: Decide = ScalaJack[Decide].fromJson("""{"a":[1,2,3]}""") + println(c) println("done.") + + /* + + Option[Left(5)] -> None + + Either[Err,Option[String]] + + Left-Policy Class Field Option-Wrapped In Collection In Tuple + ---------------- -------------- --------------- --------------- ------------ + NO_WRITE Null None () Null <-- KILL NO_WRITE!! A symantic mess! + AS_VALUE Value Value Value Value + AS_NULL Null Null Null Null + ERR_MSG_STRING Err string Err String Err String Err String + THROW_EXCEPTION Throw Exception Throw Exception Throw Exception Throw Excpeiton + + */ diff --git a/src/main/scala/co.blocke.scalajack/run/Record.scala b/src/main/scala/co.blocke.scalajack/run/Record.scala index 05c5d49e..23eea00c 100644 --- a/src/main/scala/co.blocke.scalajack/run/Record.scala +++ b/src/main/scala/co.blocke.scalajack/run/Record.scala @@ -100,7 +100,7 @@ case class Person2(age: XList) case class Foom(a: schema.Schema) -case class Group(t: (Int, String, Boolean)) +case class Decide(a: List[Int]) sealed trait Candy: val isSweet: Boolean diff --git a/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala index f02d316a..6b35e11f 100644 --- a/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala @@ -20,94 +20,94 @@ class MapSpec() extends AnyFunSpec with JsonMatchers: val sj = sjCodecOf[MapHolder[Int, Int]] val js = sj.toJson(inst) js should matchJson("""{"a":null}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map key of string must work") { val inst = MapHolder[String, Int](Map("x" -> 1, "y" -> 2)) val sj = sjCodecOf[MapHolder[String, Int]] val js = sj.toJson(inst) js should matchJson("""{"a":{"x":1,"y":2}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map key of long must work") { val inst = MapHolder[Long, Int](Map(15L -> 1, 25L -> 2)) val sj = sjCodecOf[MapHolder[Long, Int]] val js = sj.toJson(inst) js should matchJson("""{"a":{"15":1,"25":2}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map key of boolean must work") { val inst = MapHolder[Boolean, Int](Map(true -> 1, false -> 2)) val sj = sjCodecOf[MapHolder[Boolean, Int]] val js = sj.toJson(inst) js should matchJson("""{"a":{"true":1,"false":2}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map key of uuid must work") { val inst = MapHolder[UUID, String](Map(UUID.fromString("1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03") -> "x", UUID.fromString("09abdeb1-8b07-4683-8f97-1f5621696008") -> "y")) val sj = sjCodecOf[MapHolder[UUID, String]] val js = sj.toJson(inst) js should matchJson("""{"a":{"1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03":"x","09abdeb1-8b07-4683-8f97-1f5621696008":"y"}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map value of string must work") { val inst = MapHolder[String, String](Map("w" -> "x", "y" -> "z")) val sj = sjCodecOf[MapHolder[String, String]] val js = sj.toJson(inst) js should matchJson("""{"a":{"w":"x","y":"z"}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map value of long must work") { val inst = MapHolder[String, Long](Map("w" -> 3L, "y" -> 4L)) val sj = sjCodecOf[MapHolder[String, Long]] val js = sj.toJson(inst) js should matchJson("""{"a":{"w":3,"y":4}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map value of boolean must work") { val inst = MapHolder[String, Boolean](Map("w" -> true, "y" -> false)) val sj = sjCodecOf[MapHolder[String, Boolean]] val js = sj.toJson(inst) js should matchJson("""{"a":{"w":true,"y":false}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map key and value of opaque alias type must work") { val inst = MapHolder[OnOff, Counter](Map(true.asInstanceOf[OnOff] -> 1.asInstanceOf[Counter], false.asInstanceOf[OnOff] -> 0.asInstanceOf[Counter])) val sj = sjCodecOf[MapHolder[OnOff, Counter]] val js = sj.toJson(inst) js should matchJson("""{"a":{"true":1,"false":0}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map value of uuid must work") { val inst = MapHolder[String, UUID](Map("x" -> UUID.fromString("1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03"), "y" -> UUID.fromString("09abdeb1-8b07-4683-8f97-1f5621696008"))) val sj = sjCodecOf[MapHolder[String, UUID]] val js = sj.toJson(inst) js should matchJson("""{"a":{"x":"1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03","y":"09abdeb1-8b07-4683-8f97-1f5621696008"}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map value of Seq must work") { val inst = MapHolder[String, List[Int]](Map("w" -> List(1, 2), "y" -> List(3, 4))) val sj = sjCodecOf[MapHolder[String, List[Int]]] val js = sj.toJson(inst) js should matchJson("""{"a":{"w":[1,2],"y":[3,4]}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map value of Map (nested) must work") { val inst = MapHolder[String, Map[String, Int]](Map("w" -> Map("r" -> 3, "t" -> 4), "y" -> Map("s" -> 7, "q" -> 9))) val sj = sjCodecOf[MapHolder[String, Map[String, Int]]] val js = sj.toJson(inst) js should matchJson("""{"a":{"w":{"r":3,"t":4},"y":{"s":7,"q":9}}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } it("Map value of union type must work") { val inst = MapHolder[String, Int | List[String]](Map("w" -> 3, "y" -> List("wow", "blah"))) val sj = sjCodecOf[MapHolder[String, Int | List[String]]] val js = sj.toJson(inst) js should matchJson("""{"a":{"w":3,"y":["wow","blah"]}}""") - sj.fromJson(js) shouldEqual(inst) + sj.fromJson(js) shouldEqual (inst) } /* - * Keys of value class (Distance) must work + * Keys of value class (Distance) must work it("Map value of class must work") { val inst = MapHolder[String, Person](Map("w" -> Person("Bob", 34), "y" -> Person("Sally", 25))) @@ -126,7 +126,7 @@ class MapSpec() extends AnyFunSpec with JsonMatchers: js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") sj.fromJson(js) shouldEqual(inst) } - */ + */ } // describe(colorString("--- Negative Tests ---")) { diff --git a/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala index 289f5df7..8d9016c5 100644 --- a/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala @@ -15,97 +15,100 @@ import java.util.UUID class LRSpec() extends AnyFunSpec with JsonMatchers: describe(colorString("-------------------------------\n: Either Tests :\n-------------------------------", Console.YELLOW)) { - it("Complex Either/Option must work (non-None)") { - val inst = ComplexEither[Int](Some(Right(Some(3)))) - val sj = sjCodecOf[ComplexEither[Int]] - val js = sj.toJson(inst) - js should matchJson("""{"a":3}""") - sj.fromJson(js) shouldEqual (inst) - } - it("Complex Either/Option must work (None no-write default)") { - val inst = ComplexEither[Int](Some(Right(None))) - val sj = sjCodecOf[ComplexEither[Int]] - val js = sj.toJson(inst) - js should matchJson("""{}""") - sj.fromJson(js) shouldEqual (ComplexEither(null)) - } - it("Complex Either/Option must work (NoneAsNull)") { - val inst = ComplexEither[Int](Some(Right(None))) - val sj = sjCodecOf[ComplexEither[Int]](JsonConfig.withNoneAsNull()) - val js = sj.toJson(inst) - js should matchJson("""{"a":null}""") - sj.fromJson(js) shouldEqual (ComplexEither(None)) // None here because value existed, but was null with NoneAsNull - } - it("Complex Either/Option must work (Left-NO_WRITE)") { - val inst = ComplexEither[Int](Some(Left("err"))) - val sj = sjCodecOf[ComplexEither[Int]] - val js = sjCodecOf[ComplexEither[Int]].toJson(inst) - js should matchJson("""{}""") - sj.fromJson(js) shouldEqual (ComplexEither(null)) // Null because value didn't exist at all - } - it("Complex Either/Option must work (Left-AS_VALUE)") { - val inst = ComplexEither[Int](Some(Left("err"))) - val sj = sjCodecOf[ComplexEither[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.AS_VALUE)) - val js = sj.toJson(inst) - js should matchJson("""{"a":"err"}""") - sj.fromJson(js) shouldEqual (inst) - } - it("Either with AS_VALUE left policy must work") { - val inst = EitherHolder[Int](Left(5), Right(3)) - val sj = sjCodecOf[EitherHolder[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.AS_VALUE)) - val js = sj.toJson(inst) - js should matchJson("""{"a":5,"b":3}""") - sj.fromJson(js) shouldEqual (inst) - } - it("Either with AS_NULL left policy must work") { - val inst = EitherHolder[Int](Left(5), Right(3)) - val sj = sjCodecOf[EitherHolder[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.AS_NULL)) - val js = sj.toJson(inst) - js should matchJson("""{"a":null,"b":3}""") - sj.fromJson(js) shouldEqual (EitherHolder(null, Right(3))) - } - it("Either with NO_WRITE left policy must work") { - val inst = EitherHolder[Int](Left(5), Right(3)) - val sj = sjCodecOf[EitherHolder[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.NO_WRITE)) - val js = sj.toJson(inst) - js should matchJson("""{"b":3}""") - // This js cannot be read back in becuase it's missing required field 'a', which wasn't written out - // per NO_WRITE policy. This is a 1-way trip... so be advised... - } - it("Either with ERR_MSG_STRING left policy must work") { - val inst = EitherHolder[Int](Left(5), Right(3)) - val sj = sjCodecOf[EitherHolder[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.ERR_MSG_STRING)) - val js = sj.toJson(inst) - js should matchJson("""{"a":"Left Error: 5","b":3}""") - sj.fromJson(js) shouldEqual (EitherHolder(Right("Left Error: 5"), Right(3))) - // WARNING! Here a Left(err_msg) was "promoted" to a Right(String) upon read, because Right was of type - // String, and "Left Error: 5" is a valid string. Use with extreme caution. Best to consider this a 1-way - // trip for debugging purposes only. You have been warned. + describe(colorString("""/// Right Tests ///""")) { + it("Complex Either/Option must work (non-None)") { + val inst = ComplexEither[Int](Some(Right(Some(3)))) + val sj = sjCodecOf[ComplexEither[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":3}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Complex Either/Option must work (None -> null for Either)") { + val inst = ComplexEither[Int](Some(Right(None))) + val sj = sjCodecOf[ComplexEither[Int]] + val js = sj.toJson(inst) + js should matchJson("""{}""") + sj.fromJson(js) shouldEqual (ComplexEither(None)) + } + it("Complex Either/Option must work (NoneAsNull)") { + val inst = ComplexEither[Int](Some(Right(None))) + val sj = sjCodecOf[ComplexEither[Int]](JsonConfig.withNoneAsNull()) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") // same output result regardless of noneAsNull setting + sj.fromJson(js) shouldEqual (ComplexEither(None)) // None here because value existed, but was null with NoneAsNull + } } - it("Either with THROW_EXCEPTION left policy must work") { - val inst = EitherHolder[Int](Left(5), Right(3)) - val caught = - intercept[JsonEitherLeftError] { - sjCodecOf[EitherHolder[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.THROW_EXCEPTION)).toJson(inst) - } - assert(caught.getMessage == "Left Error: 5") + describe(colorString("""\\\ Left Tests \\\""")) { + it("Complex Either/Option must work (Left-default: AS_VALUE)") { + val inst = ComplexEither[Int](Some(Left("msg"))) + val sj = sjCodecOf[ComplexEither[Int]] + val js = sjCodecOf[ComplexEither[Int]].toJson(inst) + js should matchJson("""{"a":"msg"}""") + sj.fromJson(js) shouldEqual inst + } + it("Complex Either/Option must work (Left-AS_NULL)") { + val inst = ComplexEither[Int](Some(Left("err"))) + val sj = sjCodecOf[ComplexEither[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.AS_NULL)) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (ComplexEither(Some(null))) + } + it("Complex Either/Option must work (Left-AS_NULL, Option nullAsNull)") { + val inst = ComplexEither[Int](Some(Left("err"))) + val sj = sjCodecOf[ComplexEither[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.AS_NULL).withNoneAsNull()) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (ComplexEither(None)) + } + it("Either with AS_VALUE (default) left policy must work") { + val inst = EitherHolder[Int](Left(5), Right(3)) + val sj = sjCodecOf[EitherHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5,"b":3}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Either with AS_NULL left policy must work") { + val inst = EitherHolder[Int](Left(5), Right(3)) + val sj = sjCodecOf[EitherHolder[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.AS_NULL)) + val js = sj.toJson(inst) + js should matchJson("""{"a":null,"b":3}""") + sj.fromJson(js) shouldEqual (EitherHolder(null, Right(3))) + } + it("Either with ERR_MSG_STRING left policy must work") { + val inst = EitherHolder[Int](Left(5), Right(3)) + val sj = sjCodecOf[EitherHolder[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.ERR_MSG_STRING)) + val js = sj.toJson(inst) + js should matchJson("""{"a":"Left Error: 5","b":3}""") + sj.fromJson(js) shouldEqual (EitherHolder(Right("Left Error: 5"), Right(3))) + // WARNING! Here a Left(err_msg) was "promoted" to a Right(String) upon read, because Right was of type + // String, and "Left Error: 5" is a valid string. Use with extreme caution. Best to consider this a 1-way + // trip for debugging purposes only. You have been warned. + } + it("Either with THROW_EXCEPTION left policy must work") { + val inst = EitherHolder[Int](Left(5), Right(3)) + val caught = + intercept[JsonEitherLeftError] { + sjCodecOf[EitherHolder[Int]](JsonConfig.withEitherLeftHandling(EitherLeftPolicy.THROW_EXCEPTION)).toJson(inst) + } + assert(caught.getMessage == "Left Error: 5") + } } } describe(colorString("----------------------------------\n: Union Tests :\n----------------------------------", Console.YELLOW)) { it("LR (union) must work with Option (non-None)") { - val inst = LRUnionHolder[Option[Int], String](List(Some(5), "x"), ("y", Some(10))) - val sj = sjCodecOf[LRUnionHolder[Option[Int], String]] - val js = sj.toJson(inst) - js should matchJson("""{"a":[5,"x"],"b":["y",10]}""") - sj.fromJson(js) shouldEqual inst + val inst = LRUnionHolder[Option[Int], String](List(Some(5), "x"), ("y", Some(10))) + val sj = sjCodecOf[LRUnionHolder[Option[Int], String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[5,"x"],"b":["y",10]}""") + sj.fromJson(js) shouldEqual inst } it("LR (union) must work with Option (None)") { - val inst = LRUnionHolder[Option[Int], String](List(None, "x"), ("y", None)) - val sj = sjCodecOf[LRUnionHolder[Option[Int], String]] - val js = sj.toJson(inst) - js should matchJson("""{"a":["x"],"b":["y",null]}""") - sj.fromJson(js) shouldEqual LRUnionHolder[Option[Int], String](List("x"), ("y", None)) + val inst = LRUnionHolder[Option[Int], String](List(None, "x"), ("y", None)) + val sj = sjCodecOf[LRUnionHolder[Option[Int], String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["x"],"b":["y",null]}""") + sj.fromJson(js) shouldEqual LRUnionHolder[Option[Int], String](List("x"), ("y", None)) } // it("LR (union) must work with Try of Option (non-None)") { // val inst = LRHolder[Try[Option[Int]], String](List(Success(Some(5)), "x"), ("y", Success(Some(10)))) @@ -122,4 +125,4 @@ class LRSpec() extends AnyFunSpec with JsonMatchers: // val js = sj[LRHolder[Try[Option[Int]], String]].toJson(inst) // js should matchJson("""{"a":["x"],"b":["y",null]}""") // } - } \ No newline at end of file + } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/Model.scala b/src/test/scala/co.blocke.scalajack/json/misc/Model.scala index 8d1c122e..f140bbaa 100644 --- a/src/test/scala/co.blocke.scalajack/json/misc/Model.scala +++ b/src/test/scala/co.blocke.scalajack/json/misc/Model.scala @@ -28,6 +28,7 @@ case class LRUnionHolder[T, U](a: Seq[T | U], b: (T | U, T | U)) case class EitherHolder[T](a: Either[T, String], b: Either[String, T]) case class ComplexEither[T](a: Option[Either[String, Option[T]]]) +case class EitherRecipe[T](a: Either[Boolean, Either[Option[T], String]]) case class AliasHolder[T](a: T, b: List[T], c: Map[T, String], d: Map[String, T]) case class AliasHolder2[T](a: T, b: List[T], c: Map[String, T]) diff --git a/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala index fabcda59..0213d0fe 100644 --- a/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala @@ -12,10 +12,9 @@ import TestUtil.* import java.util.UUID -class OptionTrySpec() extends AnyFunSpec with JsonMatchers: +class OptionSpec() extends AnyFunSpec with JsonMatchers: describe(colorString("-------------------------------\n: Option Tests :\n-------------------------------", Console.YELLOW)) { - /* it("Non-empty Options must work") { val inst = OptionHolder[Int]( Some(5), // straight Option @@ -29,8 +28,11 @@ class OptionTrySpec() extends AnyFunSpec with JsonMatchers: Right(Some(15)), // Either of Option (R) Left(Some(-3)) // Either of Option (L) ) - val js = sjCodecOf[OptionHolder[Int]].toJson(inst) - js should matchJson("""{"a":5,"b":[1,"ok"],"c":[1,3],"d":{"1":2,"3":4},"e":99,"f":100,"g":0,"h":{"name":"BoB","age":34},"i":15}""") + val sj = sjCodecOf[OptionHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5,"b":[1,"ok"],"c":[1,3],"d":{"1":2,"3":4},"e":99,"f":100,"g":0,"h":{"name":"BoB","age":34},"i":15,"j":-3}""") + // Some fields get changed/"promoted" when read back in per policy, as Either reads "best-fit" starting with Right value first + sj.fromJson(js) shouldEqual (inst.copy(c = List(Some(1), Some(3))).copy(e = 99).copy(j = Right(-3))) } it("Empty Options must work (default)") { val inst = OptionHolder[Int]( @@ -45,8 +47,10 @@ class OptionTrySpec() extends AnyFunSpec with JsonMatchers: Right(None), // Either of Option (R) Left(None) // Either of Option (L) ) - val js = sjCodecOf[OptionHolder[Int]].toJson(inst) + val sj = sjCodecOf[OptionHolder[Int]] + val js = sj.toJson(inst) js should matchJson("""{"b":[null,"ok"],"c":[],"d":{}}""") + sj.fromJson(js) shouldEqual (inst.copy(c = List(), d = Map(), g = None)) } it("Empty Options must work (config noneAsNull = true)") { val inst = OptionHolder[Int]( @@ -61,13 +65,40 @@ class OptionTrySpec() extends AnyFunSpec with JsonMatchers: Right(None), // Either of Option (R) Left(None) // Either of Option (L) ) - val js = sj[OptionHolder[Int]]( - JsonConfig.withNoneAsNull().withEitherLeftHandling(EitherLeftPolicy.AS_VALUE) - ).toJson(inst) + val sj = sjCodecOf[OptionHolder[Int]]( + JsonConfig.withNoneAsNull() + ) + val js = sj.toJson(inst) js should matchJson("""{"a":null,"b":[null,"ok"],"c":[null,null,null],"d":{"1":null,"3":null},"e":null,"f":null,"g":null,"h":null,"i":null,"j":null}""") + sj.fromJson(js) shouldEqual (inst.copy(g = None, i = null, j = null)) + } + it("Either recipe should work (non-None)") { + val inst = EitherRecipe[Int](Right(Left(Some(5)))) + val sj = sjCodecOf[EitherRecipe[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Either recipe should work (None)") { + val inst = EitherRecipe[Int](Right(Left(None))) + val sj = sjCodecOf[EitherRecipe[Int]] + val js = sj.toJson(inst) + js should matchJson("""{}""") + sj.fromJson(js) shouldEqual (inst) } + it("Either recipe should work (None as null)") { + val inst = EitherRecipe[Int](Right(Left(None))) + val sj = sjCodecOf[EitherRecipe[Int]](JsonConfig.withNoneAsNull()) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (EitherRecipe[Int](null)) + } + + /* + TODO: Java Optional flavor... + it("Java Optional must work") { ??? } - */ + */ } diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala b/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala index 5968f70d..49ede9e6 100644 --- a/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala +++ b/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala @@ -204,7 +204,7 @@ class ScalaPrimSpec() extends AnyFunSpec with JsonMatchers: val js = """{"d1":1.79769313486E23157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123.4567}""" val msg = - """Expected ',' or '}' but found E at position [25] + """Expected ',' or '}' but found 'E' at position [25] |{"d1":1.79769313486E23157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123... |-------------------------^""".stripMargin val ex = intercept[JsonParseError](sjCodecOf[SampleDouble].fromJson(js)) @@ -251,7 +251,7 @@ class ScalaPrimSpec() extends AnyFunSpec with JsonMatchers: val js2 = """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123}""" val msg2 = - """Expected ',' or '}' but found . at position [58] + """Expected ',' or '}' but found '.' at position [58] |...3372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123} |----------------------------------------------------^""".stripMargin val ex2 = intercept[JsonParseError](sj.fromJson(js2))