From f30d31b1f87372bc0687125c2718c550c907fff9 Mon Sep 17 00:00:00 2001 From: niegrzybkowski Date: Tue, 16 Dec 2025 10:49:12 +0100 Subject: [PATCH 1/3] Add support for custom Vals with materialization logic --- sjsonnet/src/sjsonnet/Materializer.scala | 3 +- sjsonnet/src/sjsonnet/Val.scala | 8 +++ .../test/src/sjsonnet/CustomValTests.scala | 69 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 sjsonnet/test/src/sjsonnet/CustomValTests.scala diff --git a/sjsonnet/src/sjsonnet/Materializer.scala b/sjsonnet/src/sjsonnet/Materializer.scala index cfffc6ff..bd497ac9 100644 --- a/sjsonnet/src/sjsonnet/Materializer.scala +++ b/sjsonnet/src/sjsonnet/Materializer.scala @@ -62,7 +62,8 @@ abstract class Materializer { "Couldn't manifest function with params [" + s.params.names.mkString(",") + "]", v.pos ) - case vv: Val => + case custom: Val.Custom => storePos(custom.pos); custom.materialize(visitor) + case vv: Val => Error.fail("Unknown value type " + vv.prettyName, vv.pos) case null => Error.fail("Unknown value type " + v) diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index 900cf7b2..39089d6b 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -3,6 +3,7 @@ package sjsonnet import java.util import sjsonnet.Expr.Member.Visibility import sjsonnet.Expr.Params +import upickle.core.Visitor import scala.annotation.tailrec import scala.collection.mutable @@ -93,6 +94,13 @@ object Val { override def asBoolean: Boolean = this.isInstanceOf[True] } + /** + * Base trait for user-defined [[Val]]s with custom materialization logic. + */ + trait Custom extends Literal { + def materialize[T](visitor: Visitor[T, T])(implicit evaluator: EvalScope): T + } + def bool(pos: Position, b: Boolean): Bool = if (b) True(pos) else False(pos) final case class True(pos: Position) extends Bool { diff --git a/sjsonnet/test/src/sjsonnet/CustomValTests.scala b/sjsonnet/test/src/sjsonnet/CustomValTests.scala new file mode 100644 index 00000000..19148d15 --- /dev/null +++ b/sjsonnet/test/src/sjsonnet/CustomValTests.scala @@ -0,0 +1,69 @@ +package sjsonnet + +import upickle.core.Visitor +import utest._ + +object CustomValTests extends TestSuite { + private final case class ImportantString(pos: Position, str: String, importance: Int) + extends Val.Literal + with Val.Custom { + override def prettyName: String = "Important string" + def materialize[T](visitor: Visitor[T, T])(implicit evaluator: EvalScope): T = { + visitor.visitString(str + "!".repeat(importance), -1) + } + } + + private final class IncreaseImportance + extends Val.Builtin1("increaseImportance", "important_string") { + def evalRhs(arg1: Lazy, ev: EvalScope, pos: Position): Val = { + arg1.force match { + case importantString: ImportantString => + importantString.copy(importance = importantString.importance + 1) + case other => other + } + } + } + + private def variableResolve(name: String): Option[Expr] = { + if (name == "message") { + Some(ImportantString(new Position(null, 0), "message", 2)) + } else if (name == "increaseImportance") { + Some(new IncreaseImportance().asFunc) + } else { + None + } + } + + private val interpreter = new Interpreter( + Map(), + Map(), + DummyPath(), + Importer.empty, + parseCache = new DefaultParseCache, + variableResolver = variableResolve + ) + + def check(s: String)(f: Function[Any, Boolean]): Unit = { + val result = interpreter.interpret(s, DummyPath("(memory)")) + assertMatch(result) { + case Right(v) if f(v) => () + case Left(e) => throw new Exception(s"check failed: $s, $e") + } + } + + def tests: Tests = Tests { + test("test custom Val materialization") { + check(s"""important""") { + case ujson.Str(v) => v == "message!!" + case _ => false + } + } + + test("test custom Val usage") { + check(s"""increaseImportance(message)""") { + case ujson.Str(v) => v == "message!!!" + case _ => false + } + } + } +} From 1c39ef8c3cc25f6acf82f740c887abcc85572bab Mon Sep 17 00:00:00 2001 From: niegrzybkowski Date: Tue, 16 Dec 2025 12:49:49 +0100 Subject: [PATCH 2/3] Move materialize to Materializable --- sjsonnet/src/sjsonnet/Materializer.scala | 11 +++++++++-- sjsonnet/src/sjsonnet/Val.scala | 8 -------- sjsonnet/test/src/sjsonnet/CustomValTests.scala | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/sjsonnet/src/sjsonnet/Materializer.scala b/sjsonnet/src/sjsonnet/Materializer.scala index bd497ac9..a5e63776 100644 --- a/sjsonnet/src/sjsonnet/Materializer.scala +++ b/sjsonnet/src/sjsonnet/Materializer.scala @@ -62,8 +62,8 @@ abstract class Materializer { "Couldn't manifest function with params [" + s.params.names.mkString(",") + "]", v.pos ) - case custom: Val.Custom => storePos(custom.pos); custom.materialize(visitor) - case vv: Val => + case mat: Materializer.Materializable => storePos(v.pos); mat.materialize(visitor) + case vv: Val => Error.fail("Unknown value type " + vv.prettyName, vv.pos) case null => Error.fail("Unknown value type " + v) @@ -148,4 +148,11 @@ object Materializer extends Materializer { final val emptyStringArray = new Array[String](0) final val emptyLazyArray = new Array[Lazy](0) + + /** + * Trait for providing custom materialization logic to the Materializer. + */ + trait Materializable { + def materialize[T](visitor: Visitor[T, T])(implicit evaluator: EvalScope): T + } } diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index 39089d6b..900cf7b2 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -3,7 +3,6 @@ package sjsonnet import java.util import sjsonnet.Expr.Member.Visibility import sjsonnet.Expr.Params -import upickle.core.Visitor import scala.annotation.tailrec import scala.collection.mutable @@ -94,13 +93,6 @@ object Val { override def asBoolean: Boolean = this.isInstanceOf[True] } - /** - * Base trait for user-defined [[Val]]s with custom materialization logic. - */ - trait Custom extends Literal { - def materialize[T](visitor: Visitor[T, T])(implicit evaluator: EvalScope): T - } - def bool(pos: Position, b: Boolean): Bool = if (b) True(pos) else False(pos) final case class True(pos: Position) extends Bool { diff --git a/sjsonnet/test/src/sjsonnet/CustomValTests.scala b/sjsonnet/test/src/sjsonnet/CustomValTests.scala index 19148d15..cf89b5c6 100644 --- a/sjsonnet/test/src/sjsonnet/CustomValTests.scala +++ b/sjsonnet/test/src/sjsonnet/CustomValTests.scala @@ -6,7 +6,7 @@ import utest._ object CustomValTests extends TestSuite { private final case class ImportantString(pos: Position, str: String, importance: Int) extends Val.Literal - with Val.Custom { + with Materializer.Materializable { override def prettyName: String = "Important string" def materialize[T](visitor: Visitor[T, T])(implicit evaluator: EvalScope): T = { visitor.visitString(str + "!".repeat(importance), -1) From d0c852260f502b8e1e575b3cfba9fdf63ab2e359 Mon Sep 17 00:00:00 2001 From: Stephen Amar Date: Wed, 17 Dec 2025 09:54:26 -0800 Subject: [PATCH 3/3] Update sjsonnet/src/sjsonnet/Materializer.scala Co-authored-by: He-Pin(kerr) --- sjsonnet/src/sjsonnet/Materializer.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/sjsonnet/src/sjsonnet/Materializer.scala b/sjsonnet/src/sjsonnet/Materializer.scala index a5e63776..5e4b2633 100644 --- a/sjsonnet/src/sjsonnet/Materializer.scala +++ b/sjsonnet/src/sjsonnet/Materializer.scala @@ -151,6 +151,7 @@ object Materializer extends Materializer { /** * Trait for providing custom materialization logic to the Materializer. + * @since 1.0.0 */ trait Materializable { def materialize[T](visitor: Visitor[T, T])(implicit evaluator: EvalScope): T