-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Greg Zoller
committed
Nov 28, 2023
1 parent
b13979c
commit bb41444
Showing
22 changed files
with
1,386 additions
and
505 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
.DS_Store | ||
out/ | ||
.vscode/ | ||
.metals/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,66 +1,8 @@ | ||
Path to Performance | ||
------------------- | ||
|
||
[ ] - Fix string value in JsonOutput to handle escaping " and anything else--maybe use apache library | ||
|
||
|
||
[ ] - Create def() for each class (and maybe each collection?) found, to output json as seamlessly as possible | ||
|
||
-- Possibly a multi-pass generation? Once to identify all the classes and generate def fn names, and another | ||
to actually generate the code in dependency order | ||
|
||
[*] - Create JsonOutput (like Jsoniter's JsonWriter, but the name conflicts w/mine). Initially continue to use | ||
StringBuilder, but use some of Jsoniter's clever ',' handling | ||
|
||
[ ] - Explore dark magic Jsoniter mechanisms for all that bitwise/byte stuff to drive extreme efficiency | ||
|
||
[*] - In scala-reflection, explode PrimitiveRTypeRef into separate Refs, like they have for primitive RTypes | ||
|
||
|
||
LEARNINGS: Jsoniter macros to gen an list: | ||
|
||
... else if (tpe <:< TypeRepr.of[List[_]]) withEncoderFor(methodKey, m, out) { (out, x) => | ||
val tpe1 = typeArg1(tpe) | ||
tpe1.asType match | ||
case '[t1] => | ||
val tx = x.asExprOf[List[t1]] | ||
'{ | ||
$out.writeArrayStart() | ||
var l = $tx | ||
while (l ne Nil) { | ||
${genWriteVal('{ l.head }, tpe1 :: types, isStringified, None, out)} | ||
l = l.tail | ||
} | ||
$out.writeArrayEnd() | ||
} | ||
} | ||
|
||
Need to learn what "withEnocderFor()" does--specifically how $out gets passed in. | ||
|
||
|
||
This is the dark magic.... It creates a Symbol and a method (DefDef) for the given function f. | ||
Not sure how to connect/call these together yet, but in isolation, this should allow us to | ||
generate discrete functions that output a "thing". | ||
|
||
def withEncoderFor[T: Type](methodKey: EncoderMethodKey, arg: Expr[T], out: Expr[JsonWriter]) | ||
(f: (Expr[JsonWriter], Expr[T])=> Expr[Unit]): Expr[Unit] = | ||
Apply(Ref(encodeMethodSyms.getOrElse(methodKey, { | ||
val sym = Symbol.newMethod(Symbol.spliceOwner, "e" + encodeMethodSyms.size, | ||
MethodType(List("x", "out"))(_ => List(TypeRepr.of[T], TypeRepr.of[JsonWriter]), _ => TypeRepr.of[Unit])) | ||
encodeMethodSyms.update(methodKey, sym) | ||
encodeMethodDefs += DefDef(sym, params => { | ||
val List(List(x, out)) = params | ||
Some(f(out.asExprOf[JsonWriter], x.asExprOf[T]).asTerm.changeOwner(sym)) | ||
}) | ||
sym | ||
})), List(arg.asTerm, out.asTerm)).asExprOf[Unit] | ||
|
||
|
||
Here's the EncoderMethodKey thingy: | ||
|
||
case class EncoderMethodKey(tpe: TypeRepr, isStringified: Boolean, discriminatorKeyValue: Option[(String, String)]) | ||
|
||
val encodeMethodSyms = new mutable.HashMap[EncoderMethodKey, Symbol] | ||
val encodeMethodDefs = new mutable.ArrayBuffer[DefDef] | ||
|
||
Not sure if we need all this drama yet or not.... Perhaps we can use this as a simple wrapper around the TypedName. Then if | ||
it needs to store more, the shell is already wired in. | ||
[ ] - Study Jsoniter and how it propagates its write config between compile-time and run-time (usage too) | ||
Ideally we'd love to be able to see cfg at compile-time and geneate accordingly.... Right now, | ||
ScalaJack's config is purely runtime, which is less convenient for us... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 0 additions & 16 deletions
16
src/main/scala/co.blocke.scalajack/internal/TreeNode.scala
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
160 changes: 147 additions & 13 deletions
160
src/main/scala/co.blocke.scalajack/json/JsonConfig.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,158 @@ | ||
package co.blocke.scalajack | ||
package json | ||
|
||
case class JsonConfig( | ||
noneAsNull: Boolean = false, | ||
forbidNullsInInput: Boolean = false, | ||
tryFailureHandling: TryOption = TryOption.NO_WRITE, | ||
undefinedFieldHandling: UndefinedValueOption = UndefinedValueOption.THROW_EXCEPTION, | ||
permissivePrimitives: Boolean = false, | ||
writeNonConstructorFields: Boolean = false, | ||
import co.blocke.scala_reflection.TypedName | ||
import co.blocke.scala_reflection.reflect.* | ||
import co.blocke.scala_reflection.reflect.rtypeRefs.* | ||
import scala.quoted.* | ||
|
||
class JsonConfig private[scalajack] ( | ||
val noneAsNull: Boolean, | ||
// forbidNullsInInput: Boolean = false, | ||
// tryFailureHandling: TryOption = TryOption.NO_WRITE, | ||
// undefinedFieldHandling: UndefinedValueOption = UndefinedValueOption.THROW_EXCEPTION, | ||
// permissivePrimitives: Boolean = false, | ||
// writeNonConstructorFields: Boolean = false, | ||
// -------------------------- | ||
typeHintLabel: String = "_hint", | ||
typeHintLabelByTrait: Map[String, String] = Map.empty[String, String], // Trait name -> type hint label | ||
typeHintDefaultTransformer: String => String = (v: String) => v, // in case you want something different than class name (simple name re-mapping) | ||
typeHintTransformer: Map[String, Any => String] = Map.empty[String, Any => String], // if you want class-specific control (instance value => String) | ||
val typeHintLabel: String, | ||
val typeHintPolicy: TypeHintPolicy = TypeHintPolicy.SIMPLE_CLASSNAME | ||
// -------------------------- | ||
enumsAsIds: Char | List[String] = Nil // Char is '*' for all enums as ids, or a list of fully-qualified class names | ||
) | ||
// enumsAsIds: Option[List[String]] = None // None=no enums as ids, Some(Nil)=all enums as ids, Some(List(...))=specified classes enums as ids | ||
): | ||
def withNoneAsNull(nan: Boolean): JsonConfig = copy(noneAsNull = nan) | ||
def withTypeHintLabel(label: String): JsonConfig = copy(typeHintLabel = label) | ||
def withTypeHintPolicy(hintPolicy: TypeHintPolicy): JsonConfig = copy(typeHintPolicy = hintPolicy) | ||
|
||
private[this] def copy( | ||
noneAsNull: Boolean = noneAsNull, | ||
typeHintLabel: String = typeHintLabel, | ||
typeHintPolicy: TypeHintPolicy = typeHintPolicy | ||
): JsonConfig = new JsonConfig( | ||
noneAsNull, | ||
typeHintLabel, | ||
typeHintPolicy | ||
) | ||
|
||
enum TryOption: | ||
case AS_NULL, NO_WRITE, ERR_MSG_STRING, THROW_EXCEPTION | ||
|
||
enum UndefinedValueOption: | ||
case AS_NULL, AS_SYMBOL, THROW_EXCEPTION | ||
|
||
enum TypeHintPolicy: | ||
case SIMPLE_CLASSNAME, SCRAMBLE_CLASSNAME, USE_ANNOTATION | ||
|
||
object JsonConfig | ||
extends JsonConfig( | ||
noneAsNull = false, | ||
typeHintLabel = "_hint", | ||
typeHintPolicy = TypeHintPolicy.SIMPLE_CLASSNAME | ||
): | ||
import scala.quoted.FromExpr.* | ||
|
||
private[scalajack] given FromExpr[JsonConfig] with { | ||
|
||
def extract[X: FromExpr](name: String, x: Expr[X])(using Quotes): X = | ||
import quotes.reflect.* | ||
summon[FromExpr[X]].unapply(x).getOrElse(throw JsonConfigError(s"Can't parse $name: ${x.show}, tree: ${x.asTerm}")) | ||
|
||
def unapply(x: Expr[JsonConfig])(using Quotes): Option[JsonConfig] = | ||
import quotes.reflect.* | ||
|
||
x match | ||
case '{ | ||
JsonConfig( | ||
$noneAsNullE, | ||
// $forbitNullsInInputE, | ||
// $tryFailureHandlerE, | ||
// $undefinedFieldHandlingE, | ||
// $permissivePrimitivesE, | ||
// $writeNonConstructorFieldsE, | ||
$typeHintLabelE, | ||
$typeHintPolicyE | ||
// $enumsAsIdsE | ||
) | ||
} => | ||
try | ||
Some( | ||
JsonConfig( | ||
extract("noneAsNull", noneAsNullE), | ||
// extract("forbitNullsInInput", forbitNullsInInputE), | ||
// extract("tryFailureHandler", tryFailureHandlerE), | ||
// extract("undefinedFieldHandling", undefinedFieldHandlingE), | ||
// extract("permissivePrimitives", permissivePrimitivesE), | ||
// extract("writeNonConstructorFields", writeNonConstructorFieldsE), | ||
// extract2[String]("typeHintLabel", x) | ||
extract("typeHintLabel", typeHintLabelE), | ||
extract("typeHintPolicy", typeHintPolicyE) | ||
// extract("enumsAsIds", enumsAsIdsE) | ||
) | ||
) | ||
catch { | ||
case x => | ||
println("ERROR: " + x.getMessage) | ||
None | ||
} | ||
case '{ JsonConfig } => Some(JsonConfig) | ||
case '{ ($x: JsonConfig).withNoneAsNull($v) } => Some(x.valueOrAbort.withNoneAsNull(v.valueOrAbort)) | ||
case '{ ($x: JsonConfig).withTypeHintLabel($v) } => Some(x.valueOrAbort.withTypeHintLabel(v.valueOrAbort)) | ||
case '{ ($x: JsonConfig).withTypeHintPolicy($v) } => Some(x.valueOrAbort.withTypeHintPolicy(v.valueOrAbort)) | ||
case z => | ||
println("Z: " + z.show) | ||
None | ||
} | ||
|
||
private[scalajack] given FromExpr[TryOption] with { | ||
def unapply(x: Expr[TryOption])(using Quotes): Option[TryOption] = | ||
import quotes.reflect.* | ||
x match | ||
case '{ TryOption.AS_NULL } => Some(TryOption.AS_NULL) | ||
case '{ TryOption.NO_WRITE } => Some(TryOption.NO_WRITE) | ||
case '{ TryOption.ERR_MSG_STRING } => Some(TryOption.ERR_MSG_STRING) | ||
case '{ TryOption.THROW_EXCEPTION } => Some(TryOption.THROW_EXCEPTION) | ||
} | ||
|
||
private[scalajack] given FromExpr[UndefinedValueOption] with { | ||
def unapply(x: Expr[UndefinedValueOption])(using Quotes): Option[UndefinedValueOption] = | ||
import quotes.reflect.* | ||
x match | ||
case '{ UndefinedValueOption.AS_NULL } => Some(UndefinedValueOption.AS_NULL) | ||
case '{ UndefinedValueOption.AS_SYMBOL } => Some(UndefinedValueOption.AS_SYMBOL) | ||
case '{ UndefinedValueOption.THROW_EXCEPTION } => Some(UndefinedValueOption.THROW_EXCEPTION) | ||
} | ||
|
||
private[scalajack] given FromExpr[TypeHintPolicy] with { | ||
def unapply(x: Expr[TypeHintPolicy])(using Quotes): Option[TypeHintPolicy] = | ||
import quotes.reflect.* | ||
x match | ||
case '{ TypeHintPolicy.SIMPLE_CLASSNAME } => Some(TypeHintPolicy.SIMPLE_CLASSNAME) | ||
case '{ TypeHintPolicy.SCRAMBLE_CLASSNAME } => Some(TypeHintPolicy.SCRAMBLE_CLASSNAME) | ||
case '{ TypeHintPolicy.USE_ANNOTATION } => Some(TypeHintPolicy.USE_ANNOTATION) | ||
} | ||
|
||
/* | ||
Here's how we use Quotes to get default values from a class...def | ||
// Constructor argument list, preloaded with optional 'None' values and any default values specified | ||
val preloaded = Expr | ||
.ofList(r.fields.map { f => | ||
val scalaF = f.asInstanceOf[ScalaFieldInfoRef] | ||
if scalaF.defaultValueAccessorName.isDefined then | ||
r.refType match | ||
case '[t] => | ||
val tpe = TypeRepr.of[t].widen | ||
val sym = tpe.typeSymbol | ||
val companionBody = sym.companionClass.tree.asInstanceOf[ClassDef].body | ||
val companion = Ref(sym.companionModule) | ||
companionBody | ||
.collect { | ||
case defaultMethod @ DefDef(name, _, _, _) if name.startsWith("$lessinit$greater$default$" + (f.index + 1)) => | ||
companion.select(defaultMethod.symbol).appliedToTypes(tpe.typeArgs).asExpr | ||
} | ||
.headOption | ||
.getOrElse(Expr(null.asInstanceOf[Boolean])) | ||
else if scalaF.fieldRef.isInstanceOf[OptionRef[_]] then Expr(None) | ||
else Expr(null.asInstanceOf[Int]) | ||
}) | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.