From db8a60d85339ba955bfab59fa0b1f17617b4d40c Mon Sep 17 00:00:00 2001 From: Greg Zoller Date: Wed, 27 Mar 2024 23:40:12 -0500 Subject: [PATCH] more read cases implement --- benchmark/src/main/scala/co.blocke/Run.scala | 10 +- .../json/JsonCodecMaker.scala | 486 +++++++++++++----- .../json/reading/JsonSource.scala | 17 + .../scala/co.blocke.scalajack/run/Play.scala | 12 +- .../{MapSpec.scalax => MapSpec.scala} | 114 ++-- .../json/collections/Model.scala | 3 + .../json/collections/TupleSpec.scala | 59 +++ .../json/collections/TupleSpec.scalax | 34 -- .../json/misc/EnumSpec.scalax | 54 ++ .../json/misc/LRSpec.scala | 125 +++++ .../co.blocke.scalajack/json/misc/Model.scala | 30 +- .../json/misc/OptionLRTrySpec.scala | 244 --------- .../json/misc/OptionSpec.scala | 73 +++ .../json/misc/TrySpec.scalax | 73 +++ .../json/primitives/Model.scala | 36 +- 15 files changed, 854 insertions(+), 516 deletions(-) rename src/test/scala/co.blocke.scalajack/json/collections/{MapSpec.scalax => MapSpec.scala} (54%) create mode 100644 src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala delete mode 100644 src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scalax create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/EnumSpec.scalax create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala delete mode 100644 src/test/scala/co.blocke.scalajack/json/misc/OptionLRTrySpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scalax diff --git a/benchmark/src/main/scala/co.blocke/Run.scala b/benchmark/src/main/scala/co.blocke/Run.scala index f7855dc4..4ad296e3 100644 --- a/benchmark/src/main/scala/co.blocke/Run.scala +++ b/benchmark/src/main/scala/co.blocke/Run.scala @@ -8,10 +8,10 @@ object RunMe extends App: import co.blocke.scalajack.ScalaJack.* import co.blocke.scalajack.* - // given codec: JsonValueCodec[Who.Type] = JsonCodecMaker.make - - // implicit val blah: ScalaJack[co.blocke.scalajack.json.run.Yippy] = sj[co.blocke.scalajack.json.run.Yippy] - - // println( ScalaJack[co.blocke.scalajack.json.run.Yippy].toJson(yippy) ) + //case class MapHolder( m: Map[Pet2, String]) + // val mh = MapHolder( Map(Pet2("Mindy","Frenchie",4)->"a", Pet2("Rosie","Terrier",8)->"b")) + // given codec: JsonValueCodec[Pet2] = JsonCodecMaker.make + // given codec2: JsonValueCodec[MapHolder] = JsonCodecMaker.make + // println(writeToString(mh)) println("\nDone") \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala b/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala index d03d4b3c..59419e1a 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala @@ -104,6 +104,16 @@ object JsonCodecMaker: // --------------------------------------------------------------------------------------------- + def testValidMapKey(testRef: RTypeRef[?]): Boolean = + val isValid = testRef match + case _: PrimitiveRef => true + case _: TimeRef => true + case _: NetRef => true + case a: AliasRef[?] => testValidMapKey(a.unwrappedType) + case _ => false + if !isValid then throw new JsonTypeError(s"For JSON serialization, map keys must be a simple type. ${testRef.name} is too complex.") + isValid + def maybeWrite[T](label: String, aE: Expr[T], ref: RTypeRef[T], out: Expr[JsonOutput], cfg: JsonConfig): Expr[Unit] = val labelE = Expr(label) _maybeWrite[T]( @@ -120,7 +130,7 @@ object JsonCodecMaker: _maybeWrite[V]( '{ $out.maybeComma() - ${ genWriteVal(keyE.asExprOf[k], keyRef.asInstanceOf[RTypeRef[k]], out, true) } + ${ genWriteVal(keyE.asExprOf[k], keyRef.asInstanceOf[RTypeRef[k]], out, false, true) } $out.colon() }, valueE, @@ -402,7 +412,48 @@ object JsonCodecMaker: $out.startObject() $tin.foreach { case (key, value) => ${ - maybeWriteMap[k, v]('{ key }, '{ value }.asExprOf[v], t.elementRef.asInstanceOf[RTypeRef[k]], t.elementRef2.asInstanceOf[RTypeRef[v]], out, cfg) + (t.elementRef, t.elementRef2) match + case (aliasK: AliasRef[?], aliasV: AliasRef[?]) => + aliasK.unwrappedType.refType match + case '[ak] => + aliasV.unwrappedType.refType match + case '[av] => + testValidMapKey(aliasK.unwrappedType) + maybeWriteMap[ak, av]( + '{ key.asInstanceOf[ak] }, + '{ value.asInstanceOf[av] }, + aliasK.unwrappedType.asInstanceOf[RTypeRef[ak]], + aliasV.unwrappedType.asInstanceOf[RTypeRef[av]], + out, + cfg + ) + case (_, aliasV: AliasRef[?]) => + aliasV.unwrappedType.refType match + case '[av] => + testValidMapKey(t.elementRef) + maybeWriteMap[k, av]( + '{ key }.asExprOf[k], + '{ value.asInstanceOf[av] }, + t.elementRef.asInstanceOf[RTypeRef[k]], + aliasV.unwrappedType.asInstanceOf[RTypeRef[av]], + out, + cfg + ) + case (aliasK: AliasRef[?], _) => + aliasK.unwrappedType.refType match + case '[ak] => + testValidMapKey(aliasK.unwrappedType) + maybeWriteMap[ak, v]( + '{ key.asInstanceOf[ak] }, + '{ value }.asExprOf[v], + aliasK.unwrappedType.asInstanceOf[RTypeRef[ak]], + t.elementRef2.asInstanceOf[RTypeRef[v]], + out, + cfg + ) + case (_, _) => + testValidMapKey(t.elementRef) + maybeWriteMap[k, v]('{ key }, '{ value }.asExprOf[v], t.elementRef.asInstanceOf[RTypeRef[k]], t.elementRef2.asInstanceOf[RTypeRef[v]], out, cfg) } } $out.endObject() @@ -656,7 +707,8 @@ object JsonCodecMaker: aE: Expr[T], ref: RTypeRef[T], out: Expr[JsonOutput], - inTuple: Boolean = false + inTuple: Boolean = false, + isMapKey: Boolean = false // only primitive or primitive-equiv types can be Map keys )(using Quotes): Expr[Unit] = val methodKey = MethodKey(ref, false) writeMethodSyms @@ -667,29 +719,69 @@ object JsonCodecMaker: .getOrElse( ref match // First cover all primitive and simple types... - case t: BigDecimalRef => '{ $out.value(${ aE.asExprOf[scala.math.BigDecimal] }) } - case t: BigIntRef => '{ $out.value(${ aE.asExprOf[scala.math.BigInt] }) } - case t: BooleanRef => '{ $out.value(${ aE.asExprOf[Boolean] }) } - case t: ByteRef => '{ $out.value(${ aE.asExprOf[Byte] }) } - case t: CharRef => '{ $out.value(${ aE.asExprOf[Char] }) } - case t: DoubleRef => '{ $out.value(${ aE.asExprOf[Double] }) } - case t: FloatRef => '{ $out.value(${ aE.asExprOf[Float] }) } - case t: IntRef => '{ $out.value(${ aE.asExprOf[Int] }) } - case t: LongRef => '{ $out.value(${ aE.asExprOf[Long] }) } - case t: ShortRef => '{ $out.value(${ aE.asExprOf[Short] }) } - case t: StringRef => '{ $out.valueEscaped(${ aE.asExprOf[String] }) } - - case t: JBigDecimalRef => '{ $out.value(${ aE.asExprOf[java.math.BigDecimal] }) } - case t: JBigIntegerRef => '{ $out.value(${ aE.asExprOf[java.math.BigInteger] }) } - case t: JBooleanRef => '{ $out.value(${ aE.asExprOf[java.lang.Boolean] }) } - case t: JByteRef => '{ $out.value(${ aE.asExprOf[java.lang.Byte] }) } - case t: JCharacterRef => '{ $out.value(${ aE.asExprOf[java.lang.Character] }) } - case t: JDoubleRef => '{ $out.value(${ aE.asExprOf[java.lang.Double] }) } - case t: JFloatRef => '{ $out.value(${ aE.asExprOf[java.lang.Float] }) } - case t: JIntegerRef => '{ $out.value(${ aE.asExprOf[java.lang.Integer] }) } - case t: JLongRef => '{ $out.value(${ aE.asExprOf[java.lang.Long] }) } - case t: JShortRef => '{ $out.value(${ aE.asExprOf[java.lang.Short] }) } - case t: JNumberRef => '{ $out.value(${ aE.asExprOf[java.lang.Number] }) } + case t: BigDecimalRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[scala.math.BigDecimal] }) } + else '{ $out.value(${ aE.asExprOf[scala.math.BigDecimal] }) } + case t: BigIntRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[scala.math.BigInt] }) } + else '{ $out.value(${ aE.asExprOf[scala.math.BigInt] }) } + case t: BooleanRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Boolean] }) } + else '{ $out.value(${ aE.asExprOf[Boolean] }) } + case t: ByteRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Byte] }) } + else '{ $out.value(${ aE.asExprOf[Byte] }) } + case t: CharRef => + '{ $out.value(${ aE.asExprOf[Char] }) } + case t: DoubleRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Double] }) } + else '{ $out.value(${ aE.asExprOf[Double] }) } + case t: FloatRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Float] }) } + else '{ $out.value(${ aE.asExprOf[Float] }) } + case t: IntRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Int] }) } + else '{ $out.value(${ aE.asExprOf[Int] }) } + case t: LongRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Long] }) } + else '{ $out.value(${ aE.asExprOf[Long] }) } + case t: ShortRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Short] }) } + else '{ $out.value(${ aE.asExprOf[Short] }) } + case t: StringRef => '{ $out.valueEscaped(${ aE.asExprOf[String] }) } + + case t: JBigDecimalRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.math.BigDecimal] }) } + else '{ $out.value(${ aE.asExprOf[java.math.BigDecimal] }) } + case t: JBigIntegerRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.math.BigInteger] }) } + else '{ $out.value(${ aE.asExprOf[java.math.BigInteger] }) } + case t: JBooleanRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Boolean] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Boolean] }) } + case t: JByteRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Byte] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Byte] }) } + case t: JCharacterRef => + '{ $out.value(${ aE.asExprOf[java.lang.Character] }) } + case t: JDoubleRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Double] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Double] }) } + case t: JFloatRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Float] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Float] }) } + case t: JIntegerRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Integer] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Integer] }) } + case t: JLongRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Long] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Long] }) } + case t: JShortRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Short] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Short] }) } + case t: JNumberRef => + if isMapKey then '{ $out.valueStringified(${ 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] }) } @@ -728,7 +820,9 @@ object JsonCodecMaker: case Some(list) if list.contains(t.name) => true case _ => false val rtype = t.expr - if enumAsId then '{ $out.value($rtype.asInstanceOf[EnumRType[_]].ordinal($aE.toString).get) } + if enumAsId then + if isMapKey then '{ $out.value($rtype.asInstanceOf[EnumRType[_]].ordinal($aE.toString).get.toString) } // stringified id + else '{ $out.value($rtype.asInstanceOf[EnumRType[_]].ordinal($aE.toString).get) } // int value of id else '{ $out.value($aE.toString) } // NeoType is a bit of a puzzle-box. To get the correct underlying base type, I had to dig into @@ -868,7 +962,8 @@ object JsonCodecMaker: // default: Expr[T], // needed? This should already be in ref... ref: RTypeRef[T], in: Expr[JsonSource], - inTuple: Boolean = false // not sure if needed... + inTuple: Boolean = false, // not sure if needed... + isMapKey: Boolean = false )(using Quotes): Expr[T] = val methodKey = MethodKey(ref, false) readMethodSyms @@ -880,21 +975,37 @@ object JsonCodecMaker: ref match // First cover all primitive and simple types... case t: BigDecimalRef => - '{ - $in.expectNumberOrNull() match - case null => null - case s => scala.math.BigDecimal(s) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case s => scala.math.BigDecimal(s) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case s => scala.math.BigDecimal(s) + }.asExprOf[T] case t: BigIntRef => - '{ - $in.expectNumberOrNull() match - case null => null - case s => scala.math.BigInt(s) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case s => scala.math.BigInt(s) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case s => scala.math.BigInt(s) + }.asExprOf[T] case t: BooleanRef => - '{ $in.expectBoolean() }.asExprOf[T] + if isMapKey then '{ $in.expectString().toBoolean }.asExprOf[T] + else '{ $in.expectBoolean() }.asExprOf[T] case t: ByteRef => - '{ $in.expectInt().toByte }.asExprOf[T] + if isMapKey then '{ $in.expectString().toInt.toByte }.asExprOf[T] + else '{ $in.expectInt().toByte }.asExprOf[T] case t: CharRef => '{ $in.expectString() match @@ -911,38 +1022,65 @@ object JsonCodecMaker: case c => c.charAt(0) }.asExprOf[T] case t: DoubleRef => - '{ $in.expectDouble() }.asExprOf[T] + if isMapKey then '{ $in.expectString().toDouble }.asExprOf[T] + else '{ $in.expectDouble() }.asExprOf[T] case t: FloatRef => - '{ $in.expectFloat() }.asExprOf[T] + if isMapKey then '{ $in.expectString().toFloat }.asExprOf[T] + else '{ $in.expectFloat() }.asExprOf[T] case t: IntRef => - '{ $in.expectInt() }.asExprOf[T] + if isMapKey then '{ $in.expectString().toInt }.asExprOf[T] + else '{ $in.expectInt() }.asExprOf[T] case t: LongRef => - '{ $in.expectLong() }.asExprOf[T] + if isMapKey then '{ $in.expectString().toLong }.asExprOf[T] + else '{ $in.expectLong() }.asExprOf[T] case t: ShortRef => - '{ $in.expectInt().toShort }.asExprOf[T] + if isMapKey then '{ $in.expectString().toShort }.asExprOf[T] + else '{ $in.expectInt().toShort }.asExprOf[T] case t: StringRef => '{ $in.expectString() }.asExprOf[T] case t: JBigDecimalRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => new java.math.BigDecimal(n) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => new java.math.BigDecimal(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => new java.math.BigDecimal(n) + }.asExprOf[T] case t: JBigIntegerRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => new java.math.BigInteger(n) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => new java.math.BigInteger(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => new java.math.BigInteger(n) + }.asExprOf[T] case t: JBooleanRef => - '{ $in.expectJavaBoolean() }.asExprOf[T] + if isMapKey then '{ java.lang.Boolean.valueOf($in.expectString()) }.asExprOf[T] + else '{ $in.expectJavaBoolean() }.asExprOf[T] case t: JByteRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => java.lang.Byte.valueOf(n) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Byte.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Byte.valueOf(n) + }.asExprOf[T] case t: JCharacterRef => '{ val c = $in.expectString() @@ -954,50 +1092,101 @@ object JsonCodecMaker: else java.lang.Character.valueOf(c.charAt(0)) }.asExprOf[T] case t: JDoubleRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => java.lang.Double.valueOf(n) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Double.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Double.valueOf(n) + }.asExprOf[T] case t: JFloatRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => java.lang.Float.valueOf(n) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Float.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Float.valueOf(n) + }.asExprOf[T] case t: JIntegerRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => java.lang.Integer.valueOf(n) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Integer.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Integer.valueOf(n) + }.asExprOf[T] case t: JLongRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => java.lang.Long.valueOf(n) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Long.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Long.valueOf(n) + }.asExprOf[T] case t: JShortRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => java.lang.Short.valueOf(n) - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Short.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Short.valueOf(n) + }.asExprOf[T] case t: JNumberRef => - '{ - $in.expectNumberOrNull() match - case null => null - case n => - scala.math.BigDecimal(n) match { - case d if d.isValidByte => java.lang.Byte.valueOf(d.toByteExact) - case d if d.isValidShort => java.lang.Short.valueOf(d.toShortExact) - case d if d.isValidInt => java.lang.Integer.valueOf(d.toIntExact) - case d if d.isValidLong => java.lang.Long.valueOf(d.toLongExact) - case d if d.isDecimalFloat => java.lang.Float.valueOf(d.toFloat) - case d if d.isDecimalDouble => java.lang.Double.valueOf(d.toDouble) - case d => d - } - }.asExprOf[T] + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => + scala.math.BigDecimal(n) match { + case d if d.isValidByte => java.lang.Byte.valueOf(d.toByteExact) + case d if d.isValidShort => java.lang.Short.valueOf(d.toShortExact) + case d if d.isValidInt => java.lang.Integer.valueOf(d.toIntExact) + case d if d.isValidLong => java.lang.Long.valueOf(d.toLongExact) + case d if d.isDecimalFloat => java.lang.Float.valueOf(d.toFloat) + case d if d.isDecimalDouble => java.lang.Double.valueOf(d.toDouble) + case d => d + } + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => + scala.math.BigDecimal(n) match { + case d if d.isValidByte => java.lang.Byte.valueOf(d.toByteExact) + case d if d.isValidShort => java.lang.Short.valueOf(d.toShortExact) + case d if d.isValidInt => java.lang.Integer.valueOf(d.toIntExact) + case d if d.isValidLong => java.lang.Long.valueOf(d.toLongExact) + case d if d.isDecimalFloat => java.lang.Float.valueOf(d.toFloat) + case d if d.isDecimalDouble => java.lang.Double.valueOf(d.toDouble) + case d => d + } + }.asExprOf[T] case t: DurationRef => '{ $in.expectString(java.time.Duration.parse) }.asExprOf[T] case t: InstantRef => '{ $in.expectString(java.time.Instant.parse) }.asExprOf[T] @@ -1029,11 +1218,16 @@ object JsonCodecMaker: else t.unwrappedType.refType match case '[e] => - genReadVal[e]( - t.unwrappedType.asInstanceOf[RTypeRef[e]], - in, - inTuple - ).asExprOf[T] + '{ + ${ + genReadVal[e]( + t.unwrappedType.asInstanceOf[RTypeRef[e]], + in, + inTuple, + isMapKey + ) + }.asInstanceOf[T] + } // -------------------- // Options... @@ -1069,30 +1263,25 @@ object JsonCodecMaker: $in.retract() throw JsonParseError("Failed to read either side of Either type", $in) }.asExprOf[T] - /* - Either: - def read(parser: Parser): Either[L, R] = { - val savedReader = parser.mark() - if (parser.peekForNull) - null - else - Try(rightTypeAdapter.read(parser)) match { - case Success(rightValue) => - Right(rightValue.asInstanceOf[R]) - case Failure(_) => // Right parse failed... try left - parser.revertToMark(savedReader) - Try(leftTypeAdapter.read(parser)) match { - case Success(leftValue) => - Left(leftValue.asInstanceOf[L]) - case Failure(x) => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"Failed to read either side of Either") - ) - } - } - } + case t: LeftRightRef[?] if t.lrkind == LRKind.UNION => + import quotes.reflect.* + t.leftRef.refType match + case '[l] => + t.rightRef.refType match + case '[r] => + '{ + scala.util.Try(${ genReadVal[l](t.leftRef.asInstanceOf[RTypeRef[l]], in, true) }) match + case Success(lval) => lval + case Failure(f) => + scala.util.Try(${ genReadVal[r](t.rightRef.asInstanceOf[RTypeRef[r]], in, true) }) match + case Success(rval) => rval + case Failure(_) => + $in.retract() + throw JsonParseError("Failed to read either side of Union type", $in) + }.asExprOf[T] + /* + TODO Intersection: val syntheticTA = taCache.typeAdapterOf[L] syntheticTA.write(t.asInstanceOf[L], writer, out) @@ -1248,6 +1437,24 @@ syntheticTA.write(t.asInstanceOf[L], writer, out) else null }.asExprOf[T] + case t: MapRef[?] => + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + $in.parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + }.asExprOf[T] + // -------------------- // Tuples... // -------------------- @@ -1275,24 +1482,21 @@ syntheticTA.write(t.asInstanceOf[L], writer, out) tpart.refType match case '[e] => if i == 0 then genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true).asTerm - else if i < t.tupleRefs.length - 1 then - '{ - $in.expectToken(',') - ${ genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true) } - }.asTerm else '{ $in.expectToken(',') - val res = ${ genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true) } - $in.expectToken(']') - res + ${ genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true) } }.asTerm } '{ - $in.expectToken('[') - ${ - Apply(TypeApply(Select.unique(New(Inferred(tpe)), ""), indexedTypes.map(x => Inferred(x))), tupleTerms).asExpr - } + if $in.expectNull() then null + else + $in.expectToken('[') + val tv: T = ${ + Apply(TypeApply(Select.unique(New(Inferred(tpe)), ""), indexedTypes.map(x => Inferred(x))), tupleTerms).asExprOf[T] + } + $in.expectToken(']') + tv }.asExprOf[T] case _ => @@ -1328,5 +1532,5 @@ syntheticTA.write(t.asInstanceOf[L], writer, out) // others here??? Refer to Jsoniter file JsonCodecMaker.scala classFieldMatrixValDefs ++ writeMethodDefs ++ readMethodDefs 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/reading/JsonSource.scala b/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala index 632335b9..92ed70de 100644 --- a/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala +++ b/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala @@ -126,6 +126,23 @@ case class JsonSource(js: CharSequence): if readToken() != ':' then throw new JsonParseError(s"Expected ':' field separator but found $here", this) fieldNameMatrix.first(bs) + @tailrec + final def parseMap[K, V](kf: () => K, vf: () => V, acc: Map[K, V], isFirst: Boolean = true): Map[K, V] = // initial '{' already consumed + readToken() match + case '}' => acc + case ',' => + val key = kf() + expectToken(':') + val value = vf() + parseMap[K, V](kf, vf, acc + (key -> value), false) + case _ if isFirst => + retract() + val key = kf() + expectToken(':') + val value = vf() + parseMap[K, V](kf, vf, acc + (key -> value), false) + case c => throw JsonParseError(s"Expected either object end '}' or field separator ',' here but got '$c'", this) + // Array and Tuple... // ======================================================= diff --git a/src/main/scala/co.blocke.scalajack/run/Play.scala b/src/main/scala/co.blocke.scalajack/run/Play.scala index aabb0c2a..07a0adb7 100644 --- a/src/main/scala/co.blocke.scalajack/run/Play.scala +++ b/src/main/scala/co.blocke.scalajack/run/Play.scala @@ -74,11 +74,11 @@ object RunMe extends App: // val c: Pizza = ScalaJack[Pizza].fromJson("\"READY\"") // println("Pizza: " + c) - // case class Group(t: (Int, String, Boolean)) - implicit val blah: ScalaJack[Group] = sjCodecOf[Group] - val g = Group((5, "Greg", true)) - val js = ScalaJack[Group].toJson(g) - println(js) - println(ScalaJack[Group].fromJson(js)) + 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) println("done.") diff --git a/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scalax b/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala similarity index 54% rename from src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scalax rename to src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala index 0119ae8a..f02d316a 100644 --- a/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scalax +++ b/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala @@ -17,70 +17,103 @@ class MapSpec() extends AnyFunSpec with JsonMatchers: describe(colorString("+++ Positive Tests +++")) { it("Map is null must work") { val inst = MapHolder[Int, Int](null) - val js = sj[MapHolder[Int, Int]].toJson(inst) + val sj = sjCodecOf[MapHolder[Int, Int]] + val js = sj.toJson(inst) js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual(inst) } it("Map key of string must work") { val inst = MapHolder[String, Int](Map("x" -> 1, "y" -> 2)) - val js = sj[MapHolder[String, Int]].toJson(inst) + val sj = sjCodecOf[MapHolder[String, Int]] + val js = sj.toJson(inst) js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual(inst) } it("Map key of long must work") { val inst = MapHolder[Long, Int](Map(15L -> 1, 25L -> 2)) - val js = sj[MapHolder[Long, Int]].toJson(inst) + val sj = sjCodecOf[MapHolder[Long, Int]] + val js = sj.toJson(inst) js should matchJson("""{"a":{"15":1,"25":2}}""") + sj.fromJson(js) shouldEqual(inst) } it("Map key of boolean must work") { val inst = MapHolder[Boolean, Int](Map(true -> 1, false -> 2)) - val js = sj[MapHolder[Boolean, Int]].toJson(inst) + val sj = sjCodecOf[MapHolder[Boolean, Int]] + val js = sj.toJson(inst) js should matchJson("""{"a":{"true":1,"false":2}}""") + 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 js = sj[MapHolder[UUID, String]].toJson(inst) + 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) } - it("Map value of string must work") { val inst = MapHolder[String, String](Map("w" -> "x", "y" -> "z")) - val js = sj[MapHolder[String, String]].toJson(inst) + val sj = sjCodecOf[MapHolder[String, String]] + val js = sj.toJson(inst) js should matchJson("""{"a":{"w":"x","y":"z"}}""") + sj.fromJson(js) shouldEqual(inst) } it("Map value of long must work") { val inst = MapHolder[String, Long](Map("w" -> 3L, "y" -> 4L)) - val js = sj[MapHolder[String, Long]].toJson(inst) + val sj = sjCodecOf[MapHolder[String, Long]] + val js = sj.toJson(inst) js should matchJson("""{"a":{"w":3,"y":4}}""") + sj.fromJson(js) shouldEqual(inst) } it("Map value of boolean must work") { val inst = MapHolder[String, Boolean](Map("w" -> true, "y" -> false)) - val js = sj[MapHolder[String, Boolean]].toJson(inst) + val sj = sjCodecOf[MapHolder[String, Boolean]] + val js = sj.toJson(inst) js should matchJson("""{"a":{"w":true,"y":false}}""") + 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) } 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 js = sj[MapHolder[String, UUID]].toJson(inst) + 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) } it("Map value of Seq must work") { val inst = MapHolder[String, List[Int]](Map("w" -> List(1, 2), "y" -> List(3, 4))) - val js = sj[MapHolder[String, List[Int]]].toJson(inst) + 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) } 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 js = sj[MapHolder[String, Map[String, Int]]].toJson(inst) + 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) } + 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) + } + /* + * 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))) val js = sj[MapHolder[String, Person]].toJson(inst) js should matchJson("""{"a":{"w":{"name":"Bob","age":34},"y":{"name":"Sally","age":25}}}""") } - it("Map value of union type must work") { - val inst = MapHolder[String, Int | List[String]](Map("w" -> 3, "y" -> List("wow", "blah"))) - val js = sj[MapHolder[String, Int | List[String]]].toJson(inst) - js should matchJson("""{"a":{"w":3,"y":["wow","blah"]}}""") - } it("Map value of value class must work") { val inst = MapHolder[String, Distance](Map("w" -> new Distance(1.23), "y" -> Distance(4.56))) val js = sj[MapHolder[String, Distance]].toJson(inst) @@ -88,47 +121,14 @@ class MapSpec() extends AnyFunSpec with JsonMatchers: } it("Mutable Map value must work") { val inst = MMapHolder[String, Distance](scala.collection.mutable.HashMap("w" -> new Distance(1.23), "y" -> Distance(4.56))) - val js = sj[MMapHolder[String, Distance]].toJson(inst) + val sj = sjCodecOf[MapHolder[String, Distance]] + val js = sj.toJson(inst) js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual(inst) } - - it("Enum as Map key and value must work") { - val inst = MapHolder[Color, Color](Map(Color.Red -> Color.Blue, Color.Green -> Color.Red)) - val js = sj[MapHolder[Color, Color]].toJson(inst) - js should matchJson("""{"a":{"Red":"Blue","Green":"Red"}}""") - } - it("Enum as Map key and value must work (using id)") { - val inst = MapHolder[Color, Color](Map(Color.Red -> Color.Blue, Color.Green -> Color.Red)) - val js = sj[MapHolder[Color, Color]](JsonConfig.withEnumsAsIds(Some(Nil))).toJson(inst) - js should matchJson("""{"a":{"0":2,"1":0}}""") - } - it("Enumeration as Map key and value must work") { - import Permissions.* - val inst = MapHolder[Permissions, Permissions](Map(Permissions.READ -> Permissions.WRITE, Permissions.EXEC -> Permissions.NONE)) - val js = sj[MapHolder[Permissions, Permissions]].toJson(inst) - js should matchJson("""{"a":{"READ":"WRITE","EXEC":"NONE"}}""") - } - it("Enumeration as Map key and value must work (using id)") { - import Permissions.* - val inst = MapHolder[Permissions, Permissions](Map(Permissions.READ -> Permissions.WRITE, Permissions.EXEC -> Permissions.NONE)) - val js = sj[MapHolder[Permissions, Permissions]](JsonConfig.withEnumsAsIds(Some(Nil))).toJson(inst) - js should matchJson("""{"a":{"0":1,"2":3}}""") - } - it("Java Enumeration as Map key and value must work") { - val inst = MapHolder[CarEnum, CarEnum](Map(CarEnum.VW -> CarEnum.PORSCHE, CarEnum.PORSCHE -> CarEnum.TOYOTA)) - val js = sj[MapHolder[CarEnum, CarEnum]].toJson(inst) - js should matchJson("""{"a":{"VW":"PORSCHE","PORSCHE":"TOYOTA"}}""") - } - it("Java Enumeration as Map key and value must work (using id)") { - val inst = MapHolder[CarEnum, CarEnum](Map(CarEnum.VW -> CarEnum.PORSCHE, CarEnum.PORSCHE -> CarEnum.TOYOTA)) - val js = sj[MapHolder[CarEnum, CarEnum]](JsonConfig.withEnumsAsIds(Some(Nil))).toJson(inst) - js should matchJson("""{"a":{"1":2,"2":0}}""") - } - it("Enum/Enumeration mix of enum as value must work") { - import Permissions.* - val inst = MapHolder[Color, Permissions](Map(Color.Red -> Permissions.WRITE, Color.Blue -> Permissions.NONE)) - val js = sj[MapHolder[Color, Permissions]](JsonConfig.withEnumsAsIds(Some(List("co.blocke.scalajack.json.collections.Color")))).toJson(inst) - js should matchJson("""{"a":{"0":"WRITE","2":"NONE"}}""") - } + */ } + + // describe(colorString("--- Negative Tests ---")) { + // } } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/Model.scala b/src/test/scala/co.blocke.scalajack/json/collections/Model.scala index ed1d450e..79ea22b0 100644 --- a/src/test/scala/co.blocke.scalajack/json/collections/Model.scala +++ b/src/test/scala/co.blocke.scalajack/json/collections/Model.scala @@ -6,6 +6,9 @@ import java.util.{ArrayList, Map as JMap, Set as JSet} case class Person(name: String, age: Int) +opaque type OnOff = Boolean +opaque type Counter = Short + case class SeqHolder[T](a: Seq[T]) case class SetHolder[T](a: Set[T]) case class ArrayHolder[T](a: Array[T]) diff --git a/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala new file mode 100644 index 00000000..25017c8d --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala @@ -0,0 +1,59 @@ +package co.blocke.scalajack +package json +package collections + +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.util.UUID + +class TupleSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Tuple Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Tuple is null must work") { + val inst = TupleHolder[Int, String, Boolean](null) + val sj = sjCodecOf[TupleHolder[Int, String, Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual inst + } + it("Tuple of simple types must work") { + val inst = TupleHolder[Int, String, Boolean]((15, "wow", true)) + val sj = sjCodecOf[TupleHolder[Int, String, Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[15,"wow",true]}""") + sj.fromJson(js) shouldEqual inst + } + // it("Tuple of collecitons (including another tuple) must work") { + // val inst = TupleHolder[Seq[Int], Map[String, Long], (Double, Char, Boolean)]((List(1, 2), Map("a" -> 3L, "b" -> 4L), (1.23d, 'X', true))) + // val js = sjCodecOf[TupleHolder[Seq[Int], Map[String, Long], (Double, Char, Boolean)]].toJson(inst) + // js should matchJson("""{"a":[[1,2],{"a":3,"b":4},[1.23,"X",true]]}""") + // } + } + + describe(colorString("--- Negative Tests ---")) { + it("Wrong number of elements in a tuple") { + val js = """{"a":[15,"wow",true,12.34]}""" + val msg = + """Expected ']' here at position [19] + |{"a":[15,"wow",true,12.34]} + |-------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[TupleHolder[Int, String, Boolean]].fromJson(js)) + ex.show shouldEqual msg + } + it("Wrong type of elements in tuple") { + val js = """{"a":[15,true,true]}""" + val msg = + """Expected a String value but got 't' at position [9] + |{"a":[15,true,true]} + |---------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[TupleHolder[Int, String, Boolean]].fromJson(js)) + ex.show shouldEqual msg + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scalax b/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scalax deleted file mode 100644 index ef31ceab..00000000 --- a/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scalax +++ /dev/null @@ -1,34 +0,0 @@ -package co.blocke.scalajack -package json -package collections - -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.util.UUID - -class TupleSpec() extends AnyFunSpec with JsonMatchers: - - describe(colorString("-------------------------------\n: Tuple Tests :\n-------------------------------", Console.YELLOW)) { - describe(colorString("+++ Positive Tests +++")) { - it("Tuple is null must work") { - val inst = TupleHolder[Int, String, Boolean](null) - val js = sj[TupleHolder[Int, String, Boolean]].toJson(inst) - js should matchJson("""{"a":null}""") - } - it("Tuple of simple types must work") { - val inst = TupleHolder[Int, String, Boolean]((15, "wow", true)) - val js = sj[TupleHolder[Int, String, Boolean]].toJson(inst) - js should matchJson("""{"a":[15,"wow",true]}""") - } - it("Tuple of collecitons (including another tuple) must work") { - val inst = TupleHolder[Seq[Int], Map[String, Long], (Double, Char, Boolean)]((List(1, 2), Map("a" -> 3L, "b" -> 4L), (1.23d, 'X', true))) - val js = sj[TupleHolder[Seq[Int], Map[String, Long], (Double, Char, Boolean)]].toJson(inst) - js should matchJson("""{"a":[[1,2],{"a":3,"b":4},[1.23,"X",true]]}""") - } - } - } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/EnumSpec.scalax b/src/test/scala/co.blocke.scalajack/json/misc/EnumSpec.scalax new file mode 100644 index 00000000..07d2dd73 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/EnumSpec.scalax @@ -0,0 +1,54 @@ +TBD + +Test: + +* Enumeration +* Enum +* Java Enumeration +* Sealed trait w/case object +* Sealed trait w/case classes + +* Try permutations of Map having keys of each of the above types, including ordinal (int) values where available + + +Partial: + + + it("Enum as Map key and value must work") { + val inst = MapHolder[Color, Color](Map(Color.Red -> Color.Blue, Color.Green -> Color.Red)) + val js = sj[MapHolder[Color, Color]].toJson(inst) + js should matchJson("""{"a":{"Red":"Blue","Green":"Red"}}""") + } + it("Enum as Map key and value must work (using id)") { + val inst = MapHolder[Color, Color](Map(Color.Red -> Color.Blue, Color.Green -> Color.Red)) + val js = sj[MapHolder[Color, Color]](JsonConfig.withEnumsAsIds(Some(Nil))).toJson(inst) + js should matchJson("""{"a":{"0":2,"1":0}}""") + } + it("Enumeration as Map key and value must work") { + import Permissions.* + val inst = MapHolder[Permissions, Permissions](Map(Permissions.READ -> Permissions.WRITE, Permissions.EXEC -> Permissions.NONE)) + val js = sj[MapHolder[Permissions, Permissions]].toJson(inst) + js should matchJson("""{"a":{"READ":"WRITE","EXEC":"NONE"}}""") + } + it("Enumeration as Map key and value must work (using id)") { + import Permissions.* + val inst = MapHolder[Permissions, Permissions](Map(Permissions.READ -> Permissions.WRITE, Permissions.EXEC -> Permissions.NONE)) + val js = sj[MapHolder[Permissions, Permissions]](JsonConfig.withEnumsAsIds(Some(Nil))).toJson(inst) + js should matchJson("""{"a":{"0":1,"2":3}}""") + } + it("Java Enumeration as Map key and value must work") { + val inst = MapHolder[CarEnum, CarEnum](Map(CarEnum.VW -> CarEnum.PORSCHE, CarEnum.PORSCHE -> CarEnum.TOYOTA)) + val js = sj[MapHolder[CarEnum, CarEnum]].toJson(inst) + js should matchJson("""{"a":{"VW":"PORSCHE","PORSCHE":"TOYOTA"}}""") + } + it("Java Enumeration as Map key and value must work (using id)") { + val inst = MapHolder[CarEnum, CarEnum](Map(CarEnum.VW -> CarEnum.PORSCHE, CarEnum.PORSCHE -> CarEnum.TOYOTA)) + val js = sj[MapHolder[CarEnum, CarEnum]](JsonConfig.withEnumsAsIds(Some(Nil))).toJson(inst) + js should matchJson("""{"a":{"1":2,"2":0}}""") + } + it("Enum/Enumeration mix of enum as value must work") { + import Permissions.* + val inst = MapHolder[Color, Permissions](Map(Color.Red -> Permissions.WRITE, Color.Blue -> Permissions.NONE)) + val js = sj[MapHolder[Color, Permissions]](JsonConfig.withEnumsAsIds(Some(List("co.blocke.scalajack.json.collections.Color")))).toJson(inst) + js should matchJson("""{"a":{"0":"WRITE","2":"NONE"}}""") + } \ No newline at end of file diff --git a/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala new file mode 100644 index 00000000..289f5df7 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala @@ -0,0 +1,125 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +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. + } + 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 + } + 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)) + } + // 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)))) + // val js = sj[LRHolder[Try[Option[Int]], String]].toJson(inst) + // js should matchJson("""{"a":[5,"x"],"b":["y",10]}""") + // } + // it("LR (union) must work with Try of Option (Success(None))") { + // val inst = LRHolder[Try[Option[Int]], String](List(Success(None), "x"), ("y", Success(None))) + // val js = sj[LRHolder[Try[Option[Int]], String]].toJson(inst) + // js should matchJson("""{"a":["x"],"b":["y",null]}""") + // } + // it("LR (union) must work with Try of Option (Failure)") { + // val inst = LRHolder[Try[Option[Int]], String](List(Failure(new Exception("boom")), "x"), ("y", Failure(new Exception("boom2")))) + // 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 aae61c3b..8d1c122e 100644 --- a/src/test/scala/co.blocke.scalajack/json/misc/Model.scala +++ b/src/test/scala/co.blocke.scalajack/json/misc/Model.scala @@ -24,7 +24,7 @@ case class OptionHolder[T]( case class TryHolder[T](a: Try[T]) case class TryHolder2[T](a: Seq[Try[T]], b: (Try[T], Try[T])) -case class LRHolder[T, U](a: Seq[T | U], b: (T | U, T | U)) +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]]]) @@ -62,3 +62,31 @@ case class AnyHolder( whichOneL: Any, // Either[String,Int] <- left bunch: Any // (Some('a'),None,Some('b')) ) + +object Size extends Enumeration { + val Small, Medium, Large = Value +} +object SizeWithType extends Enumeration { + type SizeWithType = Value + val Little, Grand = Value +} +import SizeWithType.* +case class SampleEnum(e1: Size.Value, e2: Size.Value, e3: Size.Value, e4: Size.Value, e5: Size.Value, e6: SizeWithType) + +enum Color { + case Red, Blue, Green +} +case class TVColors(color1: Color, color2: Color) + +sealed trait Flavor +case object Vanilla extends Flavor +case object Chocolate extends Flavor +case object Bourbon extends Flavor + +sealed trait Vehicle +case class Truck(numberOfWheels: Int) extends Vehicle +case class Car(numberOfWheels: Int, color: String) extends Vehicle +case class Plane(numberOfEngines: Int) extends Vehicle + +case class Ride(wheels: Vehicle) +case class Favorite(flavor: Flavor) diff --git a/src/test/scala/co.blocke.scalajack/json/misc/OptionLRTrySpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/OptionLRTrySpec.scala deleted file mode 100644 index 1165e091..00000000 --- a/src/test/scala/co.blocke.scalajack/json/misc/OptionLRTrySpec.scala +++ /dev/null @@ -1,244 +0,0 @@ -package co.blocke.scalajack -package json -package misc - -import ScalaJack.* -import co.blocke.scala_reflection.* -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers.* -import org.scalatest.* -import scala.util.* -import TestUtil.* - -import java.util.UUID - -class OptionLRTrySpec() extends AnyFunSpec with JsonMatchers: - - /* - describe(colorString("-------------------------------\n: Option Tests :\n-------------------------------", Console.YELLOW)) { - describe(colorString("+++ Positive Tests +++")) { - it("Non-empty Options must work") { - val inst = OptionHolder[Int]( - Some(5), // straight Option - (Some(1), "ok"), // tuple w/Option - List(Some(1), None, Some(3)), // Seq of Option - Map(1 -> Some(2), 3 -> Some(4)), // Map of Option - Some(99), // Union of Option (R) - Some(100), // Union of Option (L) - Some(Some(0)), // Nested Option - Some(Person("BoB", 34)), // Option of class - 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}""") - } - it("Empty Options must work (default)") { - val inst = OptionHolder[Int]( - None, // straight Option - (None, "ok"), // tuple w/Option - List(None, None, None), // Seq of Option - Map(1 -> None, 3 -> None), // Map of Option - None, // Union of Option (R) - None, // Union of Option (L) - Some(None), // Nested Option - None, // Option of class - Right(None), // Either of Option (R) - Left(None) // Either of Option (L) - ) - val js = sjCodecOf[OptionHolder[Int]].toJson(inst) - js should matchJson("""{"b":[null,"ok"],"c":[],"d":{}}""") - } - /* - it("Empty Options must work (config noneAsNull = true)") { - val inst = OptionHolder[Int]( - None, // straight Option - (None, "ok"), // tuple w/Option - List(None, None, None), // Seq of Option - Map(1 -> None, 3 -> None), // Map of Option - None, // Union of Option (R) - None, // Union of Option (L) - Some(None), // Nested Option - None, // Option of class - 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) - 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}""") - } - */ - } - } - */ - - describe(colorString("-------------------------------\n: Either Tests :\n-------------------------------", Console.YELLOW)) { - describe(colorString("+++ Positive 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 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. - } - 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: LR Tests :\n-------------------------------", Console.YELLOW)) { - describe(colorString("+++ Positive Tests +++")) { - it("LR (union) must work with Option (non-None)") { - val inst = LRHolder[Option[Int], String](List(Some(5), "x"), ("y", Some(10))) - val js = sj[LRHolder[Option[Int], String]].toJson(inst) - js should matchJson("""{"a":[5,"x"],"b":["y",10]}""") - } - it("LR (union) must work with Option (None)") { - val inst = LRHolder[Option[Int], String](List(None, "x"), ("y", None)) - val js = sj[LRHolder[Option[Int], String]].toJson(inst) - js should matchJson("""{"a":["x"],"b":["y",null]}""") - } - 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)))) - val js = sj[LRHolder[Try[Option[Int]], String]].toJson(inst) - js should matchJson("""{"a":[5,"x"],"b":["y",10]}""") - } - it("LR (union) must work with Try of Option (Success(None))") { - val inst = LRHolder[Try[Option[Int]], String](List(Success(None), "x"), ("y", Success(None))) - val js = sj[LRHolder[Try[Option[Int]], String]].toJson(inst) - js should matchJson("""{"a":["x"],"b":["y",null]}""") - } - it("LR (union) must work with Try of Option (Failure)") { - val inst = LRHolder[Try[Option[Int]], String](List(Failure(new Exception("boom")), "x"), ("y", Failure(new Exception("boom2")))) - val js = sj[LRHolder[Try[Option[Int]], String]].toJson(inst) - js should matchJson("""{"a":["x"],"b":["y",null]}""") - } - } - } - - describe(colorString("-------------------------------\n: Try Tests :\n-------------------------------", Console.YELLOW)) { - describe(colorString("+++ Positive Tests +++")) { - it("Try must work (Success)") { - val inst = TryHolder(Success(15)) - val js = sj[TryHolder[Int]].toJson(inst) - js should matchJson("""{"a":15}""") - } - it("Try of Option (non-None) must work (Success)") { - val inst = TryHolder[Option[Int]](Success(Some(15))) - val js = sj[TryHolder[Option[Int]]].toJson(inst) - js should matchJson("""{"a":15}""") - } - it("Try of Option (None) must work (Success)") { - val inst = TryHolder[Option[Int]](Success(None)) - val js = sj[TryHolder[Option[Int]]].toJson(inst) - js should matchJson("""{}""") - } - it("Try w/policy AS_NULL must work (Failure)") { - val inst = TryHolder[Int](Failure(new Exception("boom"))) - val js = sj[TryHolder[Int]](JsonConfig.withTryFailureHandling(TryPolicy.AS_NULL)).toJson(inst) - js should matchJson("""{"a":null}""") - } - it("Try w/policy NO_WRITE must work (Failure)") { - val inst = TryHolder[Int](Failure(new Exception("boom"))) - val js = sj[TryHolder[Int]](JsonConfig.withTryFailureHandling(TryPolicy.NO_WRITE)).toJson(inst) - js should matchJson("""{}""") - } - it("Try w/policy ERR_MSG_STRING must work (Failure)") { - val inst = TryHolder[Int](Failure(new Exception("boom"))) - val js = sj[TryHolder[Int]](JsonConfig.withTryFailureHandling(TryPolicy.ERR_MSG_STRING)).toJson(inst) - js should matchJson("""{"a":"Try Failure with msg: boom"}""") - } - it("Try w/policy ATHROW_EXCEPTIONS_NULL must work (Failure)") { - val inst = TryHolder[Int](Failure(new Exception("boom"))) - val caught = - intercept[java.lang.Exception] { - sj[TryHolder[Int]](JsonConfig.withTryFailureHandling(TryPolicy.THROW_EXCEPTION)).toJson(inst) - } - assert(caught.getMessage == "boom") - } - it("Seq and Tuple of Try must work for AS_NULL (Failure)") { - val inst = TryHolder2[Int](List(Success(1), Failure(new Exception("boom")), Success(3)), (Failure(new Exception("boom")), Success(0))) - val js = sj[TryHolder2[Int]](JsonConfig.withTryFailureHandling(TryPolicy.AS_NULL)).toJson(inst) - js should matchJson("""{"a":[1,null,3],"b":[null,0]}""") - } - it("Seq and Tuple of Try must work for NO_WRITE (Failure)") { - val inst = TryHolder2[Int](List(Success(1), Failure(new Exception("boom")), Success(3)), (Failure(new Exception("boom")), Success(0))) - val js = sj[TryHolder2[Int]](JsonConfig.withTryFailureHandling(TryPolicy.NO_WRITE)).toJson(inst) - js should matchJson("""{"a":[1,3],"b":[null,0]}""") - } - it("Seq and Tuple of Try of an Option must work for NO_WRITE (Failure)") { - val inst = TryHolder2[Option[Int]](List(Success(None), Failure(new Exception("boom")), Success(Some(3))), (Failure(new Exception("boom")), Success(None))) - val js = sj[TryHolder2[Option[Int]]](JsonConfig.withTryFailureHandling(TryPolicy.NO_WRITE)).toJson(inst) - js should matchJson("""{"a":[3],"b":[null,null]}""") - } - } - } - */ diff --git a/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala new file mode 100644 index 00000000..fabcda59 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala @@ -0,0 +1,73 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +import java.util.UUID + +class OptionTrySpec() 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 + (Some(1), "ok"), // tuple w/Option + List(Some(1), None, Some(3)), // Seq of Option + Map(1 -> Some(2), 3 -> Some(4)), // Map of Option + Some(99), // Union of Option (R) + Some(100), // Union of Option (L) + Some(Some(0)), // Nested Option + Some(Person("BoB", 34)), // Option of class + 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}""") + } + it("Empty Options must work (default)") { + val inst = OptionHolder[Int]( + None, // straight Option + (None, "ok"), // tuple w/Option + List(None, None, None), // Seq of Option + Map(1 -> None, 3 -> None), // Map of Option + None, // Union of Option (R) + None, // Union of Option (L) + Some(None), // Nested Option + None, // Option of class + Right(None), // Either of Option (R) + Left(None) // Either of Option (L) + ) + val js = sjCodecOf[OptionHolder[Int]].toJson(inst) + js should matchJson("""{"b":[null,"ok"],"c":[],"d":{}}""") + } + it("Empty Options must work (config noneAsNull = true)") { + val inst = OptionHolder[Int]( + None, // straight Option + (None, "ok"), // tuple w/Option + List(None, None, None), // Seq of Option + Map(1 -> None, 3 -> None), // Map of Option + None, // Union of Option (R) + None, // Union of Option (L) + Some(None), // Nested Option + None, // Option of class + 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) + 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}""") + } + it("Java Optional must work") { + ??? + } + */ + } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scalax b/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scalax new file mode 100644 index 00000000..efcdb1b9 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scalax @@ -0,0 +1,73 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +import java.util.UUID + +class TrySpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Try Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Try must work (Success)") { + val inst = TryHolder(Success(15)) + val js = sj[TryHolder[Int]].toJson(inst) + js should matchJson("""{"a":15}""") + } + it("Try of Option (non-None) must work (Success)") { + val inst = TryHolder[Option[Int]](Success(Some(15))) + val js = sj[TryHolder[Option[Int]]].toJson(inst) + js should matchJson("""{"a":15}""") + } + it("Try of Option (None) must work (Success)") { + val inst = TryHolder[Option[Int]](Success(None)) + val js = sj[TryHolder[Option[Int]]].toJson(inst) + js should matchJson("""{}""") + } + it("Try w/policy AS_NULL must work (Failure)") { + val inst = TryHolder[Int](Failure(new Exception("boom"))) + val js = sj[TryHolder[Int]](JsonConfig.withTryFailureHandling(TryPolicy.AS_NULL)).toJson(inst) + js should matchJson("""{"a":null}""") + } + it("Try w/policy NO_WRITE must work (Failure)") { + val inst = TryHolder[Int](Failure(new Exception("boom"))) + val js = sj[TryHolder[Int]](JsonConfig.withTryFailureHandling(TryPolicy.NO_WRITE)).toJson(inst) + js should matchJson("""{}""") + } + it("Try w/policy ERR_MSG_STRING must work (Failure)") { + val inst = TryHolder[Int](Failure(new Exception("boom"))) + val js = sj[TryHolder[Int]](JsonConfig.withTryFailureHandling(TryPolicy.ERR_MSG_STRING)).toJson(inst) + js should matchJson("""{"a":"Try Failure with msg: boom"}""") + } + it("Try w/policy ATHROW_EXCEPTIONS_NULL must work (Failure)") { + val inst = TryHolder[Int](Failure(new Exception("boom"))) + val caught = + intercept[java.lang.Exception] { + sj[TryHolder[Int]](JsonConfig.withTryFailureHandling(TryPolicy.THROW_EXCEPTION)).toJson(inst) + } + assert(caught.getMessage == "boom") + } + it("Seq and Tuple of Try must work for AS_NULL (Failure)") { + val inst = TryHolder2[Int](List(Success(1), Failure(new Exception("boom")), Success(3)), (Failure(new Exception("boom")), Success(0))) + val js = sj[TryHolder2[Int]](JsonConfig.withTryFailureHandling(TryPolicy.AS_NULL)).toJson(inst) + js should matchJson("""{"a":[1,null,3],"b":[null,0]}""") + } + it("Seq and Tuple of Try must work for NO_WRITE (Failure)") { + val inst = TryHolder2[Int](List(Success(1), Failure(new Exception("boom")), Success(3)), (Failure(new Exception("boom")), Success(0))) + val js = sj[TryHolder2[Int]](JsonConfig.withTryFailureHandling(TryPolicy.NO_WRITE)).toJson(inst) + js should matchJson("""{"a":[1,3],"b":[null,0]}""") + } + it("Seq and Tuple of Try of an Option must work for NO_WRITE (Failure)") { + val inst = TryHolder2[Option[Int]](List(Success(None), Failure(new Exception("boom")), Success(Some(3))), (Failure(new Exception("boom")), Success(None))) + val js = sj[TryHolder2[Option[Int]]](JsonConfig.withTryFailureHandling(TryPolicy.NO_WRITE)).toJson(inst) + js should matchJson("""{"a":[3],"b":[null,null]}""") + } + } + } 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 8ce7b642..db85415f 100644 --- a/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala +++ b/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala @@ -8,34 +8,6 @@ import java.math.{BigDecimal as JBigDecimal, BigInteger as JBigInteger} import java.time.* import scala.math.* -object Size extends Enumeration { - val Small, Medium, Large = Value -} -object SizeWithType extends Enumeration { - type SizeWithType = Value - val Little, Grand = Value -} -import SizeWithType.* -case class SampleEnum(e1: Size.Value, e2: Size.Value, e3: Size.Value, e4: Size.Value, e5: Size.Value, e6: SizeWithType) - -enum Color { - case Red, Blue, Green -} -case class TVColors(color1: Color, color2: Color) - -sealed trait Flavor -case object Vanilla extends Flavor -case object Chocolate extends Flavor -case object Bourbon extends Flavor - -sealed trait Vehicle -case class Truck(numberOfWheels: Int) extends Vehicle -case class Car(numberOfWheels: Int, color: String) extends Vehicle -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) @@ -84,6 +56,14 @@ case class SampleZoneOffset(z1: ZoneOffset, z2: ZoneOffset) // === Any primitives case class AnyShell(a: Any) +object Size extends Enumeration { + val Small, Medium, Large = Value +} + +enum Color { + case Red, Blue, Green +} + // === Value Classes case class VCBigDecimal(vc: BigDecimal) extends AnyVal case class VCBigInt(vc: BigInt) extends AnyVal