diff --git a/src/main/java/co/blocke/scalajack/AESEncryptionDecryption.java b/src/main/java/co/blocke/scalajack/AESEncryptionDecryption.java new file mode 100644 index 00000000..b03ebddb --- /dev/null +++ b/src/main/java/co/blocke/scalajack/AESEncryptionDecryption.java @@ -0,0 +1,50 @@ +package co.blocke.scalajack; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.InvalidKeyException; +import java.io.UnsupportedEncodingException; +import javax.crypto.IllegalBlockSizeException; +import java.util.Arrays; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.BadPaddingException; + +/** + * Java String Encryption Decryption Example + * @author Ramesh Fadatare + * + */ +public class AESEncryptionDecryption { + private static SecretKeySpec secretKey; + private static byte[] key; + private static final String ALGORITHM = "AES"; + + public void prepareSecreteKey(String myKey) throws NoSuchAlgorithmException { + MessageDigest sha = null; + key = myKey.getBytes(StandardCharsets.UTF_8); + sha = MessageDigest.getInstance("SHA-1"); + key = sha.digest(key); + key = Arrays.copyOf(key, 16); + secretKey = new SecretKeySpec(key, ALGORITHM); + } + + public String encrypt(String strToEncrypt, String secret) + throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException, IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException { + prepareSecreteKey(secret); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8"))); + } + + public String decrypt(String strToDecrypt, String secret) throws NoSuchAlgorithmException, NoSuchPaddingException, BadPaddingException, InvalidKeyException, IllegalBlockSizeException { + prepareSecreteKey(secret); + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey); + return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt))); + } +} \ No newline at end of file diff --git a/src/main/java/co/blocke/scalajack/TypeHint.java b/src/main/java/co/blocke/scalajack/TypeHint.java new file mode 100644 index 00000000..9d958b9c --- /dev/null +++ b/src/main/java/co/blocke/scalajack/TypeHint.java @@ -0,0 +1,8 @@ +package co.blocke.scalajack; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeHint{} + diff --git a/src/main/scala/co.blocke.scalajack/json/JsonWriter.scala b/src/main/scala/co.blocke.scalajack/json/JsonWriter.scala index 4ec474d1..869240c1 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonWriter.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonWriter.scala @@ -3,7 +3,7 @@ package json import scala.quoted.* import co.blocke.scala_reflection.* -import co.blocke.scala_reflection.rtypes.{EnumRType, ScalaClassRType, TraitRType} +import co.blocke.scala_reflection.rtypes.{EnumRType, NonConstructorFieldInfo, ScalaClassRType, TraitRType} import co.blocke.scala_reflection.reflect.{ReflectOnType, TypeSymbolMapper} import co.blocke.scala_reflection.reflect.rtypeRefs.* import scala.jdk.CollectionConverters.* @@ -12,8 +12,8 @@ import scala.quoted.staging.* /* TODO: - [ ] - Scala non-case class - [ ] - Java class (Do I still want to support this???) + [*] - Scala non-case class + [*] - Java class (Do I still want to support this???) [*] - Enum [*] - Enumeration [*] - Java Enum @@ -25,16 +25,22 @@ import scala.quoted.staging.* [*] - Object (How???) [*] - Trait (How???) [*] - Sealed trait - [ ] - sealed abstract class (handle like sealed trait....) + [*] - sealed abstract class (handle like sealed trait....) [*] - SelfRef [*] - Tuple [*] - Unknown (throw exception) [*] - Scala 2 (throw exception) [*] - TypeSymbol (throw exception) + [*] - Value class - [ ] -- correct all the 'if $aE == null...' - [ ] -- type hint label mapping - [ ] -- type hint value mapping + [*] -- correct all the 'if $aE == null...' + [*] -- type hint label mapping + [*] -- type hint value mapping + [*] -- Discontinue use of inTermsOf "open" (non-sealed) trait support (?!) + [*] -- update runtime-size TraitRType handling to match new compile-time code + + [ ] -- Streaming JSON write support + [ ] -- BigJSON support (eg. multi-gig file) */ object JsonWriter: @@ -45,6 +51,7 @@ object JsonWriter: def isOkToWrite(a: Any, cfg: JsonConfig) = a match case None if !cfg.noneAsNull => false + case o: java.util.Optional[?] if o.isEmpty && !cfg.noneAsNull => false case Left(None) if !cfg.noneAsNull => false case Right(None) if !cfg.noneAsNull => false case Failure(_) if cfg.tryFailureHandling == TryOption.NO_WRITE => false @@ -62,15 +69,16 @@ object JsonWriter: ref match case t: PrimitiveRef[?] if t.family == PrimFamily.Stringish => '{ if $aE == null then $sbE.append("null") else $sbE.append("\"" + $aE.toString + "\"") } case t: PrimitiveRef[?] => + val isNullable = Expr(t.isNullable) if isMapKey then '{ - if $aE == null then $sbE.append("\"null\"") + if $isNullable && $aE == null then $sbE.append("\"null\"") else $sbE.append('"') $sbE.append($aE.toString) $sbE.append('"') } - else '{ if $aE == null then $sbE.append("null") else $sbE.append($aE.toString) } + else '{ if $isNullable && $aE == null then $sbE.append("null") else $sbE.append($aE.toString) } case t: SeqRef[?] => if isMapKey then throw new JsonError("Seq instances cannot be map keys") @@ -117,8 +125,44 @@ object JsonWriter: else sb.setCharAt(sb.length() - 1, ']') } - case t: ClassRef[?] => + case t: ScalaClassRef[?] if t.isSealed && t.isAbstractClass => + classesSeen.put(t.typedName, t) + if t.childrenAreObject then + // case object -> just write the simple name of the object + '{ + if $aE == null then $sbE.append("null") + else + $sbE.append('"') + $sbE.append(lastPart($aE.getClass.getName)) + $sbE.append('"') + } + else + // Wow! If this is a sealed trait, all the children already have correctly-typed parameters--no need for expensive inTermsOf() + val rt = t.expr.asInstanceOf[Expr[ScalaClassRType[T]]] + '{ + if $aE == null then $sbE.append("null") + else + val className = $aE.getClass.getName + $rt.sealedChildren + .find(_.name == className) + .map(foundKid => + val augmented = foundKid.asInstanceOf[ScalaClassRType[foundKid.T]].copy(renderTrait = Some($rt.name)).asInstanceOf[RType[foundKid.T]] + JsonWriterRT.refWriteRT[foundKid.T]($cfgE, augmented, $aE.asInstanceOf[foundKid.T], $sbE)(using scala.collection.mutable.Map.empty[TypedName, RType[?]]) + ) + .getOrElse(throw new JsonError(s"Unrecognized child $className of seald trait " + $rt.name)) + } + + case t: ScalaClassRef[?] if t.isValueClass => + val theField = t.fields.head.fieldRef + theField.refType match + case '[e] => + val fieldValue = Select.unique(aE.asTerm, t.fields.head.name).asExprOf[e] + refWrite[e](cfgE, theField.asInstanceOf[RTypeRef[e]], fieldValue, sbE) + + case t: ScalaClassRef[?] => + if t.isAbstractClass then throw new JsonError("Cannot serialize an abstract class") classesSeen.put(t.typedName, t) + val isCase = Expr(t.isCaseClass) '{ val sb = $sbE if $aE == null then sb.append("null") @@ -144,6 +188,26 @@ object JsonWriter: } } } + if ! $isCase && $cfgE.writeNonConstructorFields then + ${ + t.nonConstructorFields.foldLeft('{ sb }) { (accE, f) => + f.fieldRef.refType match + case '[e] => + val fieldValue = Select.unique(aE.asTerm, f.getterLabel).asExprOf[e] + val name = Expr(f.name) + '{ + val acc = $accE + if isOkToWrite($fieldValue, $cfgE) then + acc.append('"') + acc.append($name) + acc.append('"') + acc.append(':') + ${ refWrite[e](cfgE, f.fieldRef.asInstanceOf[RTypeRef[e]], fieldValue, '{ acc }) } + acc.append(',') + else acc + } + } + } if sbLen == sb.length then sb.append('}') else sb.setCharAt(sb.length() - 1, '}') } @@ -154,45 +218,53 @@ object JsonWriter: if t.childrenAreObject then // case object -> just write the simple name of the object '{ - $sbE.append('"') - $sbE.append(lastPart($aE.getClass.getName)) - $sbE.append('"') + if $aE == null then $sbE.append("null") + else + $sbE.append('"') + $sbE.append(lastPart($aE.getClass.getName)) + $sbE.append('"') } else if t.isSealed then // Wow! If this is a sealed trait, all the children already have correctly-typed parameters--no need for expensive inTermsOf() '{ - val className = $aE.getClass.getName - $rt.sealedChildren - .find(_.name == className) - .map(foundKid => - val augmented = foundKid.asInstanceOf[ScalaClassRType[foundKid.T]].copy(renderTrait = Some($rt.name)).asInstanceOf[RType[foundKid.T]] - JsonWriterRT.refWriteRT[foundKid.T]($cfgE, augmented, $aE.asInstanceOf[foundKid.T], $sbE)(using scala.collection.mutable.Map.empty[TypedName, RType[?]]) - ) - .getOrElse(throw new JsonError(s"Unrecognized child $className of seald trait " + $rt.name)) - } - else - '{ - given Compiler = Compiler.make($aE.getClass.getClassLoader) - val fn = (q: Quotes) ?=> { - import q.reflect.* - val sb = $sbE - val classRType = RType.inTermsOf[T]($aE.getClass).asInstanceOf[ScalaClassRType[T]].copy(renderTrait = Some($rt.name)).asInstanceOf[RType[T]] - JsonWriterRT.refWriteRT[classRType.T]($cfgE, classRType, $aE.asInstanceOf[classRType.T], $sbE)(using scala.collection.mutable.Map.empty[TypedName, RType[?]]) - Expr(1) // do-nothing... '{} requires Expr(something) be returned, so... - } - quoted.staging.run(fn) - $sbE + if $aE == null then $sbE.append("null") + else + val className = $aE.getClass.getName + $rt.sealedChildren + .find(_.name == className) + .map(foundKid => + val augmented = foundKid.asInstanceOf[ScalaClassRType[foundKid.T]].copy(renderTrait = Some($rt.name)).asInstanceOf[RType[foundKid.T]] + JsonWriterRT.refWriteRT[foundKid.T]($cfgE, augmented, $aE.asInstanceOf[foundKid.T], $sbE)(using scala.collection.mutable.Map.empty[TypedName, RType[?]]) + ) + .getOrElse(throw new JsonError(s"Unrecognized child $className of seald trait " + $rt.name)) } + else throw new JsonError("non-sealed traits are not supported") + // '{ + // if $aE == null then $sbE.append("null") + // else + // given Compiler = Compiler.make($aE.getClass.getClassLoader) + // val fn = (q: Quotes) ?=> { + // import q.reflect.* + // val sb = $sbE + // val classRType = RType.inTermsOf[T]($aE.getClass).asInstanceOf[ScalaClassRType[T]].copy(renderTrait = Some($rt.name)).asInstanceOf[RType[T]] + // JsonWriterRT.refWriteRT[classRType.T]($cfgE, classRType, $aE.asInstanceOf[classRType.T], $sbE)(using scala.collection.mutable.Map.empty[TypedName, RType[?]]) + // Expr(1) // do-nothing... '{} requires Expr(something) be returned, so... + // } + // quoted.staging.run(fn) + // $sbE + // } case t: OptionRef[?] => if isMapKey then throw new JsonError("Option valuess cannot be map keys") t.optionParamType.refType match case '[e] => '{ - $aE match - case None => $sbE.append("null") - case Some(v) => - ${ refWrite[e](cfgE, t.optionParamType.asInstanceOf[RTypeRef[e]], '{ v }.asInstanceOf[Expr[e]], sbE) } + if $aE == null then $sbE.append("null") + else + $aE match + case None => $sbE.append("null") + case Some(v) => + ${ refWrite[e](cfgE, t.optionParamType.asInstanceOf[RTypeRef[e]], '{ v }.asInstanceOf[Expr[e]], sbE) } } case t: MapRef[?] => @@ -258,17 +330,19 @@ object JsonWriter: t.tryRef.refType match case '[e] => '{ - $aE match - case Success(v) => - ${ refWrite[e](cfgE, t.tryRef.asInstanceOf[RTypeRef[e]], '{ v }.asInstanceOf[Expr[e]], sbE) } - case Failure(_) if $cfgE.tryFailureHandling == TryOption.AS_NULL => $sbE.append("null") - case Failure(_) if $cfgE.tryFailureHandling == TryOption.AS_NULL => $sbE.append("null") - case Failure(f) if $cfgE.tryFailureHandling == TryOption.THROW_EXCEPTION => - throw new JsonError("A try value was Failure with message: " + f.getMessage()) - case Failure(v) => - $sbE.append("\"Failure(") - $sbE.append(v.getMessage) - $sbE.append(")\"") + if $aE == null then $sbE.append("null") + else + $aE match + case Success(v) => + ${ refWrite[e](cfgE, t.tryRef.asInstanceOf[RTypeRef[e]], '{ v }.asInstanceOf[Expr[e]], sbE) } + case Failure(_) if $cfgE.tryFailureHandling == TryOption.AS_NULL => $sbE.append("null") + case Failure(_) if $cfgE.tryFailureHandling == TryOption.AS_NULL => $sbE.append("null") + case Failure(f) if $cfgE.tryFailureHandling == TryOption.THROW_EXCEPTION => + throw new JsonError("A try value was Failure with message: " + f.getMessage()) + case Failure(v) => + $sbE.append("\"Failure(") + $sbE.append(v.getMessage) + $sbE.append(")\"") } case t: TupleRef[?] => @@ -355,24 +429,34 @@ object JsonWriter: val enumE = t.expr val isMapKeyE = Expr(isMapKey) '{ - val enumRT = $enumE.asInstanceOf[EnumRType[T]] - val enumAsId = $cfgE.enumsAsIds match - case '*' => true - case aList: List[String] if aList.contains(enumRT.name) => true - case _ => false - if enumAsId then - val enumVal = enumRT.ordinal($aE.toString).getOrElse(throw new JsonError("Value " + $aE.toString + s" is not a valid enum value for ${enumRT.name}")) - if $isMapKeyE then + if $aE == null then $sbE.append("null") + else + val enumRT = $enumE.asInstanceOf[EnumRType[T]] + val enumAsId = $cfgE.enumsAsIds match + case '*' => true + case aList: List[String] if aList.contains(enumRT.name) => true + case _ => false + if enumAsId then + val enumVal = enumRT.ordinal($aE.toString).getOrElse(throw new JsonError("Value " + $aE.toString + s" is not a valid enum value for ${enumRT.name}")) + if $isMapKeyE then + $sbE.append('"') + $sbE.append(enumVal.toString) + $sbE.append('"') + else $sbE.append(enumVal.toString) + else $sbE.append('"') - $sbE.append(enumVal.toString) + $sbE.append($aE.toString) $sbE.append('"') - else $sbE.append(enumVal.toString) - else - $sbE.append('"') - $sbE.append($aE.toString) - $sbE.append('"') } + // Just handle Java classes 100% runtime since we need to leverage Java reflection entirely anyway + case t: JavaClassRef[?] => + t.refType match + case '[e] => + '{ + JsonWriterRT.refWriteRT[e]($cfgE, ${ t.expr }.asInstanceOf[RType[e]], $aE.asInstanceOf[e], $sbE)(using scala.collection.mutable.Map.empty[TypedName, RType[?]]) + } + case t: ObjectRef => val tname = Expr(t.name) '{ @@ -385,7 +469,7 @@ object JsonWriter: $cfgE.undefinedFieldHandling match case UndefinedValueOption.AS_NULL => $sbE.append("null") case UndefinedValueOption.AS_SYMBOL => $sbE.append("\"" + $tname + "\"") - case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Value " + $aE.toString + " is of some unknown/unsupported Scala 2 type " + $tname) + case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Unknown/unsupported Scala 2 type " + $tname) } case t: UnknownRef[?] => @@ -394,7 +478,7 @@ object JsonWriter: $cfgE.undefinedFieldHandling match case UndefinedValueOption.AS_NULL => $sbE.append("null") case UndefinedValueOption.AS_SYMBOL => $sbE.append("\"" + $tname + "\"") - case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Value " + $aE.toString + " is of some unknown/unsupported type " + $tname) + case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Unknown/unsupported type " + $tname) } case t: TypeSymbolRef => @@ -403,5 +487,5 @@ object JsonWriter: $cfgE.undefinedFieldHandling match case UndefinedValueOption.AS_NULL => $sbE.append("null") case UndefinedValueOption.AS_SYMBOL => $sbE.append("\"" + $tname + "\"") - case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Value " + $aE.toString + " is of some unknown/unsupported type " + $tname + ". (Class didn't fully define all its type parameters.)") + case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Unknown/unsupported type " + $tname + ". (Class didn't fully define all its type parameters.)") } diff --git a/src/main/scala/co.blocke.scalajack/json/JsonWriterRT.scala b/src/main/scala/co.blocke.scalajack/json/JsonWriterRT.scala index d217d38f..4ceced2a 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonWriterRT.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonWriterRT.scala @@ -23,14 +23,8 @@ import scala.util.{Failure, Success, Try} object JsonWriterRT: - // Tests whether we should write something or not--mainly in the case of Option, or wrapped Option - def isOkToWrite(a: Any, cfg: JsonConfig) = - a match - case None if !cfg.noneAsNull => false - case Left(None) if !cfg.noneAsNull => false - case Right(None) if !cfg.noneAsNull => false - case Failure(_) if cfg.tryFailureHandling == TryOption.NO_WRITE => false - case _ => true + val secret = "N@rrow !s the w@y" + val encryptionDecryption = new AESEncryptionDecryption() def refWriteRT[T]( cfg: JsonConfig, @@ -40,15 +34,15 @@ object JsonWriterRT: isMapKey: Boolean = false )(using classesSeen: scala.collection.mutable.Map[TypedName, RType[?]]): StringBuilder = rt match - case StringRType() | CharRType() | JavaCharacterRType() => if a == null then sb.append("null") else sb.append("\"" + a.toString + "\"") + case StringRType(_) | CharRType(_) | JavaCharacterRType(_) => if a == null then sb.append("null") else sb.append("\"" + a.toString + "\"") case t: PrimitiveRType => if isMapKey then - if a == null then sb.append("\"null\"") + if t.isNullable && a == null then sb.append("\"null\"") else sb.append('"') sb.append(a.toString) sb.append('"') - else if a == null then sb.append("null") + else if t.isNullable && a == null then sb.append("null") else sb.append(a.toString) case t: SeqRType[?] => @@ -57,7 +51,7 @@ object JsonWriterRT: else sb.append('[') val sbLen = sb.length a.asInstanceOf[Seq[t.elementType.T]].foldLeft(sb) { (acc, one) => - if isOkToWrite(one, cfg) then + if JsonWriter.isOkToWrite(one, cfg) then refWriteRT[t.elementType.T](cfg, t.elementType.asInstanceOf[RType[t.elementType.T]], one, acc) sb.append(',') else sb @@ -71,7 +65,7 @@ object JsonWriterRT: else sb.append('[') val sbLen = sb.length a.asInstanceOf[Seq[t.elementType.T]].foldLeft(sb) { (acc, one) => - if isOkToWrite(one, cfg) then + if JsonWriter.isOkToWrite(one, cfg) then refWriteRT[t.elementType.T](cfg, t.elementType.asInstanceOf[RType[t.elementType.T]], one, acc) sb.append(',') else sb @@ -79,18 +73,56 @@ object JsonWriterRT: if sbLen == sb.length then sb.append(']') else sb.setCharAt(sb.length() - 1, ']') + case t: ScalaClassRType[?] if t.isSealed && t.isAbstractClass => + classesSeen.put(t.typedName, t) + if a == null then sb.append("null") + else + val className = a.getClass.getName + if t.childrenAreObject then + // case object -> just write the simple name of the object + sb.append('"') + sb.append(JsonWriter.lastPart(className)) + sb.append('"') + else + // Wow! If this is a sealed trait, all the children already have correctly-typed parameters--no need for expensive inTermsOf() + t.sealedChildren + .find(_.name == className) + .map(foundKid => + val augmented = foundKid.asInstanceOf[ScalaClassRType[foundKid.T]].copy(renderTrait = Some(t.name)).asInstanceOf[RType[foundKid.T]] + JsonWriterRT.refWriteRT[foundKid.T](cfg, augmented, a.asInstanceOf[foundKid.T], sb) + ) + .getOrElse(throw new JsonError(s"Unrecognized child $className of seald trait " + t.name)) + case t: ScalaClassRType[?] => + if t.isAbstractClass then throw new JsonError("Cannot serialize an abstract class") classesSeen.put(t.typedName, t) if a == null then sb.append("null") else sb.append('{') val sbLen = sb.length - t.renderTrait.map(traitName => sb.append(s"\"_hint\":\"$traitName\",")) + t.renderTrait.map { traitName => + sb.append('"') + sb.append(cfg.typeHintLabelByTrait.getOrElse(traitName, cfg.typeHintLabel)) + sb.append('"') + sb.append(':') + sb.append('"') + val hint = t.annotations + .get("co.blocke.scalajack.TypeHint") + .map(_ => encryptionDecryption.encrypt(JsonWriter.lastPart(t.name), secret)) // only need lat part of class name because the rest is the same as the parent + .getOrElse { + cfg.typeHintTransformer.get(a.getClass.getName) match + case Some(xform) => xform(a) + case None => cfg.typeHintDefaultTransformer(a.getClass.getName) + } + sb.append(hint) + sb.append('"') + sb.append(',') + } t.fields.foldLeft(sb) { (acc, f) => val m = a.getClass.getMethod(f.name) m.setAccessible(true) val fieldValue = m.invoke(a).asInstanceOf[f.fieldType.T] - if isOkToWrite(fieldValue, cfg) then + if JsonWriter.isOkToWrite(fieldValue, cfg) then acc.append('"') acc.append(f.name) acc.append('"') @@ -99,20 +131,52 @@ object JsonWriterRT: acc.append(',') else acc } + if !t.isCaseClass && cfg.writeNonConstructorFields then + t.nonConstructorFields.foldLeft(sb) { (acc, f) => + val m = a.getClass.getMethod(f.getterLabel) + m.setAccessible(true) + val fieldValue = m.invoke(a).asInstanceOf[f.fieldType.T] + if JsonWriter.isOkToWrite(fieldValue, cfg) then + acc.append('"') + acc.append(f.name) + acc.append('"') + acc.append(':') + refWriteRT[f.fieldType.T](cfg, f.fieldType.asInstanceOf[RType[f.fieldType.T]], fieldValue, acc) + acc.append(',') + else acc + } if sbLen == sb.length then sb.append('}') else sb.setCharAt(sb.length() - 1, '}') case t: TraitRType[?] => classesSeen.put(t.typedName, t) - val classRType = RType.inTermsOf[T](a.getClass).asInstanceOf[ScalaClassRType[T]].copy(renderTrait = Some(t.name)).asInstanceOf[RType[T]] - JsonWriterRT.refWriteRT[classRType.T](cfg, classRType, a.asInstanceOf[classRType.T], sb) + if a == null then sb.append("null") + if t.childrenAreObject then + sb.append('"') + sb.append(JsonWriter.lastPart(a.getClass.getName)) + sb.append('"') + else if t.isSealed then + // Wow! If this is a sealed trait, all the children already have correctly-typed parameters--no need for expensive inTermsOf() + val className = a.getClass.getName + t.sealedChildren + .find(_.name == className) + .map(foundKid => + val augmented = foundKid.asInstanceOf[ScalaClassRType[foundKid.T]].copy(renderTrait = Some(t.name)).asInstanceOf[RType[foundKid.T]] + JsonWriterRT.refWriteRT[foundKid.T](cfg, augmented, a.asInstanceOf[foundKid.T], sb) + ) + .getOrElse(throw new JsonError(s"Unrecognized child $className of seald trait " + t.name)) + else throw new JsonError("non-sealed traits are not supported") + // val classRType = RType.inTermsOf[T](a.getClass).asInstanceOf[ScalaClassRType[T]].copy(renderTrait = Some(t.name)).asInstanceOf[RType[T]] + // JsonWriterRT.refWriteRT[classRType.T](cfg, classRType, a.asInstanceOf[classRType.T], sb) case t: OptionRType[?] => if isMapKey then throw new JsonError("Option valuess cannot be map keys") - a match - case None => sb.append("null") - case Some(v) => - refWriteRT[t.optionParamType.T](cfg, t.optionParamType.asInstanceOf[RType[t.optionParamType.T]], v.asInstanceOf[t.optionParamType.T], sb) + if a == null then sb.append("null") + else + a match + case None => sb.append("null") + case Some(v) => + refWriteRT[t.optionParamType.T](cfg, t.optionParamType.asInstanceOf[RType[t.optionParamType.T]], v.asInstanceOf[t.optionParamType.T], sb) case t: MapRType[?] => if isMapKey then throw new JsonError("Map values cannot be map keys") @@ -121,7 +185,7 @@ object JsonWriterRT: sb.append('{') val sbLen = sb.length a.asInstanceOf[Map[?, ?]].foreach { case (key, value) => - if isOkToWrite(value, cfg) then + if JsonWriter.isOkToWrite(value, cfg) then val b = refWriteRT[t.elementType.T](cfg, t.elementType.asInstanceOf[RType[t.elementType.T]], key.asInstanceOf[t.elementType.T], sb, true) b.append(':') val b2 = refWriteRT[t.elementType2.T](cfg, t.elementType2.asInstanceOf[RType[t.elementType2.T]], value.asInstanceOf[t.elementType2.T], sb) @@ -164,16 +228,18 @@ object JsonWriterRT: case t: TryRType[?] => if isMapKey then throw new JsonError("Try values (Succeed/Fail) cannot be map keys") - a match - case Success(v) => - refWriteRT[t.tryType.T](cfg, t.tryType.asInstanceOf[RType[t.tryType.T]], v.asInstanceOf[t.tryType.T], sb) - case Failure(_) if cfg.tryFailureHandling == TryOption.AS_NULL => sb.append("null") - case Failure(f) if cfg.tryFailureHandling == TryOption.THROW_EXCEPTION => - throw new JsonError("A try value was Failure with message: " + f.getMessage()) - case Failure(v) => - sb.append('"') - sb.append(v.getMessage) - sb.append('"') + if a == null then sb.append("null") + else + a match + case Success(v) => + refWriteRT[t.tryType.T](cfg, t.tryType.asInstanceOf[RType[t.tryType.T]], v.asInstanceOf[t.tryType.T], sb) + case Failure(_) if cfg.tryFailureHandling == TryOption.AS_NULL => sb.append("null") + case Failure(f) if cfg.tryFailureHandling == TryOption.THROW_EXCEPTION => + throw new JsonError("A try value was Failure with message: " + f.getMessage()) + case Failure(v) => + sb.append('"') + sb.append(v.getMessage) + sb.append('"') case t: TupleRType[?] => if isMapKey then throw new JsonError("Tuples cannot be map keys") @@ -195,7 +261,7 @@ object JsonWriterRT: sb.append('[') val sbLen = sb.length a.asInstanceOf[java.util.Collection[?]].toArray.foreach { elem => - if isOkToWrite(elem, cfg) then refWriteRT[t.elementType.T](cfg, t.elementType.asInstanceOf[RType[t.elementType.T]], elem.asInstanceOf[t.elementType.T], sb) + if JsonWriter.isOkToWrite(elem, cfg) then refWriteRT[t.elementType.T](cfg, t.elementType.asInstanceOf[RType[t.elementType.T]], elem.asInstanceOf[t.elementType.T], sb) } sb.append(',') if sbLen == sb.length then sb.append(']') @@ -207,7 +273,7 @@ object JsonWriterRT: sb.append('{') val sbLen = sb.length a.asInstanceOf[java.util.Map[?, ?]].asScala.foreach { case (key, value) => - if isOkToWrite(value, cfg) then + if JsonWriter.isOkToWrite(value, cfg) then refWriteRT[t.elementType.T](cfg, t.elementType.asInstanceOf[RType[t.elementType.T]], key.asInstanceOf[t.elementType.T], sb, true) sb.append(':') refWriteRT[t.elementType2.T](cfg, t.elementType2.asInstanceOf[RType[t.elementType2.T]], value.asInstanceOf[t.elementType2.T], sb) @@ -232,21 +298,46 @@ object JsonWriterRT: JsonWriterRT.refWriteRT[again.T](cfg, again, a.asInstanceOf[again.T], sb) case t: EnumRType[?] => - val enumAsId = cfg.enumsAsIds match - case '*' => true - case aList: List[String] if aList.contains(t.name) => true - case _ => false - if enumAsId then - val enumVal = t.ordinal(a.toString).getOrElse(throw new JsonError(s"Value $a is not a valid enum value for ${t.name}")) - if isMapKey then + if a == null then sb.append("null") + else + val enumAsId = cfg.enumsAsIds match + case '*' => true + case aList: List[String] if aList.contains(t.name) => true + case _ => false + if enumAsId then + val enumVal = t.ordinal(a.toString).getOrElse(throw new JsonError(s"Value $a is not a valid enum value for ${t.name}")) + if isMapKey then + sb.append('"') + sb.append(enumVal.toString) + sb.append('"') + else sb.append(enumVal.toString) + else sb.append('"') - sb.append(enumVal.toString) + sb.append(a.toString) sb.append('"') - else sb.append(enumVal.toString) + + case t: JavaClassRType[?] => + classesSeen.put(t.typedName, t) + if a == null then sb.append("null") else - sb.append('"') - sb.append(a.toString) - sb.append('"') + sb.append('{') + val sbLen = sb.length + t.fields.foldLeft(sb) { (acc, f) => + val field = f.asInstanceOf[NonConstructorFieldInfo] + val m = a.getClass.getMethod(field.getterLabel) + m.setAccessible(true) + val fieldValue = m.invoke(a).asInstanceOf[field.fieldType.T] + if JsonWriter.isOkToWrite(fieldValue, cfg) then + acc.append('"') + acc.append(field.name) + acc.append('"') + acc.append(':') + JsonWriterRT.refWriteRT[field.fieldType.T](cfg, field.fieldType, a.asInstanceOf[field.fieldType.T], sb)(using scala.collection.mutable.Map.empty[TypedName, RType[?]]) + acc.append(',') + else acc + } + if sbLen == sb.length then sb.append('}') + else sb.setCharAt(sb.length() - 1, '}') case t: ObjectRef => sb.append("\"" + t.name + "\"") @@ -255,16 +346,16 @@ object JsonWriterRT: cfg.undefinedFieldHandling match case UndefinedValueOption.AS_NULL => sb.append("null") case UndefinedValueOption.AS_SYMBOL => sb.append("\"" + t.name + "\"") - case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Value " + a.toString + " is of some unknown/unsupported Scala 2 type " + t.name) + case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Unknown/unsupported Scala 2 type " + t.name) case t: UnknownRType[?] => cfg.undefinedFieldHandling match case UndefinedValueOption.AS_NULL => sb.append("null") case UndefinedValueOption.AS_SYMBOL => sb.append("\"" + t.name + "\"") - case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Value " + a.toString + " is of some unknown/unsupported type " + t.name) + case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Unknown/unsupported type " + t.name) case t: TypeSymbolRType => cfg.undefinedFieldHandling match case UndefinedValueOption.AS_NULL => sb.append("null") case UndefinedValueOption.AS_SYMBOL => sb.append("\"" + t.name + "\"") - case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Value " + a.toString + " is of some unknown/unsupported type " + t.name + ". (Class didn't fully define all its type parameters.)") + case UndefinedValueOption.THROW_EXCEPTION => throw new JsonError("Unknown/unsupported type " + t.name + ". (Class didn't fully define all its type parameters.)") diff --git a/src/main/scala/co.blocke.scalajack/run/Play.scala b/src/main/scala/co.blocke.scalajack/run/Play.scala index ba3f4315..c5ef4a4a 100644 --- a/src/main/scala/co.blocke.scalajack/run/Play.scala +++ b/src/main/scala/co.blocke.scalajack/run/Play.scala @@ -17,12 +17,14 @@ object RunMe extends App: given json.JsonConfig = json .JsonConfig() .copy(noneAsNull = true) + .copy(writeNonConstructorFields = true) // .copy(enumsAsIds = '*') try - val v = Person("Greg", DIAMOND, Command(15), Foom(3)) + val v = Person("Greg", DIAMOND, Command(15), new Wrapper(-10)) println("HERE: " + ScalaJack.write(v)) + println(RType.of[Wrapper].pretty) // println(RType.of[Person[Int]]) diff --git a/src/main/scala/co.blocke.scalajack/run/Sample.scala b/src/main/scala/co.blocke.scalajack/run/Sample.scala index 8739f15b..dd7a650f 100644 --- a/src/main/scala/co.blocke.scalajack/run/Sample.scala +++ b/src/main/scala/co.blocke.scalajack/run/Sample.scala @@ -1,4 +1,5 @@ -package co.blocke.scalajack.run +package co.blocke.scalajack +package run import neotype.* @@ -9,10 +10,13 @@ case object HEART extends card case object DIAMOND extends card case object SPADE extends card -sealed trait msg[X] +sealed abstract class msg[X] +@TypeHint case class Command[T](item: T) extends msg[T] case class Query[T](item: T) extends msg[T] +class Wrapper(val underlying: Int) extends AnyVal + enum Colors: case Red, Blue, Green @@ -21,7 +25,12 @@ case class Foom[X](x: X) extends Miss[X] // case class Person[Y](name: String, age: Miss[Y], again: Option[Person[Y]]) -case class Person[T](name: String, card: card, msg: msg[T], miss: Miss[T]) +case class Person[T](val name: String, val card: card, val msg: msg[T], meh: Wrapper): + var thingy: String = "wow" + + private var c: Int = 5 + def count: Int = c + def count_=(x: Int) = c = x // type NonEmptyString = NonEmptyString.Type // given NonEmptyString: Newtype[String] with