From e74cf3b1292b009587c4a2862025db61feedd6a5 Mon Sep 17 00:00:00 2001 From: HollandDM Date: Sat, 27 Jan 2024 12:09:42 +0700 Subject: [PATCH 01/13] add macro & test --- .../split/SplittableTypeMacros.scala | 222 ++++++++ .../airstream/split/SplittableTypeSpec.scala | 473 ++++++++++++++++++ 2 files changed, 695 insertions(+) create mode 100644 src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala create mode 100644 src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala new file mode 100644 index 00000000..69b9d48d --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -0,0 +1,222 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.core.{EventStream, Signal, Observable, BaseObservable} +import scala.quoted.{Expr, Quotes, Type} + +/** + * `SplittableTypeMacros` turns this code + * + * ```scala + * sealed trait Foo + * final case class Bar(strOpt: Option[String]) extends Foo + * enum Baz extends Foo { + * case Baz1, Baz2 + * } + * case object Tar extends Foo + * val splitter = fooSignal.splitMatch + * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => renderStrNode(str, strSignal) } + * .handleCase { case baz: Baz => baz } { (baz, bazSignal) => renderBazNode(baz, bazSignal) } + * .handleCase { + * case Tar => () + * case _: Int => () + * } { (_, _) => div("Taz") } + * .toSignal + * ``` + * + * into this code: + * + * ```scala + * val splitter = fooSignal. + * .map { i => + * i match { + * case Bar(Some(str)) => (0, str) + * case baz: Baz => (1, baz) + * case Tar => (2, ()) + * case _: Int => (2, ()) + * } + * } + * .splitOne(_._1) { ??? } + * ``` + * + * After macros expansion, compiler will warns above code "match may not be exhaustive" and "unreachable case" as expected. + */ +object SplittableTypeMacros { + + /** + * `MatchSplitObservable` served as macro's data holder for macro expansion. + * + * Like any class runtime builder, which uses methods to add various data and a dedicated method to build an instance of the class, `MatchSplitObservable` uses `handleCase`s to keep all code data and `toSignal`/`toStream` to expands macro into to match case definition. + * + * For example: + * + * ```scala + * fooSignal.splitMatch + * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => renderStrNode(str, strSignal) } + * .handleCase { case baz: Baz => baz } { (baz, bazSignal) => renderBazNode(baz, bazSignal) } + * ``` + * + * will be expanded sematically into: + * + * ```scala + * MatchSplitObservable.build(fooSignal, ({ case baz: Baz => baz }) :: ({ case Bar(Some(str)) => str }) :: Nil, handlerMap) + * ``` + * + * This is important, because `({ case baz: Baz => baz }) :: ({ case Bar(Some(str)) => str }) :: Nil` saves all the infomation needed for the macro to expands into match case definition + */ + final class MatchSplitObservable[Self[+_] <: Observable[_] , I, O] private[SplittableTypeMacros] ( + private[SplittableTypeMacros] val observable: BaseObservable[Self, I], + private[SplittableTypeMacros] val caseList: List[PartialFunction[Any, Any]], + private[SplittableTypeMacros] val handlerMap: Map[Int, Function[Any, O]] + ) + + extension [Self[+_] <: Observable[_], I](inline observable: BaseObservable[Self, I]) { + inline def splitMatch: MatchSplitObservable[Self, I, Nothing] = MatchSplitObservable.build(observable, Nil, Map.empty[Int, Function[Any, Nothing]]) + } + + extension [Self[+_] <: Observable[_], I, O](inline matchSplitObservable: MatchSplitObservable[Self, I, O]) { + inline def handleCase[A, B, O1 >: O](inline casePf: PartialFunction[A, B])(inline handleFn: ((B, Signal[B])) => O1) = ${ handleCaseImpl('{ matchSplitObservable }, '{ casePf }, '{ handleFn })} + } + + extension [I, O](inline matchSplitObservable: MatchSplitObservable[Signal, I, O]) { + inline def toSignal: Signal[O] = ${ observableImpl('{ matchSplitObservable })} + } + + extension [I, O](inline matchSplitObservable: MatchSplitObservable[EventStream, I, O]) { + inline def toStream: EventStream[O] = ${ observableImpl('{ matchSplitObservable })} + } + + object MatchSplitObservable { + def build[Self[+_] <: Observable[_] , I, O]( + observable: BaseObservable[Self, I], + caseList: List[PartialFunction[Any, Any]], + handlerMap: Map[Int, Function[Any, O]] + ): MatchSplitObservable[Self, I, O] = { + MatchSplitObservable(observable, caseList, handlerMap) + } + } + + private def handleCaseImpl[Self[+_] <: Observable[_] : Type, I: Type, O: Type, O1 >: O : Type, A: Type, B: Type]( + matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], + casePfExpr: Expr[PartialFunction[A, B]], + handleFnExpr: Expr[Function[(B, Signal[B]), O1]], + )( + using quotes: Quotes + ): Expr[MatchSplitObservable[Self, I, O1]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ MatchSplitObservable.build[Self, I, O]($observableExpr, $caseListExpr, $handlerMapExpr)} => innerHandleCaseImpl(observableExpr, caseListExpr, handlerMapExpr, casePfExpr, handleFnExpr) + case other => report.errorAndAbort("Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly") + } + } + + private def innerHandleCaseImpl[Self[+_] <: Observable[_] : Type, I: Type, O: Type, O1 >: O : Type, A: Type, B: Type]( + observableExpr: Expr[BaseObservable[Self, I]], + caseListExpr: Expr[List[PartialFunction[Any, Any]]], + handlerMapExpr: Expr[Map[Int, Function[Any, O]]], + casePfExpr: Expr[PartialFunction[A, B]], + handleFnExpr: Expr[Function[(B, Signal[B]), O1]] + )( + using quotes: Quotes + ): Expr[MatchSplitObservable[Self, I, O1]] = { + val caseExprList = exprOfListToListOfExpr(caseListExpr) + + val nextCaseExprList = casePfExpr.asExprOf[PartialFunction[Any, Any]] :: caseExprList + + val nextCaseListExpr = listOfExprToExprOfList(nextCaseExprList) + + '{ MatchSplitObservable.build[Self, I, O1]($observableExpr, $nextCaseListExpr, ($handlerMapExpr + ($handlerMapExpr.size -> $handleFnExpr.asInstanceOf[Function[Any, O1]]))) } + } + + private def exprOfListToListOfExpr( + pfListExpr: Expr[List[PartialFunction[Any, Any]]] + )( + using quotes: Quotes + ): List[Expr[PartialFunction[Any, Any]]] = { + import quotes.reflect.* + + pfListExpr match { + case '{ $headExpr :: (${tailExpr}: List[PartialFunction[Any, Any]]) } => + headExpr :: exprOfListToListOfExpr(tailExpr) + case '{ Nil } => Nil + case _ => report.errorAndAbort("Macro expansion failed, please use `handleCase` instead of modify MatchSplitObservable explicitly") + } + + } + + private def listOfExprToExprOfList( + pfExprList: List[Expr[PartialFunction[Any, Any]]] + )( + using quotes: Quotes + ): Expr[List[PartialFunction[Any, Any]]] = { + import quotes.reflect.* + + pfExprList match + case head :: tail => '{ $head :: ${listOfExprToExprOfList(tail)} } + case Nil => '{ Nil } + } + + private inline def toSplittableOneObservable[Self[+_] <: Observable[_], O]( + parentObservable: BaseObservable[Self, (Int, Any)], + handlerMap: Map[Int, Function[Any, O]] + ): Self[O] = { + parentObservable.matchStreamOrSignal( + ifStream = _.splitOne(_._1) { case (idx, (_, b), dataSignal) => + val bSignal = dataSignal.map(_._2) + handlerMap.apply(idx).apply(b -> bSignal) + }, + ifSignal = _.splitOne(_._1) { case (idx, (_, b), dataSignal) => + val bSignal = dataSignal.map(_._2) + handlerMap.apply(idx).apply(b -> bSignal) + } + ).asInstanceOf[Self[O]] // #TODO[Integrity] Same as FlatMap/AsyncStatusObservable, how to type this properly? + } + + private def observableImpl[Self[+_] <: Observable[_] : Type, I: Type, O: Type]( + matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]] + )( + using quotes: Quotes + ): Expr[Self[O]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ MatchSplitObservable.build[Self, I, O]($_, Nil, $_)} => + report.errorAndAbort("Macro expansion failed, need at least one handleCase") + case '{ MatchSplitObservable.build[Self, I, O]($observableExpr, $caseListExpr, $handlerMapExpr)} => + '{toSplittableOneObservable( + $observableExpr + .map(i => ${ innerObservableImpl('i, caseListExpr) }) + .asInstanceOf[BaseObservable[Self, (Int, Any)]], + $handlerMapExpr + )} + case _ => report.errorAndAbort("Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly") + } + } + + private def innerObservableImpl[I: Type]( + iExpr: Expr[I], + caseListExpr: Expr[List[PartialFunction[Any, Any]]] + )( + using quotes: Quotes + ): Expr[(Int, Any)] = { + import quotes.reflect.* + + val caseExprList = exprOfListToListOfExpr(caseListExpr) + + val allCaseDefLists = caseExprList.reverse.zipWithIndex.flatMap { case (caseExpr, idx) => + caseExpr.asTerm match { + case Block(List(DefDef(_, _, _, Some(Match(_, caseDefList)))), _) => { + caseDefList.map { caseDef => + val idxExpr = Expr.apply(idx) + val newRhsExpr = '{ val res = ${caseDef.rhs.asExprOf[Any]}; ($idxExpr, res)} + CaseDef.copy(caseDef)(caseDef.pattern, caseDef.guard, newRhsExpr.asTerm) + } + } + case _ => report.errorAndAbort("Macro expansion failed, please use `handleCase` with annonymous partial function") + } + }.map(_.changeOwner(Symbol.spliceOwner)) + + Match(iExpr.asTerm, allCaseDefLists).asExprOf[(Int, Any)] + } + +} diff --git a/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala new file mode 100644 index 00000000..97788cb6 --- /dev/null +++ b/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala @@ -0,0 +1,473 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.UnitSpec +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.fixtures.{Effect, TestableOwner} +import com.raquo.airstream.state.Var +import com.raquo.airstream.split.SplittableTypeMacros.{splitMatch, handleCase, toSignal, toStream} + +import scala.collection.{immutable, mutable} +import scala.scalajs.js + +class SplittableTypeSpec extends UnitSpec { + + sealed trait Foo + + final case class Bar(strOpt: Option[String]) extends Foo + enum Baz extends Foo { + case Baz1, Baz2 + } + case object Tar extends Foo + + final case class Res(result: Any) + + it("split match signal") { + val effects = mutable.Buffer[Effect[String]]() + + val myVar = Var[Foo](Bar(Some("initial"))) + + val owner = new TestableOwner + + val signal = myVar.signal + .splitMatch + .handleCase { + case Bar(Some(str)) => str + case Bar(None) => "null" + } { case (str, strSignal) => + effects += Effect("init-child", s"Bar-$str") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + strSignal.foreach { str => + effects += Effect("update-child", s"Bar-$str") + }(owner) + + Res(str) + } + .handleCase { case baz: Baz => baz } { case (baz, bazSignal) => + effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + bazSignal.foreach { baz => + effects += Effect("update-child", s"Baz-${baz.ordinal}-${baz.toString}") + }(owner) + + Res(baz) + } + .handleCase { case Tar => 10 } { case (int, intSignal) => + effects += Effect("init-child", s"Tar-${int}") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + intSignal.foreach { int => + effects += Effect("update-child", s"Tar-${int}") + }(owner) + + Res(int) + } + .toSignal + + signal.foreach { result => + effects += Effect("result", result.toString) + }(owner) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Bar-initial"), + Effect("update-child", "Bar-initial"), + Effect("result", "Res(initial)") + ) + + effects.clear() + + myVar.writer.onNext(Bar(None)) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(initial)"), + Effect("update-child", "Bar-null") + ) + + effects.clear() + + myVar.writer.onNext(Bar(None)) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(initial)"), // sematically, splitMatch/handleCase/toSignal use splitOne underlying, so this is the same as splitOne spec + Effect("update-child", "Bar-null") + ) + + effects.clear() + + myVar.writer.onNext(Bar(Some("other"))) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(initial)"), + Effect("update-child", "Bar-other") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(Baz.Baz1) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Baz-0-Baz1"), + Effect("update-child", "Baz-0-Baz1"), + Effect("result", "Res(Baz1)") + ) + + effects.clear() + + myVar.writer.onNext(Baz.Baz2) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(Baz1)"), + Effect("update-child", "Baz-1-Baz2") + ) + + effects.clear() + + myVar.writer.onNext(Baz.Baz2) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(Baz1)"), + Effect("update-child", "Baz-1-Baz2") + ) + + effects.clear() + + myVar.writer.onNext(Tar) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Tar-10"), + Effect("update-child", "Tar-10"), + Effect("result", "Res(10)") + ) + + effects.clear() + + myVar.writer.onNext(Tar) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(10)"), + Effect("update-child", "Tar-10") + ) + + effects.clear() + + } + + it("split match signal - with warning in compiler") { + val effects = mutable.Buffer[Effect[String]]() + + val myVar = Var[Foo](Bar(Some("initial"))) + + val owner = new TestableOwner + + // This should warn "match may not be exhaustive" with mising cases, and some idea can also flag it + val signal = myVar.signal + .splitMatch + .handleCase { + case Bar(Some(str)) => str + } { case (str, strSignal) => + effects += Effect("init-child", s"Bar-$str") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + strSignal.foreach { str => + effects += Effect("update-child", s"Bar-$str") + }(owner) + + Res(str) + } + .handleCase { case baz: Baz.Baz1.type => baz } { case (baz, bazSignal) => + effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + bazSignal.foreach { baz => + effects += Effect("update-child", s"Baz-${baz.ordinal}-${baz.toString}") + }(owner) + + Res(baz) + } + .handleCase { case Tar => 10 } { case (int, intSignal) => + effects += Effect("init-child", s"Tar-${int}") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + intSignal.foreach { int => + effects += Effect("update-child", s"Tar-${int}") + }(owner) + + Res(int) + } + .toSignal + + signal.foreach { result => + effects += Effect("result", result.toString) + }(owner) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Bar-initial"), + Effect("update-child", "Bar-initial"), + Effect("result", "Res(initial)") + ) + + effects.clear() + + myVar.writer.onNext(Bar(Some("other"))) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(initial)"), + Effect("update-child", "Bar-other") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(Baz.Baz1) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Baz-0-Baz1"), + Effect("update-child", "Baz-0-Baz1"), + Effect("result", "Res(Baz1)") + ) + + effects.clear() + + myVar.writer.onNext(Tar) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Tar-10"), + Effect("update-child", "Tar-10"), + Effect("result", "Res(10)") + ) + + effects.clear() + + myVar.writer.onNext(Tar) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(10)"), + Effect("update-child", "Tar-10") + ) + + effects.clear() + + } + + it("split match stream") { + val effects = mutable.Buffer[Effect[String]]() + + val myEventBus = new EventBus[Foo] + + val owner = new TestableOwner + + val stream = myEventBus.events + .splitMatch + .handleCase { + case Bar(Some(str)) => str + case Bar(None) => "null" + } { case (str, strSignal) => + effects += Effect("init-child", s"Bar-$str") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + strSignal.foreach { str => + effects += Effect("update-child", s"Bar-$str") + }(owner) + + Res(str) + } + .handleCase { case baz: Baz => baz } { case (baz, bazSignal) => + effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + bazSignal.foreach { baz => + effects += Effect("update-child", s"Baz-${baz.ordinal}-${baz.toString}") + }(owner) + + Res(baz) + } + .handleCase { case Tar => 10 } { case (int, intSignal) => + effects += Effect("init-child", s"Tar-${int}") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + intSignal.foreach { int => + effects += Effect("update-child", s"Tar-${int}") + }(owner) + + Res(int) + } + .toStream + + stream.foreach { result => + effects += Effect("result", result.toString) + }(owner) + + myEventBus.writer.onNext(Bar(None)) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Bar-null"), + Effect("update-child", "Bar-null"), + Effect("result", "Res(null)") + ) + + effects.clear() + + myEventBus.writer.onNext(Bar(None)) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(null)"), // sematically, splitMatch/handleCase/toStream use splitOne underlying, so this is the same as splitOne spec + Effect("update-child", "Bar-null") + ) + + effects.clear() + + myEventBus.writer.onNext(Bar(Some("other"))) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(null)"), + Effect("update-child", "Bar-other") + ) + + effects.clear() + + // -- + + myEventBus.writer.onNext(Baz.Baz1) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Baz-0-Baz1"), + Effect("update-child", "Baz-0-Baz1"), + Effect("result", "Res(Baz1)") + ) + + effects.clear() + + myEventBus.writer.onNext(Baz.Baz2) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(Baz1)"), + Effect("update-child", "Baz-1-Baz2") + ) + + effects.clear() + + myEventBus.writer.onNext(Baz.Baz2) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(Baz1)"), + Effect("update-child", "Baz-1-Baz2") + ) + + effects.clear() + + myEventBus.writer.onNext(Tar) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Tar-10"), + Effect("update-child", "Tar-10"), + Effect("result", "Res(10)") + ) + + effects.clear() + + myEventBus.writer.onNext(Tar) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(10)"), + Effect("update-child", "Tar-10") + ) + + effects.clear() + + } + + it("split match stream - with warning in compiler") { + val effects = mutable.Buffer[Effect[String]]() + + val myEventBus = new EventBus[Foo] + + val owner = new TestableOwner + + // This should warn "match may not be exhaustive" with mising cases, and some idea can also flag it + // Compiler only flag the first warning in some case, so it's best to comment out first warning test for this to flag the warning + val stream = myEventBus.events + .splitMatch + .handleCase { + case Bar(None) => "null" + } { case (str, strSignal) => + effects += Effect("init-child", s"Bar-$str") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + strSignal.foreach { str => + effects += Effect("update-child", s"Bar-$str") + }(owner) + + Res(str) + } + .handleCase { case baz: Baz => baz } { case (baz, bazSignal) => + effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + bazSignal.foreach { baz => + effects += Effect("update-child", s"Baz-${baz.ordinal}-${baz.toString}") + }(owner) + + Res(baz) + } + .toStream + + stream.foreach { result => + effects += Effect("result", result.toString) + }(owner) + + myEventBus.writer.onNext(Bar(None)) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Bar-null"), + Effect("update-child", "Bar-null"), + Effect("result", "Res(null)") + ) + + effects.clear() + + myEventBus.writer.onNext(Bar(None)) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(null)"), // sematically, splitMatch/handleCase/toStream use splitOne underlying, so this is the same as splitOne spec + Effect("update-child", "Bar-null") + ) + + effects.clear() + + // -- + + myEventBus.writer.onNext(Baz.Baz1) + + effects shouldBe mutable.Buffer( + Effect("init-child", "Baz-0-Baz1"), + Effect("update-child", "Baz-0-Baz1"), + Effect("result", "Res(Baz1)") + ) + + effects.clear() + + myEventBus.writer.onNext(Baz.Baz2) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(Baz1)"), + Effect("update-child", "Baz-1-Baz2") + ) + + effects.clear() + + myEventBus.writer.onNext(Baz.Baz2) + + effects shouldBe mutable.Buffer( + Effect("result", "Res(Baz1)"), + Effect("update-child", "Baz-1-Baz2") + ) + + effects.clear() + + } + +} From cebb1345885760002db4437982a9f3882d401610 Mon Sep 17 00:00:00 2001 From: HollandDM Date: Sun, 28 Jan 2024 22:47:26 +0700 Subject: [PATCH 02/13] feat: add illegal usage check --- .../split/SplittableTypeMacros.scala | 7 +++++- .../airstream/split/SplittableTypeSpec.scala | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index 69b9d48d..c5c65da3 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -67,7 +67,12 @@ object SplittableTypeMacros { private[SplittableTypeMacros] val observable: BaseObservable[Self, I], private[SplittableTypeMacros] val caseList: List[PartialFunction[Any, Any]], private[SplittableTypeMacros] val handlerMap: Map[Int, Function[Any, O]] - ) + ) { + + // MatchSplitObservable should be erased after macro expansion, any instance in runtime is illegal + throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") + + } extension [Self[+_] <: Observable[_], I](inline observable: BaseObservable[Self, I]) { inline def splitMatch: MatchSplitObservable[Self, I, Nothing] = MatchSplitObservable.build(observable, Nil, Map.empty[Int, Function[Any, Nothing]]) diff --git a/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala index 97788cb6..58748784 100644 --- a/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala +++ b/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala @@ -8,6 +8,7 @@ import com.raquo.airstream.split.SplittableTypeMacros.{splitMatch, handleCase, t import scala.collection.{immutable, mutable} import scala.scalajs.js +import com.raquo.airstream.ShouldSyntax.shouldBeEmpty class SplittableTypeSpec extends UnitSpec { @@ -470,4 +471,27 @@ class SplittableTypeSpec extends UnitSpec { } + it("illegal usage should throw") { + val owner = new TestableOwner + val fooVar = Var[Foo](Tar) + var isThrown = false + + try { + fooVar.signal + .splitMatch + .handleCase { + case Bar(None) => "null" + } { case (str, strSignal) => + () + } + .handleCase { case baz: Baz => baz } { case (baz, bazSignal) => + () + } + } catch { + case _: UnsupportedOperationException => isThrown = true + } finally { + isThrown shouldBe true + } + } + } From 9705542b0a3f2e873387d1950f2b63b9b42a1ad0 Mon Sep 17 00:00:00 2001 From: HollandDM Date: Wed, 28 Feb 2024 21:43:56 +0700 Subject: [PATCH 03/13] fix: use Lamda instead of DefDef --- .../com/raquo/airstream/split/SplittableTypeMacros.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index c5c65da3..b9a7ae9a 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -210,7 +210,7 @@ object SplittableTypeMacros { val allCaseDefLists = caseExprList.reverse.zipWithIndex.flatMap { case (caseExpr, idx) => caseExpr.asTerm match { - case Block(List(DefDef(_, _, _, Some(Match(_, caseDefList)))), _) => { + case Lambda(_, Match(_, caseDefList)) => { caseDefList.map { caseDef => val idxExpr = Expr.apply(idx) val newRhsExpr = '{ val res = ${caseDef.rhs.asExprOf[Any]}; ($idxExpr, res)} From 57b3ea3c3d892ee4240be54d0c2b4d46435bcc4c Mon Sep 17 00:00:00 2001 From: HollandDM Date: Tue, 12 Mar 2024 18:21:37 +0700 Subject: [PATCH 04/13] some small update --- .../split/SplittableTypeMacros.scala | 27 +++++++------------ .../airstream/split/SplittableTypeSpec.scala | 23 ---------------- 2 files changed, 10 insertions(+), 40 deletions(-) diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index b9a7ae9a..3eeb591b 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -2,6 +2,7 @@ package com.raquo.airstream.split import com.raquo.airstream.core.{EventStream, Signal, Observable, BaseObservable} import scala.quoted.{Expr, Quotes, Type} +import scala.annotation.compileTimeOnly /** * `SplittableTypeMacros` turns this code @@ -63,15 +64,17 @@ object SplittableTypeMacros { * * This is important, because `({ case baz: Baz => baz }) :: ({ case Bar(Some(str)) => str }) :: Nil` saves all the infomation needed for the macro to expands into match case definition */ - final class MatchSplitObservable[Self[+_] <: Observable[_] , I, O] private[SplittableTypeMacros] ( - private[SplittableTypeMacros] val observable: BaseObservable[Self, I], - private[SplittableTypeMacros] val caseList: List[PartialFunction[Any, Any]], - private[SplittableTypeMacros] val handlerMap: Map[Int, Function[Any, O]] - ) { - - // MatchSplitObservable should be erased after macro expansion, any instance in runtime is illegal + final class MatchSplitObservable[Self[+_] <: Observable[_] , I, O] { throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") + } + object MatchSplitObservable { + @compileTimeOnly("splitMatch without toSignal/toStream is illegal") + def build[Self[+_] <: Observable[_] , I, O]( + observable: BaseObservable[Self, I], + caseList: List[PartialFunction[Any, Any]], + handlerMap: Map[Int, Function[Any, O]] + ): MatchSplitObservable[Self, I, O] = throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") } extension [Self[+_] <: Observable[_], I](inline observable: BaseObservable[Self, I]) { @@ -90,16 +93,6 @@ object SplittableTypeMacros { inline def toStream: EventStream[O] = ${ observableImpl('{ matchSplitObservable })} } - object MatchSplitObservable { - def build[Self[+_] <: Observable[_] , I, O]( - observable: BaseObservable[Self, I], - caseList: List[PartialFunction[Any, Any]], - handlerMap: Map[Int, Function[Any, O]] - ): MatchSplitObservable[Self, I, O] = { - MatchSplitObservable(observable, caseList, handlerMap) - } - } - private def handleCaseImpl[Self[+_] <: Observable[_] : Type, I: Type, O: Type, O1 >: O : Type, A: Type, B: Type]( matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], casePfExpr: Expr[PartialFunction[A, B]], diff --git a/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala index 58748784..b8922f73 100644 --- a/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala +++ b/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala @@ -471,27 +471,4 @@ class SplittableTypeSpec extends UnitSpec { } - it("illegal usage should throw") { - val owner = new TestableOwner - val fooVar = Var[Foo](Tar) - var isThrown = false - - try { - fooVar.signal - .splitMatch - .handleCase { - case Bar(None) => "null" - } { case (str, strSignal) => - () - } - .handleCase { case baz: Baz => baz } { case (baz, bazSignal) => - () - } - } catch { - case _: UnsupportedOperationException => isThrown = true - } finally { - isThrown shouldBe true - } - } - } From d3680f3e90dbd32dd02c2091661955f457b8a115 Mon Sep 17 00:00:00 2001 From: HollandDM Date: Wed, 13 Mar 2024 11:09:26 +0700 Subject: [PATCH 05/13] fix: use opaque type --- .../split/MatchSplitObservable.scala | 35 +++++++++++++++++++ .../split/SplittableTypeMacros.scala | 35 ------------------- 2 files changed, 35 insertions(+), 35 deletions(-) create mode 100644 src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala new file mode 100644 index 00000000..9f66b3ce --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala @@ -0,0 +1,35 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.core.{Observable, BaseObservable} +import scala.annotation.compileTimeOnly + +/** + * `MatchSplitObservable` served as macro's data holder for macro expansion. + * + * For example: + * + * ```scala + * fooSignal.splitMatch + * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => renderStrNode(str, strSignal) } + * .handleCase { case baz: Baz => baz } { (baz, bazSignal) => renderBazNode(baz, bazSignal) } + * ``` + * + * will be expanded sematically into: + * + * ```scala + * MatchSplitObservable.build(fooSignal, ({ case baz: Baz => baz }) :: ({ case Bar(Some(str)) => str }) :: Nil, handlerMap) + * ``` + */ + +opaque type MatchSplitObservable[Self[+_] <: Observable[_] , I, O] = Unit + +object MatchSplitObservable { + + @compileTimeOnly("splitMatch without toSignal/toStream is illegal") + def build[Self[+_] <: Observable[_] , I, O]( + observable: BaseObservable[Self, I], + caseList: List[PartialFunction[Any, Any]], + handlerMap: Map[Int, Function[Any, O]] + ): MatchSplitObservable[Self, I, O] = throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") + +} diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index 3eeb591b..d38eaf34 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -2,7 +2,6 @@ package com.raquo.airstream.split import com.raquo.airstream.core.{EventStream, Signal, Observable, BaseObservable} import scala.quoted.{Expr, Quotes, Type} -import scala.annotation.compileTimeOnly /** * `SplittableTypeMacros` turns this code @@ -43,40 +42,6 @@ import scala.annotation.compileTimeOnly */ object SplittableTypeMacros { - /** - * `MatchSplitObservable` served as macro's data holder for macro expansion. - * - * Like any class runtime builder, which uses methods to add various data and a dedicated method to build an instance of the class, `MatchSplitObservable` uses `handleCase`s to keep all code data and `toSignal`/`toStream` to expands macro into to match case definition. - * - * For example: - * - * ```scala - * fooSignal.splitMatch - * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => renderStrNode(str, strSignal) } - * .handleCase { case baz: Baz => baz } { (baz, bazSignal) => renderBazNode(baz, bazSignal) } - * ``` - * - * will be expanded sematically into: - * - * ```scala - * MatchSplitObservable.build(fooSignal, ({ case baz: Baz => baz }) :: ({ case Bar(Some(str)) => str }) :: Nil, handlerMap) - * ``` - * - * This is important, because `({ case baz: Baz => baz }) :: ({ case Bar(Some(str)) => str }) :: Nil` saves all the infomation needed for the macro to expands into match case definition - */ - final class MatchSplitObservable[Self[+_] <: Observable[_] , I, O] { - throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") - } - - object MatchSplitObservable { - @compileTimeOnly("splitMatch without toSignal/toStream is illegal") - def build[Self[+_] <: Observable[_] , I, O]( - observable: BaseObservable[Self, I], - caseList: List[PartialFunction[Any, Any]], - handlerMap: Map[Int, Function[Any, O]] - ): MatchSplitObservable[Self, I, O] = throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") - } - extension [Self[+_] <: Observable[_], I](inline observable: BaseObservable[Self, I]) { inline def splitMatch: MatchSplitObservable[Self, I, Nothing] = MatchSplitObservable.build(observable, Nil, Map.empty[Int, Function[Any, Nothing]]) } From ebe571881ad01115ab2298cb78ef546d93f10a94 Mon Sep 17 00:00:00 2001 From: HollandDM Date: Thu, 30 May 2024 23:03:53 +0700 Subject: [PATCH 06/13] ref: test to scala 3 only --- .../com/raquo/airstream/split/SplittableTypeSpec.scala | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/{scala => scala-3}/com/raquo/airstream/split/SplittableTypeSpec.scala (100%) diff --git a/src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala similarity index 100% rename from src/test/scala/com/raquo/airstream/split/SplittableTypeSpec.scala rename to src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala From 9a41579a73cff2a62ebe41b6886c333007e7a77a Mon Sep 17 00:00:00 2001 From: HollandDM Date: Mon, 16 Sep 2024 00:22:56 +0700 Subject: [PATCH 07/13] feat: add handle type --- .../split/MatchSplitObservable.scala | 10 +- .../airstream/split/MatchTypeObservable.scala | 52 +++ .../split/SplittableTypeMacros.scala | 329 +++++++++++++----- .../airstream/split/SplittableTypeSpec.scala | 6 +- 4 files changed, 301 insertions(+), 96 deletions(-) create mode 100644 src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala index 9f66b3ce..0c4b0732 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala @@ -5,15 +5,15 @@ import scala.annotation.compileTimeOnly /** * `MatchSplitObservable` served as macro's data holder for macro expansion. - * + * * For example: * * ```scala - * fooSignal.splitMatch - * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => renderStrNode(str, strSignal) } + * fooSignal.splitMatch + * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => renderStrNode(str, strSignal) } * .handleCase { case baz: Baz => baz } { (baz, bazSignal) => renderBazNode(baz, bazSignal) } * ``` - * + * * will be expanded sematically into: * * ```scala @@ -24,7 +24,7 @@ import scala.annotation.compileTimeOnly opaque type MatchSplitObservable[Self[+_] <: Observable[_] , I, O] = Unit object MatchSplitObservable { - + @compileTimeOnly("splitMatch without toSignal/toStream is illegal") def build[Self[+_] <: Observable[_] , I, O]( observable: BaseObservable[Self, I], diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala new file mode 100644 index 00000000..a32b12a8 --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala @@ -0,0 +1,52 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.core.{Observable, BaseObservable} +import scala.annotation.compileTimeOnly + +/** `MatchTypeObservable` served as macro's data holder for macro expansion. + * + * For example: + * + * ```scala + * fooSignal.splitMatch + * .splitType[Baz] { (baz, bazSignal) => renderBazNode(baz, bazSignal) } + * ``` + * + * will be expanded sematically into: + * + * ```scala + * MatchTypeObservable.build[*, *, *, Baz]( + * fooSignal, + * Nil, + * handlerMap, + * ({ case t: Baz => t }) + * ) + * ``` + * + * and then into: + * + * ```scala + * MatchSplitObservable.build( + * fooSignal, + * ({ case baz: Baz => baz }) :: Nil, + * handlerMap + * ) + * ``` + */ + +opaque type MatchTypeObservable[Self[+_] <: Observable[_], I, O, T] = Unit + +object MatchTypeObservable { + + @compileTimeOnly("splitMatch without toSignal/toStream is illegal") + def build[Self[+_] <: Observable[_], I, O, T]( + observable: BaseObservable[Self, I], + caseList: List[PartialFunction[Any, Any]], + handlerMap: Map[Int, Function[Any, O]], + tCast: PartialFunction[T, T] + ): MatchTypeObservable[Self, I, O, T] = + throw new UnsupportedOperationException( + "splitMatch without toSignal/toStream is illegal" + ) + +} diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index d38eaf34..ef0dca8e 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -1,79 +1,191 @@ package com.raquo.airstream.split -import com.raquo.airstream.core.{EventStream, Signal, Observable, BaseObservable} +import com.raquo.airstream.core.{ + EventStream, + Signal, + Observable, + BaseObservable +} import scala.quoted.{Expr, Quotes, Type} -/** - * `SplittableTypeMacros` turns this code - * - * ```scala - * sealed trait Foo - * final case class Bar(strOpt: Option[String]) extends Foo - * enum Baz extends Foo { - * case Baz1, Baz2 - * } - * case object Tar extends Foo - * val splitter = fooSignal.splitMatch - * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => renderStrNode(str, strSignal) } - * .handleCase { case baz: Baz => baz } { (baz, bazSignal) => renderBazNode(baz, bazSignal) } - * .handleCase { - * case Tar => () - * case _: Int => () - * } { (_, _) => div("Taz") } - * .toSignal - * ``` - * - * into this code: - * - * ```scala - * val splitter = fooSignal. - * .map { i => - * i match { - * case Bar(Some(str)) => (0, str) - * case baz: Baz => (1, baz) - * case Tar => (2, ()) - * case _: Int => (2, ()) - * } - * } - * .splitOne(_._1) { ??? } - * ``` - * - * After macros expansion, compiler will warns above code "match may not be exhaustive" and "unreachable case" as expected. - */ +/** `SplittableTypeMacros` turns this code + * + * ```scala + * sealed trait Foo + * final case class Bar(strOpt: Option[String]) extends Foo + * enum Baz extends Foo { + * case Baz1, Baz2 + * } + * case object Tar extends Foo + * val splitter = fooSignal.splitMatch + * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => + * renderStrNode(str, strSignal) + * } + * .handleCase { case baz: Baz => baz } { (baz, bazSignal) => + * renderBazNode(baz, bazSignal) + * } + * .handleCase { + * case Tar => () + * case _: Int => () + * } { (_, _) => div("Taz") } + * .toSignal + * ``` + * + * into this code: + * + * ```scala + * val splitter = fooSignal. + * .map { i => + * i match { + * case Bar(Some(str)) => (0, str) + * case baz: Baz => (1, baz) + * case Tar => (2, ()) + * case _: Int => (2, ()) + * } + * } + * .splitOne(_._1) { ??? } + * ``` + * + * After macros expansion, compiler will warns above code "match may not be + * exhaustive" and "unreachable case" as expected. + */ object SplittableTypeMacros { extension [Self[+_] <: Observable[_], I](inline observable: BaseObservable[Self, I]) { - inline def splitMatch: MatchSplitObservable[Self, I, Nothing] = MatchSplitObservable.build(observable, Nil, Map.empty[Int, Function[Any, Nothing]]) + inline def splitMatch: MatchSplitObservable[Self, I, Nothing] = + MatchSplitObservable.build( + observable, + Nil, + Map.empty[Int, Function[Any, Nothing]] + ) } - extension [Self[+_] <: Observable[_], I, O](inline matchSplitObservable: MatchSplitObservable[Self, I, O]) { - inline def handleCase[A, B, O1 >: O](inline casePf: PartialFunction[A, B])(inline handleFn: ((B, Signal[B])) => O1) = ${ handleCaseImpl('{ matchSplitObservable }, '{ casePf }, '{ handleFn })} + extension [Self[+_] <: Observable[_], I, O]( + inline matchSplitObservable: MatchSplitObservable[Self, I, O] + ) { + inline def handleCase[A, B, O1 >: O](inline casePf: PartialFunction[A, B])(inline handleFn: ((B, Signal[B])) => O1) = ${ + handleCaseImpl('{ matchSplitObservable }, '{ casePf }, '{ handleFn }) + } + + inline private def handlePfType[T](inline casePf: PartialFunction[Any, T]) = ${ + handleTypeImpl[Self, I, O, T]('{ matchSplitObservable }, '{ casePf }) + } + + inline def handleType[T]: MatchTypeObservable[Self, I, O, T] = handlePfType[T] { case t: T => t } + } + + extension [Self[+_] <: Observable[_], I, O, T](inline matchTypeObserver: MatchTypeObservable[Self, I, O, T]) { + inline def apply[O1 >: O](inline handleFn: ((T, Signal[T])) => O1) = ${ + handleTypeApplyImpl('{ matchTypeObserver }, '{ handleFn }) + } } extension [I, O](inline matchSplitObservable: MatchSplitObservable[Signal, I, O]) { - inline def toSignal: Signal[O] = ${ observableImpl('{ matchSplitObservable })} + inline def toSignal: Signal[O] = ${ + observableImpl('{ matchSplitObservable }) + } } extension [I, O](inline matchSplitObservable: MatchSplitObservable[EventStream, I, O]) { - inline def toStream: EventStream[O] = ${ observableImpl('{ matchSplitObservable })} + inline def toStream: EventStream[O] = ${ + observableImpl('{ matchSplitObservable }) + } } - private def handleCaseImpl[Self[+_] <: Observable[_] : Type, I: Type, O: Type, O1 >: O : Type, A: Type, B: Type]( + private def handleCaseImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, A: Type, B: Type]( matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], casePfExpr: Expr[PartialFunction[A, B]], - handleFnExpr: Expr[Function[(B, Signal[B]), O1]], + handleFnExpr: Expr[Function[(B, Signal[B]), O1]] )( using quotes: Quotes ): Expr[MatchSplitObservable[Self, I, O1]] = { import quotes.reflect.* - + matchSplitObservableExpr match { - case '{ MatchSplitObservable.build[Self, I, O]($observableExpr, $caseListExpr, $handlerMapExpr)} => innerHandleCaseImpl(observableExpr, caseListExpr, handlerMapExpr, casePfExpr, handleFnExpr) - case other => report.errorAndAbort("Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly") + case '{ + MatchSplitObservable.build[Self, I, O]( + $observableExpr, + $caseListExpr, + $handlerMapExpr + ) + } => + innerHandleCaseImpl( + observableExpr, + caseListExpr, + handlerMapExpr, + casePfExpr, + handleFnExpr + ) + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + ) } } - private def innerHandleCaseImpl[Self[+_] <: Observable[_] : Type, I: Type, O: Type, O1 >: O : Type, A: Type, B: Type]( + private def handleTypeImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, T: Type]( + matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], + casePfExpr: Expr[PartialFunction[T, T]] + )( + using quotes: Quotes + ): Expr[MatchTypeObservable[Self, I, O, T]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ + MatchSplitObservable.build[Self, I, O]( + $observableExpr, + $caseListExpr, + $handlerMapExpr + ) + } => + '{ + MatchTypeObservable.build[Self, I, O, T]( + $observableExpr, + $caseListExpr, + $handlerMapExpr, + $casePfExpr + ) + } + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + ) + } + } + + private def handleTypeApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, T: Type]( + matchSplitObservableExpr: Expr[MatchTypeObservable[Self, I, O, T]], + handleFnExpr: Expr[Function[(T, Signal[T]), O1]] + )( + using quotes: Quotes + ): Expr[MatchSplitObservable[Self, I, O1]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ + MatchTypeObservable.build[Self, I, O, T]( + $observableExpr, + $caseListExpr, + $handlerMapExpr, + $tCaseExpr + ) + } => + innerHandleCaseImpl[Self, I, O, O1, T, T]( + observableExpr, + caseListExpr, + handlerMapExpr, + tCaseExpr, + handleFnExpr + ) + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + ) + } + } + + private def innerHandleCaseImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, A: Type, B: Type]( observableExpr: Expr[BaseObservable[Self, I]], caseListExpr: Expr[List[PartialFunction[Any, Any]]], handlerMapExpr: Expr[Map[Int, Function[Any, O]]], @@ -82,13 +194,25 @@ object SplittableTypeMacros { )( using quotes: Quotes ): Expr[MatchSplitObservable[Self, I, O1]] = { + import quotes.reflect.* + + report.info(Printer.TreeCode.show(casePfExpr.asTerm)) + val caseExprList = exprOfListToListOfExpr(caseListExpr) - val nextCaseExprList = casePfExpr.asExprOf[PartialFunction[Any, Any]] :: caseExprList + val nextCaseExprList = + casePfExpr.asExprOf[PartialFunction[Any, Any]] :: caseExprList val nextCaseListExpr = listOfExprToExprOfList(nextCaseExprList) - '{ MatchSplitObservable.build[Self, I, O1]($observableExpr, $nextCaseListExpr, ($handlerMapExpr + ($handlerMapExpr.size -> $handleFnExpr.asInstanceOf[Function[Any, O1]]))) } + '{ + MatchSplitObservable.build[Self, I, O1]( + $observableExpr, + $nextCaseListExpr, + ($handlerMapExpr + ($handlerMapExpr.size -> $handleFnExpr + .asInstanceOf[Function[Any, O1]])) + ) + } } private def exprOfListToListOfExpr( @@ -97,14 +221,17 @@ object SplittableTypeMacros { using quotes: Quotes ): List[Expr[PartialFunction[Any, Any]]] = { import quotes.reflect.* - + pfListExpr match { - case '{ $headExpr :: (${tailExpr}: List[PartialFunction[Any, Any]]) } => + case '{ $headExpr :: (${ tailExpr }: List[PartialFunction[Any, Any]]) } => headExpr :: exprOfListToListOfExpr(tailExpr) case '{ Nil } => Nil - case _ => report.errorAndAbort("Macro expansion failed, please use `handleCase` instead of modify MatchSplitObservable explicitly") + case _ => + report.errorAndAbort( + "Macro expansion failed, please use `handleCase` instead of modify MatchSplitObservable explicitly" + ) } - + } private def listOfExprToExprOfList( @@ -113,29 +240,31 @@ object SplittableTypeMacros { using quotes: Quotes ): Expr[List[PartialFunction[Any, Any]]] = { import quotes.reflect.* - + pfExprList match - case head :: tail => '{ $head :: ${listOfExprToExprOfList(tail)} } - case Nil => '{ Nil } + case head :: tail => '{ $head :: ${ listOfExprToExprOfList(tail) } } + case Nil => '{ Nil } } private inline def toSplittableOneObservable[Self[+_] <: Observable[_], O]( parentObservable: BaseObservable[Self, (Int, Any)], handlerMap: Map[Int, Function[Any, O]] ): Self[O] = { - parentObservable.matchStreamOrSignal( - ifStream = _.splitOne(_._1) { case (idx, (_, b), dataSignal) => - val bSignal = dataSignal.map(_._2) - handlerMap.apply(idx).apply(b -> bSignal) - }, - ifSignal = _.splitOne(_._1) { case (idx, (_, b), dataSignal) => - val bSignal = dataSignal.map(_._2) - handlerMap.apply(idx).apply(b -> bSignal) - } - ).asInstanceOf[Self[O]] // #TODO[Integrity] Same as FlatMap/AsyncStatusObservable, how to type this properly? + parentObservable + .matchStreamOrSignal( + ifStream = _.splitOne(_._1) { case (idx, (_, b), dataSignal) => + val bSignal = dataSignal.map(_._2) + handlerMap.apply(idx).apply(b -> bSignal) + }, + ifSignal = _.splitOne(_._1) { case (idx, (_, b), dataSignal) => + val bSignal = dataSignal.map(_._2) + handlerMap.apply(idx).apply(b -> bSignal) + } + ) + .asInstanceOf[Self[O]] // #TODO[Integrity] Same as FlatMap/AsyncStatusObservable, how to type this properly? } - private def observableImpl[Self[+_] <: Observable[_] : Type, I: Type, O: Type]( + private def observableImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type]( matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]] )( using quotes: Quotes @@ -143,16 +272,29 @@ object SplittableTypeMacros { import quotes.reflect.* matchSplitObservableExpr match { - case '{ MatchSplitObservable.build[Self, I, O]($_, Nil, $_)} => - report.errorAndAbort("Macro expansion failed, need at least one handleCase") - case '{ MatchSplitObservable.build[Self, I, O]($observableExpr, $caseListExpr, $handlerMapExpr)} => - '{toSplittableOneObservable( - $observableExpr - .map(i => ${ innerObservableImpl('i, caseListExpr) }) - .asInstanceOf[BaseObservable[Self, (Int, Any)]], - $handlerMapExpr - )} - case _ => report.errorAndAbort("Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly") + case '{ MatchSplitObservable.build[Self, I, O]($_, Nil, $_) } => + report.errorAndAbort( + "Macro expansion failed, need at least one handleCase" + ) + case '{ + MatchSplitObservable.build[Self, I, O]( + $observableExpr, + $caseListExpr, + $handlerMapExpr + ) + } => + '{ + toSplittableOneObservable( + $observableExpr + .map(i => ${ innerObservableImpl('i, caseListExpr) }) + .asInstanceOf[BaseObservable[Self, (Int, Any)]], + $handlerMapExpr + ) + } + case _ => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + ) } } @@ -166,18 +308,29 @@ object SplittableTypeMacros { val caseExprList = exprOfListToListOfExpr(caseListExpr) - val allCaseDefLists = caseExprList.reverse.zipWithIndex.flatMap { case (caseExpr, idx) => - caseExpr.asTerm match { - case Lambda(_, Match(_, caseDefList)) => { - caseDefList.map { caseDef => - val idxExpr = Expr.apply(idx) - val newRhsExpr = '{ val res = ${caseDef.rhs.asExprOf[Any]}; ($idxExpr, res)} - CaseDef.copy(caseDef)(caseDef.pattern, caseDef.guard, newRhsExpr.asTerm) + val allCaseDefLists = caseExprList.reverse.zipWithIndex + .flatMap { case (caseExpr, idx) => + caseExpr.asTerm match { + case Lambda(_, Match(_, caseDefList)) => { + caseDefList.map { caseDef => + val idxExpr = Expr.apply(idx) + val newRhsExpr = '{ + val res = ${ caseDef.rhs.asExprOf[Any] }; ($idxExpr, res) + } + CaseDef.copy(caseDef)( + caseDef.pattern, + caseDef.guard, + newRhsExpr.asTerm + ) + } } + case _ => + report.errorAndAbort( + "Macro expansion failed, please use `handleCase` with annonymous partial function" + ) } - case _ => report.errorAndAbort("Macro expansion failed, please use `handleCase` with annonymous partial function") } - }.map(_.changeOwner(Symbol.spliceOwner)) + .map(_.changeOwner(Symbol.spliceOwner)) Match(iExpr.asTerm, allCaseDefLists).asExprOf[(Int, Any)] } diff --git a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala index b8922f73..5ba98912 100644 --- a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala +++ b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala @@ -4,7 +4,7 @@ import com.raquo.airstream.UnitSpec import com.raquo.airstream.eventbus.EventBus import com.raquo.airstream.fixtures.{Effect, TestableOwner} import com.raquo.airstream.state.Var -import com.raquo.airstream.split.SplittableTypeMacros.{splitMatch, handleCase, toSignal, toStream} +import com.raquo.airstream.split.SplittableTypeMacros.* import scala.collection.{immutable, mutable} import scala.scalajs.js @@ -44,7 +44,7 @@ class SplittableTypeSpec extends UnitSpec { Res(str) } - .handleCase { case baz: Baz => baz } { case (baz, bazSignal) => + .handleType[Baz] { case (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -276,7 +276,7 @@ class SplittableTypeSpec extends UnitSpec { Res(str) } - .handleCase { case baz: Baz => baz } { case (baz, bazSignal) => + .handleType[Baz] { case (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. From 36926bb37f9e82c8e45923f0244c57fdbedb40cc Mon Sep 17 00:00:00 2001 From: HollandDM Date: Mon, 16 Sep 2024 16:39:42 +0700 Subject: [PATCH 08/13] feat: remove debug info and sanitize test cases --- .../split/SplittableTypeMacros.scala | 2 - .../airstream/split/SplittableTypeSpec.scala | 76 +++++++++---------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index ef0dca8e..7f8ee97a 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -196,8 +196,6 @@ object SplittableTypeMacros { ): Expr[MatchSplitObservable[Self, I, O1]] = { import quotes.reflect.* - report.info(Printer.TreeCode.show(casePfExpr.asTerm)) - val caseExprList = exprOfListToListOfExpr(caseListExpr) val nextCaseExprList = diff --git a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala index 5ba98912..66860633 100644 --- a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala +++ b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala @@ -42,7 +42,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Bar-$str") }(owner) - Res(str) + Res("Bar") } .handleType[Baz] { case (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") @@ -52,7 +52,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Baz-${baz.ordinal}-${baz.toString}") }(owner) - Res(baz) + Res("Baz") } .handleCase { case Tar => 10 } { case (int, intSignal) => effects += Effect("init-child", s"Tar-${int}") @@ -62,7 +62,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Tar-${int}") }(owner) - Res(int) + Res("Tar") } .toSignal @@ -73,7 +73,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Bar-initial"), Effect("update-child", "Bar-initial"), - Effect("result", "Res(initial)") + Effect("result", "Res(Bar)") ) effects.clear() @@ -81,7 +81,7 @@ class SplittableTypeSpec extends UnitSpec { myVar.writer.onNext(Bar(None)) effects shouldBe mutable.Buffer( - Effect("result", "Res(initial)"), + Effect("result", "Res(Bar)"), Effect("update-child", "Bar-null") ) @@ -90,7 +90,7 @@ class SplittableTypeSpec extends UnitSpec { myVar.writer.onNext(Bar(None)) effects shouldBe mutable.Buffer( - Effect("result", "Res(initial)"), // sematically, splitMatch/handleCase/toSignal use splitOne underlying, so this is the same as splitOne spec + Effect("result", "Res(Bar)"), // sematically, splitMatch/handleCase/toSignal use splitOne underlying, so this is the same as splitOne spec Effect("update-child", "Bar-null") ) @@ -99,7 +99,7 @@ class SplittableTypeSpec extends UnitSpec { myVar.writer.onNext(Bar(Some("other"))) effects shouldBe mutable.Buffer( - Effect("result", "Res(initial)"), + Effect("result", "Res(Bar)"), Effect("update-child", "Bar-other") ) @@ -112,7 +112,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Baz-0-Baz1"), Effect("update-child", "Baz-0-Baz1"), - Effect("result", "Res(Baz1)") + Effect("result", "Res(Baz)") ) effects.clear() @@ -120,7 +120,7 @@ class SplittableTypeSpec extends UnitSpec { myVar.writer.onNext(Baz.Baz2) effects shouldBe mutable.Buffer( - Effect("result", "Res(Baz1)"), + Effect("result", "Res(Baz)"), Effect("update-child", "Baz-1-Baz2") ) @@ -129,7 +129,7 @@ class SplittableTypeSpec extends UnitSpec { myVar.writer.onNext(Baz.Baz2) effects shouldBe mutable.Buffer( - Effect("result", "Res(Baz1)"), + Effect("result", "Res(Baz)"), Effect("update-child", "Baz-1-Baz2") ) @@ -140,7 +140,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Tar-10"), Effect("update-child", "Tar-10"), - Effect("result", "Res(10)") + Effect("result", "Res(Tar)") ) effects.clear() @@ -148,7 +148,7 @@ class SplittableTypeSpec extends UnitSpec { myVar.writer.onNext(Tar) effects shouldBe mutable.Buffer( - Effect("result", "Res(10)"), + Effect("result", "Res(Tar)"), Effect("update-child", "Tar-10") ) @@ -176,7 +176,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Bar-$str") }(owner) - Res(str) + Res("Bar") } .handleCase { case baz: Baz.Baz1.type => baz } { case (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") @@ -186,7 +186,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Baz-${baz.ordinal}-${baz.toString}") }(owner) - Res(baz) + Res("Baz1") } .handleCase { case Tar => 10 } { case (int, intSignal) => effects += Effect("init-child", s"Tar-${int}") @@ -196,7 +196,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Tar-${int}") }(owner) - Res(int) + Res("Tar") } .toSignal @@ -207,7 +207,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Bar-initial"), Effect("update-child", "Bar-initial"), - Effect("result", "Res(initial)") + Effect("result", "Res(Bar)") ) effects.clear() @@ -215,7 +215,7 @@ class SplittableTypeSpec extends UnitSpec { myVar.writer.onNext(Bar(Some("other"))) effects shouldBe mutable.Buffer( - Effect("result", "Res(initial)"), + Effect("result", "Res(Bar)"), Effect("update-child", "Bar-other") ) @@ -238,7 +238,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Tar-10"), Effect("update-child", "Tar-10"), - Effect("result", "Res(10)") + Effect("result", "Res(Tar)") ) effects.clear() @@ -246,7 +246,7 @@ class SplittableTypeSpec extends UnitSpec { myVar.writer.onNext(Tar) effects shouldBe mutable.Buffer( - Effect("result", "Res(10)"), + Effect("result", "Res(Tar)"), Effect("update-child", "Tar-10") ) @@ -274,7 +274,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Bar-$str") }(owner) - Res(str) + Res("Bar") } .handleType[Baz] { case (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") @@ -284,7 +284,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Baz-${baz.ordinal}-${baz.toString}") }(owner) - Res(baz) + Res("Baz") } .handleCase { case Tar => 10 } { case (int, intSignal) => effects += Effect("init-child", s"Tar-${int}") @@ -294,7 +294,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Tar-${int}") }(owner) - Res(int) + Res("Tar") } .toStream @@ -307,7 +307,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Bar-null"), Effect("update-child", "Bar-null"), - Effect("result", "Res(null)") + Effect("result", "Res(Bar)") ) effects.clear() @@ -315,7 +315,7 @@ class SplittableTypeSpec extends UnitSpec { myEventBus.writer.onNext(Bar(None)) effects shouldBe mutable.Buffer( - Effect("result", "Res(null)"), // sematically, splitMatch/handleCase/toStream use splitOne underlying, so this is the same as splitOne spec + Effect("result", "Res(Bar)"), // sematically, splitMatch/handleCase/toStream use splitOne underlying, so this is the same as splitOne spec Effect("update-child", "Bar-null") ) @@ -324,7 +324,7 @@ class SplittableTypeSpec extends UnitSpec { myEventBus.writer.onNext(Bar(Some("other"))) effects shouldBe mutable.Buffer( - Effect("result", "Res(null)"), + Effect("result", "Res(Bar)"), Effect("update-child", "Bar-other") ) @@ -337,7 +337,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Baz-0-Baz1"), Effect("update-child", "Baz-0-Baz1"), - Effect("result", "Res(Baz1)") + Effect("result", "Res(Baz)") ) effects.clear() @@ -345,7 +345,7 @@ class SplittableTypeSpec extends UnitSpec { myEventBus.writer.onNext(Baz.Baz2) effects shouldBe mutable.Buffer( - Effect("result", "Res(Baz1)"), + Effect("result", "Res(Baz)"), Effect("update-child", "Baz-1-Baz2") ) @@ -354,7 +354,7 @@ class SplittableTypeSpec extends UnitSpec { myEventBus.writer.onNext(Baz.Baz2) effects shouldBe mutable.Buffer( - Effect("result", "Res(Baz1)"), + Effect("result", "Res(Baz)"), Effect("update-child", "Baz-1-Baz2") ) @@ -365,7 +365,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Tar-10"), Effect("update-child", "Tar-10"), - Effect("result", "Res(10)") + Effect("result", "Res(Tar)") ) effects.clear() @@ -373,7 +373,7 @@ class SplittableTypeSpec extends UnitSpec { myEventBus.writer.onNext(Tar) effects shouldBe mutable.Buffer( - Effect("result", "Res(10)"), + Effect("result", "Res(Tar)"), Effect("update-child", "Tar-10") ) @@ -402,9 +402,9 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Bar-$str") }(owner) - Res(str) + Res("Bar") } - .handleCase { case baz: Baz => baz } { case (baz, bazSignal) => + .handleType[Baz] { case (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -412,7 +412,7 @@ class SplittableTypeSpec extends UnitSpec { effects += Effect("update-child", s"Baz-${baz.ordinal}-${baz.toString}") }(owner) - Res(baz) + Res("Baz") } .toStream @@ -425,7 +425,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Bar-null"), Effect("update-child", "Bar-null"), - Effect("result", "Res(null)") + Effect("result", "Res(Bar)") ) effects.clear() @@ -433,7 +433,7 @@ class SplittableTypeSpec extends UnitSpec { myEventBus.writer.onNext(Bar(None)) effects shouldBe mutable.Buffer( - Effect("result", "Res(null)"), // sematically, splitMatch/handleCase/toStream use splitOne underlying, so this is the same as splitOne spec + Effect("result", "Res(Bar)"), // sematically, splitMatch/handleCase/toStream use splitOne underlying, so this is the same as splitOne spec Effect("update-child", "Bar-null") ) @@ -446,7 +446,7 @@ class SplittableTypeSpec extends UnitSpec { effects shouldBe mutable.Buffer( Effect("init-child", "Baz-0-Baz1"), Effect("update-child", "Baz-0-Baz1"), - Effect("result", "Res(Baz1)") + Effect("result", "Res(Baz)") ) effects.clear() @@ -454,7 +454,7 @@ class SplittableTypeSpec extends UnitSpec { myEventBus.writer.onNext(Baz.Baz2) effects shouldBe mutable.Buffer( - Effect("result", "Res(Baz1)"), + Effect("result", "Res(Baz)"), Effect("update-child", "Baz-1-Baz2") ) @@ -463,7 +463,7 @@ class SplittableTypeSpec extends UnitSpec { myEventBus.writer.onNext(Baz.Baz2) effects shouldBe mutable.Buffer( - Effect("result", "Res(Baz1)"), + Effect("result", "Res(Baz)"), Effect("update-child", "Baz-1-Baz2") ) From 27dadd26c685f2920cb2dac05b1362cd38527b28 Mon Sep 17 00:00:00 2001 From: HollandDM Date: Fri, 27 Sep 2024 17:51:46 +0700 Subject: [PATCH 09/13] feat: add handleValue --- .../split/MatchValueObservable.scala | 52 ++++++++++++ .../split/SplittableTypeMacros.scala | 83 ++++++++++++++++++- .../airstream/split/SplittableTypeSpec.scala | 8 +- 3 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala new file mode 100644 index 00000000..55df03a1 --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala @@ -0,0 +1,52 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.core.{Observable, BaseObservable} +import scala.annotation.compileTimeOnly + +/** `MatchSingletonObservable` served as macro's data holder for macro expansion. + * + * For example: + * + * ```scala + * fooSignal.splitMatch + * .splitValue(Tar)(tarSignal => renderTarNode(tarSignal)) + * ``` + * + * will be expanded sematically into: + * + * ```scala + * MatchTypeObservable.build[*, *, *, Baz]( + * fooSignal, + * Nil, + * handlerMap, + * ({ case Tar => Tar }) + * ) + * ``` + * + * and then into: + * + * ```scala + * MatchSplitObservable.build( + * fooSignal, + * ({ case Tar => Tar }) :: Nil, + * handlerMap + * ) + * ``` + */ + +opaque type MatchValueObservable[Self[+_] <: Observable[_], I, O, V0, V1] = Unit + +object MatchValueObservable { + + @compileTimeOnly("splitMatch without toSignal/toStream is illegal") + def build[Self[+_] <: Observable[_], I, O, V0, V1]( + observable: BaseObservable[Self, I], + caseList: List[PartialFunction[Any, Any]], + handlerMap: Map[Int, Function[Any, O]], + vCast: PartialFunction[V0, V1] + ): MatchValueObservable[Self, I, O, V0, V1] = + throw new UnsupportedOperationException( + "splitMatch without toSignal/toStream is illegal" + ) + +} diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index 7f8ee97a..35c8b013 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -7,6 +7,8 @@ import com.raquo.airstream.core.{ BaseObservable } import scala.quoted.{Expr, Quotes, Type} +import scala.annotation.{unused, targetName} +import scala.compiletime.summonInline /** `SplittableTypeMacros` turns this code * @@ -72,14 +74,28 @@ object SplittableTypeMacros { } inline def handleType[T]: MatchTypeObservable[Self, I, O, T] = handlePfType[T] { case t: T => t } + + inline private def handlePfValue[V](inline casePf: PartialFunction[Any, V]) = ${ + handleValueImpl[Self, I, O, V]('{ matchSplitObservable }, '{ casePf }) + } + + inline def handleValue[V](inline v: V)(using inline valueOf: ValueOf[V]): MatchValueObservable[Self, I, O, V, V] = handlePfValue[V] { case _: V => v } } extension [Self[+_] <: Observable[_], I, O, T](inline matchTypeObserver: MatchTypeObservable[Self, I, O, T]) { - inline def apply[O1 >: O](inline handleFn: ((T, Signal[T])) => O1) = ${ + inline def apply[O1 >: O](inline handleFn: ((T, Signal[T])) => O1): MatchSplitObservable[Self, I, O1] = ${ handleTypeApplyImpl('{ matchTypeObserver }, '{ handleFn }) } } + extension [Self[+_] <: Observable[_], I, O, V0, V1](inline matchValueObservable: MatchValueObservable[Self, I, O, V0, V1]) { + inline private def deglate[O1 >: O](inline handleFn: ((V1, Signal[V1])) => O1) = ${ + handleValueApplyImpl('{ matchValueObservable }, '{ handleFn }) + } + + inline def apply[O1 >: O](inline handleFn: Signal[V1] => O1): MatchSplitObservable[Self, I, O1] = deglate { case (_, vSignal) => handleFn(vSignal) } + } + extension [I, O](inline matchSplitObservable: MatchSplitObservable[Signal, I, O]) { inline def toSignal: Signal[O] = ${ observableImpl('{ matchSplitObservable }) @@ -185,6 +201,68 @@ object SplittableTypeMacros { } } + private def handleValueImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, V: Type]( + matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], + casePfExpr: Expr[PartialFunction[V, V]] + )( + using quotes: Quotes + ): Expr[MatchValueObservable[Self, I, O, V, V]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ + MatchSplitObservable.build[Self, I, O]( + $observableExpr, + $caseListExpr, + $handlerMapExpr + ) + } => + '{ + MatchValueObservable.build[Self, I, O, V, V]( + $observableExpr, + $caseListExpr, + $handlerMapExpr, + $casePfExpr + ) + } + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + ) + } + } + + private def handleValueApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, V0: Type, V1: Type]( + matchValueObservableExpr: Expr[MatchValueObservable[Self, I, O, V0, V1]], + handleFnExpr: Expr[Function[(V1, Signal[V1]), O1]] + )( + using quotes: Quotes + ): Expr[MatchSplitObservable[Self, I, O1]] = { + import quotes.reflect.* + + matchValueObservableExpr match { + case '{ + MatchValueObservable.build[Self, I, O, V0, V1]( + $observableExpr, + $caseListExpr, + $handlerMapExpr, + $tCaseExpr + ) + } => + innerHandleCaseImpl[Self, I, O, O1, V0, V1]( + observableExpr, + caseListExpr, + handlerMapExpr, + tCaseExpr, + handleFnExpr + ) + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + ) + } + } + private def innerHandleCaseImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, A: Type, B: Type]( observableExpr: Expr[BaseObservable[Self, I]], caseListExpr: Expr[List[PartialFunction[Any, Any]]], @@ -330,6 +408,9 @@ object SplittableTypeMacros { } .map(_.changeOwner(Symbol.spliceOwner)) + // val matchExpr = Match(iExpr.asTerm, allCaseDefLists).asExprOf[(Int, Any)] + // report.info(matchExpr.show) + // matchExpr Match(iExpr.asTerm, allCaseDefLists).asExprOf[(Int, Any)] } diff --git a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala index 66860633..08f122e3 100644 --- a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala +++ b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala @@ -54,12 +54,12 @@ class SplittableTypeSpec extends UnitSpec { Res("Baz") } - .handleCase { case Tar => 10 } { case (int, intSignal) => - effects += Effect("init-child", s"Tar-${int}") + .handleValue(Tar) { tarSignal => + effects += Effect("init-child", s"Tar-${10}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. - intSignal.foreach { int => - effects += Effect("update-child", s"Tar-${int}") + tarSignal.foreach { _ => + effects += Effect("update-child", s"Tar-${10}") }(owner) Res("Tar") From d7f1ef9d88c85edb3e4ca453f21fe454f6cdd50d Mon Sep 17 00:00:00 2001 From: HollandDM Date: Fri, 27 Sep 2024 18:02:10 +0700 Subject: [PATCH 10/13] ref: change handleCase signature --- .../split/MatchSplitObservable.scala | 2 +- .../airstream/split/MatchTypeObservable.scala | 2 +- .../split/MatchValueObservable.scala | 2 +- .../split/SplittableTypeMacros.scala | 28 ++++++++-------- .../airstream/split/SplittableTypeSpec.scala | 32 +++++++++---------- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala index 0c4b0732..abb7bffc 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala @@ -29,7 +29,7 @@ object MatchSplitObservable { def build[Self[+_] <: Observable[_] , I, O]( observable: BaseObservable[Self, I], caseList: List[PartialFunction[Any, Any]], - handlerMap: Map[Int, Function[Any, O]] + handlerMap: Map[Int, Function2[Any, Any, O]] ): MatchSplitObservable[Self, I, O] = throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") } diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala index a32b12a8..11019753 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala @@ -42,7 +42,7 @@ object MatchTypeObservable { def build[Self[+_] <: Observable[_], I, O, T]( observable: BaseObservable[Self, I], caseList: List[PartialFunction[Any, Any]], - handlerMap: Map[Int, Function[Any, O]], + handlerMap: Map[Int, Function2[Any, Any, O]], tCast: PartialFunction[T, T] ): MatchTypeObservable[Self, I, O, T] = throw new UnsupportedOperationException( diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala index 55df03a1..4a1ef776 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala @@ -42,7 +42,7 @@ object MatchValueObservable { def build[Self[+_] <: Observable[_], I, O, V0, V1]( observable: BaseObservable[Self, I], caseList: List[PartialFunction[Any, Any]], - handlerMap: Map[Int, Function[Any, O]], + handlerMap: Map[Int, Function2[Any, Any, O]], vCast: PartialFunction[V0, V1] ): MatchValueObservable[Self, I, O, V0, V1] = throw new UnsupportedOperationException( diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index 35c8b013..5941ac49 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -58,14 +58,14 @@ object SplittableTypeMacros { MatchSplitObservable.build( observable, Nil, - Map.empty[Int, Function[Any, Nothing]] + Map.empty[Int, Function2[Any, Any, Nothing]] ) } extension [Self[+_] <: Observable[_], I, O]( inline matchSplitObservable: MatchSplitObservable[Self, I, O] ) { - inline def handleCase[A, B, O1 >: O](inline casePf: PartialFunction[A, B])(inline handleFn: ((B, Signal[B])) => O1) = ${ + inline def handleCase[A, B, O1 >: O](inline casePf: PartialFunction[A, B])(inline handleFn: (B, Signal[B]) => O1) = ${ handleCaseImpl('{ matchSplitObservable }, '{ casePf }, '{ handleFn }) } @@ -83,17 +83,17 @@ object SplittableTypeMacros { } extension [Self[+_] <: Observable[_], I, O, T](inline matchTypeObserver: MatchTypeObservable[Self, I, O, T]) { - inline def apply[O1 >: O](inline handleFn: ((T, Signal[T])) => O1): MatchSplitObservable[Self, I, O1] = ${ + inline def apply[O1 >: O](inline handleFn: (T, Signal[T]) => O1): MatchSplitObservable[Self, I, O1] = ${ handleTypeApplyImpl('{ matchTypeObserver }, '{ handleFn }) } } extension [Self[+_] <: Observable[_], I, O, V0, V1](inline matchValueObservable: MatchValueObservable[Self, I, O, V0, V1]) { - inline private def deglate[O1 >: O](inline handleFn: ((V1, Signal[V1])) => O1) = ${ + inline private def deglate[O1 >: O](inline handleFn: (V1, Signal[V1]) => O1) = ${ handleValueApplyImpl('{ matchValueObservable }, '{ handleFn }) } - inline def apply[O1 >: O](inline handleFn: Signal[V1] => O1): MatchSplitObservable[Self, I, O1] = deglate { case (_, vSignal) => handleFn(vSignal) } + inline def apply[O1 >: O](inline handleFn: Signal[V1] => O1): MatchSplitObservable[Self, I, O1] = deglate { (_, vSignal) => handleFn(vSignal) } } extension [I, O](inline matchSplitObservable: MatchSplitObservable[Signal, I, O]) { @@ -111,7 +111,7 @@ object SplittableTypeMacros { private def handleCaseImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, A: Type, B: Type]( matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], casePfExpr: Expr[PartialFunction[A, B]], - handleFnExpr: Expr[Function[(B, Signal[B]), O1]] + handleFnExpr: Expr[Function2[B, Signal[B], O1]] )( using quotes: Quotes ): Expr[MatchSplitObservable[Self, I, O1]] = { @@ -172,7 +172,7 @@ object SplittableTypeMacros { private def handleTypeApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, T: Type]( matchSplitObservableExpr: Expr[MatchTypeObservable[Self, I, O, T]], - handleFnExpr: Expr[Function[(T, Signal[T]), O1]] + handleFnExpr: Expr[Function2[T, Signal[T], O1]] )( using quotes: Quotes ): Expr[MatchSplitObservable[Self, I, O1]] = { @@ -234,7 +234,7 @@ object SplittableTypeMacros { private def handleValueApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, V0: Type, V1: Type]( matchValueObservableExpr: Expr[MatchValueObservable[Self, I, O, V0, V1]], - handleFnExpr: Expr[Function[(V1, Signal[V1]), O1]] + handleFnExpr: Expr[Function2[V1, Signal[V1], O1]] )( using quotes: Quotes ): Expr[MatchSplitObservable[Self, I, O1]] = { @@ -266,9 +266,9 @@ object SplittableTypeMacros { private def innerHandleCaseImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, A: Type, B: Type]( observableExpr: Expr[BaseObservable[Self, I]], caseListExpr: Expr[List[PartialFunction[Any, Any]]], - handlerMapExpr: Expr[Map[Int, Function[Any, O]]], + handlerMapExpr: Expr[Map[Int, Function2[Any, Any, O]]], casePfExpr: Expr[PartialFunction[A, B]], - handleFnExpr: Expr[Function[(B, Signal[B]), O1]] + handleFnExpr: Expr[Function2[B, Signal[B], O1]] )( using quotes: Quotes ): Expr[MatchSplitObservable[Self, I, O1]] = { @@ -286,7 +286,7 @@ object SplittableTypeMacros { $observableExpr, $nextCaseListExpr, ($handlerMapExpr + ($handlerMapExpr.size -> $handleFnExpr - .asInstanceOf[Function[Any, O1]])) + .asInstanceOf[Function2[Any, Any, O1]])) ) } } @@ -324,17 +324,17 @@ object SplittableTypeMacros { private inline def toSplittableOneObservable[Self[+_] <: Observable[_], O]( parentObservable: BaseObservable[Self, (Int, Any)], - handlerMap: Map[Int, Function[Any, O]] + handlerMap: Map[Int, Function2[Any, Any, O]] ): Self[O] = { parentObservable .matchStreamOrSignal( ifStream = _.splitOne(_._1) { case (idx, (_, b), dataSignal) => val bSignal = dataSignal.map(_._2) - handlerMap.apply(idx).apply(b -> bSignal) + handlerMap.apply(idx).apply(b, bSignal) }, ifSignal = _.splitOne(_._1) { case (idx, (_, b), dataSignal) => val bSignal = dataSignal.map(_._2) - handlerMap.apply(idx).apply(b -> bSignal) + handlerMap.apply(idx).apply(b, bSignal) } ) .asInstanceOf[Self[O]] // #TODO[Integrity] Same as FlatMap/AsyncStatusObservable, how to type this properly? diff --git a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala index 08f122e3..a20993b0 100644 --- a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala +++ b/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala @@ -34,7 +34,7 @@ class SplittableTypeSpec extends UnitSpec { .handleCase { case Bar(Some(str)) => str case Bar(None) => "null" - } { case (str, strSignal) => + } { (str, strSignal) => effects += Effect("init-child", s"Bar-$str") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -44,7 +44,7 @@ class SplittableTypeSpec extends UnitSpec { Res("Bar") } - .handleType[Baz] { case (baz, bazSignal) => + .handleType[Baz] { (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -168,7 +168,7 @@ class SplittableTypeSpec extends UnitSpec { .splitMatch .handleCase { case Bar(Some(str)) => str - } { case (str, strSignal) => + } { (str, strSignal) => effects += Effect("init-child", s"Bar-$str") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -178,7 +178,7 @@ class SplittableTypeSpec extends UnitSpec { Res("Bar") } - .handleCase { case baz: Baz.Baz1.type => baz } { case (baz, bazSignal) => + .handleType[Baz.Baz1.type] { (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -188,12 +188,12 @@ class SplittableTypeSpec extends UnitSpec { Res("Baz1") } - .handleCase { case Tar => 10 } { case (int, intSignal) => - effects += Effect("init-child", s"Tar-${int}") + .handleValue(Tar) { tarSignal => + effects += Effect("init-child", s"Tar-${10}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. - intSignal.foreach { int => - effects += Effect("update-child", s"Tar-${int}") + tarSignal.foreach { _ => + effects += Effect("update-child", s"Tar-${10}") }(owner) Res("Tar") @@ -266,7 +266,7 @@ class SplittableTypeSpec extends UnitSpec { .handleCase { case Bar(Some(str)) => str case Bar(None) => "null" - } { case (str, strSignal) => + } { (str, strSignal) => effects += Effect("init-child", s"Bar-$str") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -276,7 +276,7 @@ class SplittableTypeSpec extends UnitSpec { Res("Bar") } - .handleType[Baz] { case (baz, bazSignal) => + .handleType[Baz] { (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -286,12 +286,12 @@ class SplittableTypeSpec extends UnitSpec { Res("Baz") } - .handleCase { case Tar => 10 } { case (int, intSignal) => - effects += Effect("init-child", s"Tar-${int}") + .handleValue(Tar){ tarSignal => + effects += Effect("init-child", s"Tar-${10}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. - intSignal.foreach { int => - effects += Effect("update-child", s"Tar-${int}") + tarSignal.foreach { _ => + effects += Effect("update-child", s"Tar-${10}") }(owner) Res("Tar") @@ -394,7 +394,7 @@ class SplittableTypeSpec extends UnitSpec { .splitMatch .handleCase { case Bar(None) => "null" - } { case (str, strSignal) => + } { (str, strSignal) => effects += Effect("init-child", s"Bar-$str") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. @@ -404,7 +404,7 @@ class SplittableTypeSpec extends UnitSpec { Res("Bar") } - .handleType[Baz] { case (baz, bazSignal) => + .handleType[Baz] { (baz, bazSignal) => effects += Effect("init-child", s"Baz-${baz.ordinal}-${baz.toString}") // @Note keep foreach or addObserver here – this is important. // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. From 649f7692b4dcfb1c6d5b4098d0dabc8de6db80b9 Mon Sep 17 00:00:00 2001 From: HollandDM Date: Sat, 28 Sep 2024 11:52:00 +0700 Subject: [PATCH 11/13] ref: opaque to anyval --- .../split/MatchSplitObservable.scala | 2 +- .../airstream/split/MatchTypeObservable.scala | 2 +- .../split/MatchValueObservable.scala | 8 +++---- .../split/SplittableTypeMacros.scala | 22 +++++++++---------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala index abb7bffc..53153403 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala @@ -21,7 +21,7 @@ import scala.annotation.compileTimeOnly * ``` */ -opaque type MatchSplitObservable[Self[+_] <: Observable[_] , I, O] = Unit +final case class MatchSplitObservable[Self[+_] <: Observable[_] , I, O] private (private val underlying: Unit) extends AnyVal object MatchSplitObservable { diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala index 11019753..d8e50942 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala @@ -34,7 +34,7 @@ import scala.annotation.compileTimeOnly * ``` */ -opaque type MatchTypeObservable[Self[+_] <: Observable[_], I, O, T] = Unit +final case class MatchTypeObservable[Self[+_] <: Observable[_], I, O, T] private (private val underlying: Unit) extends AnyVal object MatchTypeObservable { diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala b/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala index 4a1ef776..cbb193fe 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala @@ -34,17 +34,17 @@ import scala.annotation.compileTimeOnly * ``` */ -opaque type MatchValueObservable[Self[+_] <: Observable[_], I, O, V0, V1] = Unit +final case class MatchValueObservable[Self[+_] <: Observable[_], I, O, V] private (private val underlying: Unit) extends AnyVal object MatchValueObservable { @compileTimeOnly("splitMatch without toSignal/toStream is illegal") - def build[Self[+_] <: Observable[_], I, O, V0, V1]( + def build[Self[+_] <: Observable[_], I, O, V]( observable: BaseObservable[Self, I], caseList: List[PartialFunction[Any, Any]], handlerMap: Map[Int, Function2[Any, Any, O]], - vCast: PartialFunction[V0, V1] - ): MatchValueObservable[Self, I, O, V0, V1] = + vCast: PartialFunction[V, V] + ): MatchValueObservable[Self, I, O, V] = throw new UnsupportedOperationException( "splitMatch without toSignal/toStream is illegal" ) diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala index 5941ac49..6b5fd05c 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala @@ -79,7 +79,7 @@ object SplittableTypeMacros { handleValueImpl[Self, I, O, V]('{ matchSplitObservable }, '{ casePf }) } - inline def handleValue[V](inline v: V)(using inline valueOf: ValueOf[V]): MatchValueObservable[Self, I, O, V, V] = handlePfValue[V] { case _: V => v } + inline def handleValue[V](inline v: V)(using inline valueOf: ValueOf[V]): MatchValueObservable[Self, I, O, V] = handlePfValue[V] { case _: V => v } } extension [Self[+_] <: Observable[_], I, O, T](inline matchTypeObserver: MatchTypeObservable[Self, I, O, T]) { @@ -88,12 +88,12 @@ object SplittableTypeMacros { } } - extension [Self[+_] <: Observable[_], I, O, V0, V1](inline matchValueObservable: MatchValueObservable[Self, I, O, V0, V1]) { - inline private def deglate[O1 >: O](inline handleFn: (V1, Signal[V1]) => O1) = ${ + extension [Self[+_] <: Observable[_], I, O, V](inline matchValueObservable: MatchValueObservable[Self, I, O, V]) { + inline private def deglate[O1 >: O](inline handleFn: (V, Signal[V]) => O1) = ${ handleValueApplyImpl('{ matchValueObservable }, '{ handleFn }) } - inline def apply[O1 >: O](inline handleFn: Signal[V1] => O1): MatchSplitObservable[Self, I, O1] = deglate { (_, vSignal) => handleFn(vSignal) } + inline def apply[O1 >: O](inline handleFn: Signal[V] => O1): MatchSplitObservable[Self, I, O1] = deglate { (_, vSignal) => handleFn(vSignal) } } extension [I, O](inline matchSplitObservable: MatchSplitObservable[Signal, I, O]) { @@ -206,7 +206,7 @@ object SplittableTypeMacros { casePfExpr: Expr[PartialFunction[V, V]] )( using quotes: Quotes - ): Expr[MatchValueObservable[Self, I, O, V, V]] = { + ): Expr[MatchValueObservable[Self, I, O, V]] = { import quotes.reflect.* matchSplitObservableExpr match { @@ -218,7 +218,7 @@ object SplittableTypeMacros { ) } => '{ - MatchValueObservable.build[Self, I, O, V, V]( + MatchValueObservable.build[Self, I, O, V]( $observableExpr, $caseListExpr, $handlerMapExpr, @@ -232,9 +232,9 @@ object SplittableTypeMacros { } } - private def handleValueApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, V0: Type, V1: Type]( - matchValueObservableExpr: Expr[MatchValueObservable[Self, I, O, V0, V1]], - handleFnExpr: Expr[Function2[V1, Signal[V1], O1]] + private def handleValueApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, V: Type]( + matchValueObservableExpr: Expr[MatchValueObservable[Self, I, O, V]], + handleFnExpr: Expr[Function2[V, Signal[V], O1]] )( using quotes: Quotes ): Expr[MatchSplitObservable[Self, I, O1]] = { @@ -242,14 +242,14 @@ object SplittableTypeMacros { matchValueObservableExpr match { case '{ - MatchValueObservable.build[Self, I, O, V0, V1]( + MatchValueObservable.build[Self, I, O, V]( $observableExpr, $caseListExpr, $handlerMapExpr, $tCaseExpr ) } => - innerHandleCaseImpl[Self, I, O, O1, V0, V1]( + innerHandleCaseImpl[Self, I, O, O1, V, V]( observableExpr, caseListExpr, handlerMapExpr, From 786b181dec33c321d3547dd7eb778b1da6a8ff95 Mon Sep 17 00:00:00 2001 From: HollandDM Date: Thu, 3 Oct 2024 18:42:42 +0700 Subject: [PATCH 12/13] ref: rename splitMatch to splitMatchOne --- ...Macros.scala => SplitMatchOneMacros.scala} | 66 +++++++++---------- ...le.scala => SplitMatchOneObservable.scala} | 6 +- ...cala => SplitMatchOneTypeObservable.scala} | 4 +- ...ala => SplitMatchOneValueObservable.scala} | 6 +- ...TypeSpec.scala => SplitMatchOneSpec.scala} | 12 ++-- 5 files changed, 47 insertions(+), 47 deletions(-) rename src/main/scala-3/com/raquo/airstream/split/{SplittableTypeMacros.scala => SplitMatchOneMacros.scala} (84%) rename src/main/scala-3/com/raquo/airstream/split/{MatchSplitObservable.scala => SplitMatchOneObservable.scala} (75%) rename src/main/scala-3/com/raquo/airstream/split/{MatchTypeObservable.scala => SplitMatchOneTypeObservable.scala} (85%) rename src/main/scala-3/com/raquo/airstream/split/{MatchValueObservable.scala => SplitMatchOneValueObservable.scala} (82%) rename src/test/scala-3/com/raquo/airstream/split/{SplittableTypeSpec.scala => SplitMatchOneSpec.scala} (98%) diff --git a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneMacros.scala similarity index 84% rename from src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala rename to src/main/scala-3/com/raquo/airstream/split/SplitMatchOneMacros.scala index 6b5fd05c..8fbbd263 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplittableTypeMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneMacros.scala @@ -10,7 +10,7 @@ import scala.quoted.{Expr, Quotes, Type} import scala.annotation.{unused, targetName} import scala.compiletime.summonInline -/** `SplittableTypeMacros` turns this code +/** `SplitMatchOneMacros` turns this code * * ```scala * sealed trait Foo @@ -51,11 +51,11 @@ import scala.compiletime.summonInline * After macros expansion, compiler will warns above code "match may not be * exhaustive" and "unreachable case" as expected. */ -object SplittableTypeMacros { +object SplitMatchOneMacros { extension [Self[+_] <: Observable[_], I](inline observable: BaseObservable[Self, I]) { - inline def splitMatch: MatchSplitObservable[Self, I, Nothing] = - MatchSplitObservable.build( + inline def splitMatchOne: SplitMatchOneObservable[Self, I, Nothing] = + SplitMatchOneObservable.build( observable, Nil, Map.empty[Int, Function2[Any, Any, Nothing]] @@ -63,7 +63,7 @@ object SplittableTypeMacros { } extension [Self[+_] <: Observable[_], I, O]( - inline matchSplitObservable: MatchSplitObservable[Self, I, O] + inline matchSplitObservable: SplitMatchOneObservable[Self, I, O] ) { inline def handleCase[A, B, O1 >: O](inline casePf: PartialFunction[A, B])(inline handleFn: (B, Signal[B]) => O1) = ${ handleCaseImpl('{ matchSplitObservable }, '{ casePf }, '{ handleFn }) @@ -73,53 +73,53 @@ object SplittableTypeMacros { handleTypeImpl[Self, I, O, T]('{ matchSplitObservable }, '{ casePf }) } - inline def handleType[T]: MatchTypeObservable[Self, I, O, T] = handlePfType[T] { case t: T => t } + inline def handleType[T]: SplitMatchOneTypeObservable[Self, I, O, T] = handlePfType[T] { case t: T => t } inline private def handlePfValue[V](inline casePf: PartialFunction[Any, V]) = ${ handleValueImpl[Self, I, O, V]('{ matchSplitObservable }, '{ casePf }) } - inline def handleValue[V](inline v: V)(using inline valueOf: ValueOf[V]): MatchValueObservable[Self, I, O, V] = handlePfValue[V] { case _: V => v } + inline def handleValue[V](inline v: V)(using inline valueOf: ValueOf[V]): SplitMatchOneValueObservable[Self, I, O, V] = handlePfValue[V] { case _: V => v } } - extension [Self[+_] <: Observable[_], I, O, T](inline matchTypeObserver: MatchTypeObservable[Self, I, O, T]) { - inline def apply[O1 >: O](inline handleFn: (T, Signal[T]) => O1): MatchSplitObservable[Self, I, O1] = ${ + extension [Self[+_] <: Observable[_], I, O, T](inline matchTypeObserver: SplitMatchOneTypeObservable[Self, I, O, T]) { + inline def apply[O1 >: O](inline handleFn: (T, Signal[T]) => O1): SplitMatchOneObservable[Self, I, O1] = ${ handleTypeApplyImpl('{ matchTypeObserver }, '{ handleFn }) } } - extension [Self[+_] <: Observable[_], I, O, V](inline matchValueObservable: MatchValueObservable[Self, I, O, V]) { + extension [Self[+_] <: Observable[_], I, O, V](inline matchValueObservable: SplitMatchOneValueObservable[Self, I, O, V]) { inline private def deglate[O1 >: O](inline handleFn: (V, Signal[V]) => O1) = ${ handleValueApplyImpl('{ matchValueObservable }, '{ handleFn }) } - inline def apply[O1 >: O](inline handleFn: Signal[V] => O1): MatchSplitObservable[Self, I, O1] = deglate { (_, vSignal) => handleFn(vSignal) } + inline def apply[O1 >: O](inline handleFn: Signal[V] => O1): SplitMatchOneObservable[Self, I, O1] = deglate { (_, vSignal) => handleFn(vSignal) } } - extension [I, O](inline matchSplitObservable: MatchSplitObservable[Signal, I, O]) { + extension [I, O](inline matchSplitObservable: SplitMatchOneObservable[Signal, I, O]) { inline def toSignal: Signal[O] = ${ observableImpl('{ matchSplitObservable }) } } - extension [I, O](inline matchSplitObservable: MatchSplitObservable[EventStream, I, O]) { + extension [I, O](inline matchSplitObservable: SplitMatchOneObservable[EventStream, I, O]) { inline def toStream: EventStream[O] = ${ observableImpl('{ matchSplitObservable }) } } private def handleCaseImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, A: Type, B: Type]( - matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], + matchSplitObservableExpr: Expr[SplitMatchOneObservable[Self, I, O]], casePfExpr: Expr[PartialFunction[A, B]], handleFnExpr: Expr[Function2[B, Signal[B], O1]] )( using quotes: Quotes - ): Expr[MatchSplitObservable[Self, I, O1]] = { + ): Expr[SplitMatchOneObservable[Self, I, O1]] = { import quotes.reflect.* matchSplitObservableExpr match { case '{ - MatchSplitObservable.build[Self, I, O]( + SplitMatchOneObservable.build[Self, I, O]( $observableExpr, $caseListExpr, $handlerMapExpr @@ -140,16 +140,16 @@ object SplittableTypeMacros { } private def handleTypeImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, T: Type]( - matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], + matchSplitObservableExpr: Expr[SplitMatchOneObservable[Self, I, O]], casePfExpr: Expr[PartialFunction[T, T]] )( using quotes: Quotes - ): Expr[MatchTypeObservable[Self, I, O, T]] = { + ): Expr[SplitMatchOneTypeObservable[Self, I, O, T]] = { import quotes.reflect.* matchSplitObservableExpr match { case '{ - MatchSplitObservable.build[Self, I, O]( + SplitMatchOneObservable.build[Self, I, O]( $observableExpr, $caseListExpr, $handlerMapExpr @@ -171,11 +171,11 @@ object SplittableTypeMacros { } private def handleTypeApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, T: Type]( - matchSplitObservableExpr: Expr[MatchTypeObservable[Self, I, O, T]], + matchSplitObservableExpr: Expr[SplitMatchOneTypeObservable[Self, I, O, T]], handleFnExpr: Expr[Function2[T, Signal[T], O1]] )( using quotes: Quotes - ): Expr[MatchSplitObservable[Self, I, O1]] = { + ): Expr[SplitMatchOneObservable[Self, I, O1]] = { import quotes.reflect.* matchSplitObservableExpr match { @@ -202,23 +202,23 @@ object SplittableTypeMacros { } private def handleValueImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, V: Type]( - matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]], + matchSplitObservableExpr: Expr[SplitMatchOneObservable[Self, I, O]], casePfExpr: Expr[PartialFunction[V, V]] )( using quotes: Quotes - ): Expr[MatchValueObservable[Self, I, O, V]] = { + ): Expr[SplitMatchOneValueObservable[Self, I, O, V]] = { import quotes.reflect.* matchSplitObservableExpr match { case '{ - MatchSplitObservable.build[Self, I, O]( + SplitMatchOneObservable.build[Self, I, O]( $observableExpr, $caseListExpr, $handlerMapExpr ) } => '{ - MatchValueObservable.build[Self, I, O, V]( + SplitMatchOneValueObservable.build[Self, I, O, V]( $observableExpr, $caseListExpr, $handlerMapExpr, @@ -233,16 +233,16 @@ object SplittableTypeMacros { } private def handleValueApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type, O1 >: O: Type, V: Type]( - matchValueObservableExpr: Expr[MatchValueObservable[Self, I, O, V]], + matchValueObservableExpr: Expr[SplitMatchOneValueObservable[Self, I, O, V]], handleFnExpr: Expr[Function2[V, Signal[V], O1]] )( using quotes: Quotes - ): Expr[MatchSplitObservable[Self, I, O1]] = { + ): Expr[SplitMatchOneObservable[Self, I, O1]] = { import quotes.reflect.* matchValueObservableExpr match { case '{ - MatchValueObservable.build[Self, I, O, V]( + SplitMatchOneValueObservable.build[Self, I, O, V]( $observableExpr, $caseListExpr, $handlerMapExpr, @@ -271,7 +271,7 @@ object SplittableTypeMacros { handleFnExpr: Expr[Function2[B, Signal[B], O1]] )( using quotes: Quotes - ): Expr[MatchSplitObservable[Self, I, O1]] = { + ): Expr[SplitMatchOneObservable[Self, I, O1]] = { import quotes.reflect.* val caseExprList = exprOfListToListOfExpr(caseListExpr) @@ -282,7 +282,7 @@ object SplittableTypeMacros { val nextCaseListExpr = listOfExprToExprOfList(nextCaseExprList) '{ - MatchSplitObservable.build[Self, I, O1]( + SplitMatchOneObservable.build[Self, I, O1]( $observableExpr, $nextCaseListExpr, ($handlerMapExpr + ($handlerMapExpr.size -> $handleFnExpr @@ -341,19 +341,19 @@ object SplittableTypeMacros { } private def observableImpl[Self[+_] <: Observable[_]: Type, I: Type, O: Type]( - matchSplitObservableExpr: Expr[MatchSplitObservable[Self, I, O]] + matchSplitObservableExpr: Expr[SplitMatchOneObservable[Self, I, O]] )( using quotes: Quotes ): Expr[Self[O]] = { import quotes.reflect.* matchSplitObservableExpr match { - case '{ MatchSplitObservable.build[Self, I, O]($_, Nil, $_) } => + case '{ SplitMatchOneObservable.build[Self, I, O]($_, Nil, $_) } => report.errorAndAbort( "Macro expansion failed, need at least one handleCase" ) case '{ - MatchSplitObservable.build[Self, I, O]( + SplitMatchOneObservable.build[Self, I, O]( $observableExpr, $caseListExpr, $handlerMapExpr diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneObservable.scala similarity index 75% rename from src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala rename to src/main/scala-3/com/raquo/airstream/split/SplitMatchOneObservable.scala index 53153403..1d17e722 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchSplitObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneObservable.scala @@ -21,15 +21,15 @@ import scala.annotation.compileTimeOnly * ``` */ -final case class MatchSplitObservable[Self[+_] <: Observable[_] , I, O] private (private val underlying: Unit) extends AnyVal +final case class SplitMatchOneObservable[Self[+_] <: Observable[_] , I, O] private (private val underlying: Unit) extends AnyVal -object MatchSplitObservable { +object SplitMatchOneObservable { @compileTimeOnly("splitMatch without toSignal/toStream is illegal") def build[Self[+_] <: Observable[_] , I, O]( observable: BaseObservable[Self, I], caseList: List[PartialFunction[Any, Any]], handlerMap: Map[Int, Function2[Any, Any, O]] - ): MatchSplitObservable[Self, I, O] = throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") + ): SplitMatchOneObservable[Self, I, O] = throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") } diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneTypeObservable.scala similarity index 85% rename from src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala rename to src/main/scala-3/com/raquo/airstream/split/SplitMatchOneTypeObservable.scala index d8e50942..f71e774f 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchTypeObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneTypeObservable.scala @@ -34,7 +34,7 @@ import scala.annotation.compileTimeOnly * ``` */ -final case class MatchTypeObservable[Self[+_] <: Observable[_], I, O, T] private (private val underlying: Unit) extends AnyVal +final case class SplitMatchOneTypeObservable[Self[+_] <: Observable[_], I, O, T] private (private val underlying: Unit) extends AnyVal object MatchTypeObservable { @@ -44,7 +44,7 @@ object MatchTypeObservable { caseList: List[PartialFunction[Any, Any]], handlerMap: Map[Int, Function2[Any, Any, O]], tCast: PartialFunction[T, T] - ): MatchTypeObservable[Self, I, O, T] = + ): SplitMatchOneTypeObservable[Self, I, O, T] = throw new UnsupportedOperationException( "splitMatch without toSignal/toStream is illegal" ) diff --git a/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneValueObservable.scala similarity index 82% rename from src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala rename to src/main/scala-3/com/raquo/airstream/split/SplitMatchOneValueObservable.scala index cbb193fe..9553e57a 100644 --- a/src/main/scala-3/com/raquo/airstream/split/MatchValueObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneValueObservable.scala @@ -34,9 +34,9 @@ import scala.annotation.compileTimeOnly * ``` */ -final case class MatchValueObservable[Self[+_] <: Observable[_], I, O, V] private (private val underlying: Unit) extends AnyVal +final case class SplitMatchOneValueObservable[Self[+_] <: Observable[_], I, O, V] private (private val underlying: Unit) extends AnyVal -object MatchValueObservable { +object SplitMatchOneValueObservable { @compileTimeOnly("splitMatch without toSignal/toStream is illegal") def build[Self[+_] <: Observable[_], I, O, V]( @@ -44,7 +44,7 @@ object MatchValueObservable { caseList: List[PartialFunction[Any, Any]], handlerMap: Map[Int, Function2[Any, Any, O]], vCast: PartialFunction[V, V] - ): MatchValueObservable[Self, I, O, V] = + ): SplitMatchOneValueObservable[Self, I, O, V] = throw new UnsupportedOperationException( "splitMatch without toSignal/toStream is illegal" ) diff --git a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala b/src/test/scala-3/com/raquo/airstream/split/SplitMatchOneSpec.scala similarity index 98% rename from src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala rename to src/test/scala-3/com/raquo/airstream/split/SplitMatchOneSpec.scala index a20993b0..1e5b9543 100644 --- a/src/test/scala-3/com/raquo/airstream/split/SplittableTypeSpec.scala +++ b/src/test/scala-3/com/raquo/airstream/split/SplitMatchOneSpec.scala @@ -4,13 +4,13 @@ import com.raquo.airstream.UnitSpec import com.raquo.airstream.eventbus.EventBus import com.raquo.airstream.fixtures.{Effect, TestableOwner} import com.raquo.airstream.state.Var -import com.raquo.airstream.split.SplittableTypeMacros.* +import com.raquo.airstream.split.SplitMatchOneMacros.* import scala.collection.{immutable, mutable} import scala.scalajs.js import com.raquo.airstream.ShouldSyntax.shouldBeEmpty -class SplittableTypeSpec extends UnitSpec { +class SplitMatchOneSpec extends UnitSpec { sealed trait Foo @@ -30,7 +30,7 @@ class SplittableTypeSpec extends UnitSpec { val owner = new TestableOwner val signal = myVar.signal - .splitMatch + .splitMatchOne .handleCase { case Bar(Some(str)) => str case Bar(None) => "null" @@ -165,7 +165,7 @@ class SplittableTypeSpec extends UnitSpec { // This should warn "match may not be exhaustive" with mising cases, and some idea can also flag it val signal = myVar.signal - .splitMatch + .splitMatchOne .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => @@ -262,7 +262,7 @@ class SplittableTypeSpec extends UnitSpec { val owner = new TestableOwner val stream = myEventBus.events - .splitMatch + .splitMatchOne .handleCase { case Bar(Some(str)) => str case Bar(None) => "null" @@ -391,7 +391,7 @@ class SplittableTypeSpec extends UnitSpec { // This should warn "match may not be exhaustive" with mising cases, and some idea can also flag it // Compiler only flag the first warning in some case, so it's best to comment out first warning test for this to flag the warning val stream = myEventBus.events - .splitMatch + .splitMatchOne .handleCase { case Bar(None) => "null" } { (str, strSignal) => From 681752040e278ee7068de36ceca208967769501f Mon Sep 17 00:00:00 2001 From: HollandDM Date: Fri, 4 Oct 2024 17:38:22 +0700 Subject: [PATCH 13/13] feat: add splitMatchSeq --- .../airstream/split/MacrosUtilities.scala | 80 ++ .../airstream/split/SplitMatchOneMacros.scala | 96 +-- .../split/SplitMatchOneObservable.scala | 4 +- .../split/SplitMatchOneTypeObservable.scala | 6 +- .../split/SplitMatchOneValueObservable.scala | 4 +- .../airstream/split/SplitMatchSeqMacros.scala | 390 +++++++++ .../split/SplitMatchSeqObservable.scala | 21 + .../split/SplitMatchSeqTypeObservable.scala | 22 + .../split/SplitMatchSeqValueObservable.scala | 21 + .../airstream/split/SplitMatchSeqSpec.scala | 773 ++++++++++++++++++ 10 files changed, 1327 insertions(+), 90 deletions(-) create mode 100644 src/main/scala-3/com/raquo/airstream/split/MacrosUtilities.scala create mode 100644 src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqMacros.scala create mode 100644 src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqObservable.scala create mode 100644 src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqTypeObservable.scala create mode 100644 src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqValueObservable.scala create mode 100644 src/test/scala-3/com/raquo/airstream/split/SplitMatchSeqSpec.scala diff --git a/src/main/scala-3/com/raquo/airstream/split/MacrosUtilities.scala b/src/main/scala-3/com/raquo/airstream/split/MacrosUtilities.scala new file mode 100644 index 00000000..02f11cda --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/MacrosUtilities.scala @@ -0,0 +1,80 @@ +package com.raquo.airstream.split + +import scala.quoted.{Expr, Quotes, Type} + +private[split] object MacrosUtilities { + + def exprOfListToListOfExpr[T: Type]( + pfListExpr: Expr[List[T]] + )( + using quotes: Quotes + ): List[Expr[T]] = { + import quotes.reflect.* + + pfListExpr match { + case '{ $headExpr :: (${ tailExpr }: List[T]) } => + headExpr :: exprOfListToListOfExpr(tailExpr) + case '{ Nil } => Nil + case _ => + report.errorAndAbort( + "Macro expansion failed, please use `handleCase` instead" + ) + } + + } + + def listOfExprToExprOfList[T: Type]( + pfExprList: List[Expr[T]] + )( + using quotes: Quotes + ): Expr[List[T]] = { + import quotes.reflect.* + + pfExprList match + case head :: tail => '{ $head :: ${ listOfExprToExprOfList(tail) } } + case Nil => '{ Nil } + } + + def innerObservableImpl[I: Type]( + iExpr: Expr[I], + caseListExpr: Expr[List[PartialFunction[Any, Any]]] + )( + using quotes: Quotes + ): Expr[(Int, Any)] = { + import quotes.reflect.* + + val caseExprList = exprOfListToListOfExpr(caseListExpr) + + val allCaseDefLists = caseExprList.reverse.zipWithIndex + .flatMap { case (caseExpr, idx) => + caseExpr.asTerm match { + case Lambda(_, Match(_, caseDefList)) => { + caseDefList.map { caseDef => + val idxExpr = Expr.apply(idx) + val newRhsExpr = '{ + val res = ${ caseDef.rhs.asExprOf[Any] }; ($idxExpr, res) + } + CaseDef.copy(caseDef)( + caseDef.pattern, + caseDef.guard, + newRhsExpr.asTerm + ) + } + } + case _ => + report.errorAndAbort( + "Macro expansion failed, please use `handleCase` with annonymous partial function" + ) + } + } + .map(_.changeOwner(Symbol.spliceOwner)) + + Match(iExpr.asTerm, allCaseDefLists).asExprOf[(Int, Any)] + } + + object ShowType { + def nameOfExpr[CC[_]](using Type[CC], Quotes): Expr[String] = Expr(Type.show[CC]) + inline def nameOf[CC[_]] = ${ nameOfExpr[CC] } + } + +} diff --git a/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneMacros.scala index 8fbbd263..a1f70928 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneMacros.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneMacros.scala @@ -19,7 +19,8 @@ import scala.compiletime.summonInline * case Baz1, Baz2 * } * case object Tar extends Foo - * val splitter = fooSignal.splitMatch + * val splitter = fooSignal + * .splitMatchOne * .handleCase { case Bar(Some(str)) => str } { (str, strSignal) => * renderStrNode(str, strSignal) * } @@ -134,7 +135,7 @@ object SplitMatchOneMacros { ) case other => report.errorAndAbort( - "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + "Macro expansion failed, please use `splitMatchOne` instead of creating new SplitMatchOneObservable explicitly" ) } } @@ -156,7 +157,7 @@ object SplitMatchOneMacros { ) } => '{ - MatchTypeObservable.build[Self, I, O, T]( + SplitMatchOneTypeObservable.build[Self, I, O, T]( $observableExpr, $caseListExpr, $handlerMapExpr, @@ -165,7 +166,7 @@ object SplitMatchOneMacros { } case other => report.errorAndAbort( - "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + "Macro expansion failed, please use `splitMatchOne` instead of creating new SplitMatchOneObservable explicitly" ) } } @@ -180,7 +181,7 @@ object SplitMatchOneMacros { matchSplitObservableExpr match { case '{ - MatchTypeObservable.build[Self, I, O, T]( + SplitMatchOneTypeObservable.build[Self, I, O, T]( $observableExpr, $caseListExpr, $handlerMapExpr, @@ -196,7 +197,7 @@ object SplitMatchOneMacros { ) case other => report.errorAndAbort( - "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + "Macro expansion failed, please use `splitMatchOne` instead of creating new SplitMatchOneObservable explicitly" ) } } @@ -227,7 +228,7 @@ object SplitMatchOneMacros { } case other => report.errorAndAbort( - "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + "Macro expansion failed, please use `splitMatchOne` instead of creating new SplitMatchOneObservable explicitly" ) } } @@ -258,7 +259,7 @@ object SplitMatchOneMacros { ) case other => report.errorAndAbort( - "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + "Macro expansion failed, please use `splitMatchOne` instead of creating new SplitMatchOneObservable explicitly" ) } } @@ -274,12 +275,12 @@ object SplitMatchOneMacros { ): Expr[SplitMatchOneObservable[Self, I, O1]] = { import quotes.reflect.* - val caseExprList = exprOfListToListOfExpr(caseListExpr) + val caseExprList = MacrosUtilities.exprOfListToListOfExpr(caseListExpr) val nextCaseExprList = casePfExpr.asExprOf[PartialFunction[Any, Any]] :: caseExprList - val nextCaseListExpr = listOfExprToExprOfList(nextCaseExprList) + val nextCaseListExpr = MacrosUtilities.listOfExprToExprOfList(nextCaseExprList) '{ SplitMatchOneObservable.build[Self, I, O1]( @@ -291,37 +292,6 @@ object SplitMatchOneMacros { } } - private def exprOfListToListOfExpr( - pfListExpr: Expr[List[PartialFunction[Any, Any]]] - )( - using quotes: Quotes - ): List[Expr[PartialFunction[Any, Any]]] = { - import quotes.reflect.* - - pfListExpr match { - case '{ $headExpr :: (${ tailExpr }: List[PartialFunction[Any, Any]]) } => - headExpr :: exprOfListToListOfExpr(tailExpr) - case '{ Nil } => Nil - case _ => - report.errorAndAbort( - "Macro expansion failed, please use `handleCase` instead of modify MatchSplitObservable explicitly" - ) - } - - } - - private def listOfExprToExprOfList( - pfExprList: List[Expr[PartialFunction[Any, Any]]] - )( - using quotes: Quotes - ): Expr[List[PartialFunction[Any, Any]]] = { - import quotes.reflect.* - - pfExprList match - case head :: tail => '{ $head :: ${ listOfExprToExprOfList(tail) } } - case Nil => '{ Nil } - } - private inline def toSplittableOneObservable[Self[+_] <: Observable[_], O]( parentObservable: BaseObservable[Self, (Int, Any)], handlerMap: Map[Int, Function2[Any, Any, O]] @@ -362,56 +332,16 @@ object SplitMatchOneMacros { '{ toSplittableOneObservable( $observableExpr - .map(i => ${ innerObservableImpl('i, caseListExpr) }) + .map(i => ${ MacrosUtilities.innerObservableImpl('i, caseListExpr) }) .asInstanceOf[BaseObservable[Self, (Int, Any)]], $handlerMapExpr ) } case _ => report.errorAndAbort( - "Macro expansion failed, please use `splitMatch` instead of creating new MatchSplitObservable explicitly" + "Macro expansion failed, please use `splitMatchOne` instead of creating new SplitMatchOneObservable explicitly" ) } } - private def innerObservableImpl[I: Type]( - iExpr: Expr[I], - caseListExpr: Expr[List[PartialFunction[Any, Any]]] - )( - using quotes: Quotes - ): Expr[(Int, Any)] = { - import quotes.reflect.* - - val caseExprList = exprOfListToListOfExpr(caseListExpr) - - val allCaseDefLists = caseExprList.reverse.zipWithIndex - .flatMap { case (caseExpr, idx) => - caseExpr.asTerm match { - case Lambda(_, Match(_, caseDefList)) => { - caseDefList.map { caseDef => - val idxExpr = Expr.apply(idx) - val newRhsExpr = '{ - val res = ${ caseDef.rhs.asExprOf[Any] }; ($idxExpr, res) - } - CaseDef.copy(caseDef)( - caseDef.pattern, - caseDef.guard, - newRhsExpr.asTerm - ) - } - } - case _ => - report.errorAndAbort( - "Macro expansion failed, please use `handleCase` with annonymous partial function" - ) - } - } - .map(_.changeOwner(Symbol.spliceOwner)) - - // val matchExpr = Match(iExpr.asTerm, allCaseDefLists).asExprOf[(Int, Any)] - // report.info(matchExpr.show) - // matchExpr - Match(iExpr.asTerm, allCaseDefLists).asExprOf[(Int, Any)] - } - } diff --git a/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneObservable.scala index 1d17e722..8e7400f0 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneObservable.scala @@ -25,11 +25,11 @@ final case class SplitMatchOneObservable[Self[+_] <: Observable[_] , I, O] priva object SplitMatchOneObservable { - @compileTimeOnly("splitMatch without toSignal/toStream is illegal") + @compileTimeOnly("`splitMatchOne` without `toSignal`/`toStream` is illegal") def build[Self[+_] <: Observable[_] , I, O]( observable: BaseObservable[Self, I], caseList: List[PartialFunction[Any, Any]], handlerMap: Map[Int, Function2[Any, Any, O]] - ): SplitMatchOneObservable[Self, I, O] = throw new UnsupportedOperationException("splitMatch without toSignal/toStream is illegal") + ): SplitMatchOneObservable[Self, I, O] = throw new UnsupportedOperationException("`splitMatchOne` without `toSignal`/`toStream` is illegal") } diff --git a/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneTypeObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneTypeObservable.scala index f71e774f..45adeabf 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneTypeObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneTypeObservable.scala @@ -36,9 +36,9 @@ import scala.annotation.compileTimeOnly final case class SplitMatchOneTypeObservable[Self[+_] <: Observable[_], I, O, T] private (private val underlying: Unit) extends AnyVal -object MatchTypeObservable { +object SplitMatchOneTypeObservable { - @compileTimeOnly("splitMatch without toSignal/toStream is illegal") + @compileTimeOnly("`splitMatchOne` without `toSignal`/`toStream` is illegal") def build[Self[+_] <: Observable[_], I, O, T]( observable: BaseObservable[Self, I], caseList: List[PartialFunction[Any, Any]], @@ -46,7 +46,7 @@ object MatchTypeObservable { tCast: PartialFunction[T, T] ): SplitMatchOneTypeObservable[Self, I, O, T] = throw new UnsupportedOperationException( - "splitMatch without toSignal/toStream is illegal" + "`splitMatchOne` without `toSignal`/`toStream` is illegal" ) } diff --git a/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneValueObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneValueObservable.scala index 9553e57a..280eae60 100644 --- a/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneValueObservable.scala +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchOneValueObservable.scala @@ -38,7 +38,7 @@ final case class SplitMatchOneValueObservable[Self[+_] <: Observable[_], I, O, V object SplitMatchOneValueObservable { - @compileTimeOnly("splitMatch without toSignal/toStream is illegal") + @compileTimeOnly("`splitMatchOne` without `toSignal`/`toStream` is illegal") def build[Self[+_] <: Observable[_], I, O, V]( observable: BaseObservable[Self, I], caseList: List[PartialFunction[Any, Any]], @@ -46,7 +46,7 @@ object SplitMatchOneValueObservable { vCast: PartialFunction[V, V] ): SplitMatchOneValueObservable[Self, I, O, V] = throw new UnsupportedOperationException( - "splitMatch without toSignal/toStream is illegal" + "`splitMatchOne` without `toSignal`/`toStream` is illegal" ) } diff --git a/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqMacros.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqMacros.scala new file mode 100644 index 00000000..982e7459 --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqMacros.scala @@ -0,0 +1,390 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.core.{ + EventStream, + Signal, + Observable, + BaseObservable +} +import scala.quoted.{Expr, Quotes, Type} +import scala.annotation.{unused, targetName} +import scala.compiletime.summonInline + +object SplitMatchSeqMacros { + + extension [Self[+_] <: Observable[_], I, K, CC[_]](inline observable: BaseObservable[Self, CC[I]]) { + inline def splitMatchSeq( + inline keyFn: Function1[I, K], + inline distinctCompose: Function1[Signal[I], Signal[I]] = (iSignal: Signal[I]) => iSignal.distinct, + inline duplicateKeysConfig: DuplicateKeysConfig = DuplicateKeysConfig.default, + ) = { + SplitMatchSeqObservable.build( + keyFn, + distinctCompose, + duplicateKeysConfig, + observable, + Nil, + Map.empty[Int, Function2[Any, Any, Nothing]] + ) + } + } + + extension [Self[+_] <: Observable[_], I, K, O, CC[_]]( + inline matchSplitObservable: SplitMatchSeqObservable[Self, I, K, O, CC] + ) { + inline def handleCase[A, B, O1 >: O](inline casePf: PartialFunction[A, B])(inline handleFn: (B, Signal[B]) => O1) = ${ + handleCaseImpl('{ matchSplitObservable }, '{ casePf }, '{ handleFn }) + } + + inline private def handlePfType[T](inline casePf: PartialFunction[Any, T]) = ${ + handleTypeImpl('{ matchSplitObservable }, '{ casePf }) + } + + inline def handleType[T]: SplitMatchSeqTypeObservable[Self, I, K, O, CC, T] = handlePfType[T] { case t: T => t } + + inline private def handlePfValue[V](inline casePf: PartialFunction[Any, V]) = ${ + handleValueImpl('{ matchSplitObservable }, '{ casePf }) + } + + inline def handleValue[V](inline v: V)(using inline valueOf: ValueOf[V]): SplitMatchSeqValueObservable[Self, I, K, O, CC, V] = handlePfValue[V] { case _: V => v } + + inline def toSignal: Signal[CC[O]] = ${ observableImpl('{ matchSplitObservable }) } + } + + extension [Self[+_] <: Observable[_], I, K, O, CC[_], T](inline matchTypeObserver: SplitMatchSeqTypeObservable[Self, I, K, O, CC, T]) { + inline def apply[O1 >: O](inline handleFn: (T, Signal[T]) => O1): SplitMatchSeqObservable[Self, I, K, O1, CC] = ${ + handleTypeApplyImpl('{ matchTypeObserver }, '{ handleFn }) + } + } + + extension [Self[+_] <: Observable[_], I, K, O, CC[_], V](inline matchValueObservable: SplitMatchSeqValueObservable[Self, I, K, O, CC, V]) { + inline private def deglate[O1 >: O](inline handleFn: (V, Signal[V]) => O1) = ${ + handleValueApplyImpl('{ matchValueObservable }, '{ handleFn }) + } + + inline def apply[O1 >: O](inline handleFn: Signal[V] => O1): SplitMatchSeqObservable[Self, I, K, O1, CC] = deglate { (_, vSignal) => handleFn(vSignal) } + } + + private def handleCaseImpl[Self[+_] <: Observable[_]: Type, I: Type, K: Type, O: Type, O1 >: O: Type, CC[_]: Type, A: Type, B: Type]( + matchSplitObservableExpr: Expr[SplitMatchSeqObservable[Self, I, K, O, CC]], + casePfExpr: Expr[PartialFunction[A, B]], + handleFnExpr: Expr[Function2[B, Signal[B], O1]] + )( + using quotes: Quotes + ): Expr[SplitMatchSeqObservable[Self, I, K, O1, CC]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ + SplitMatchSeqObservable.build[Self, I, K, O, CC]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $caseListExpr, + $handlerMapExpr + ) + } => + innerHandleCaseImpl( + keyFnExpr, + distinctComposeExpr, + duplicateKeysConfigExpr, + observableExpr, + caseListExpr, + handlerMapExpr, + casePfExpr, + handleFnExpr + ) + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatchSeq` instead of creating new SplitMatchSeqObservable explicitly" + ) + } + } + + private def handleTypeImpl[Self[+_] <: Observable[_]: Type, I: Type, K: Type, O: Type, CC[_]: Type, T: Type]( + matchSplitObservableExpr: Expr[SplitMatchSeqObservable[Self, I, K, O, CC]], + casePfExpr: Expr[PartialFunction[T, T]] + )( + using quotes: Quotes + ): Expr[SplitMatchSeqTypeObservable[Self, I, K, O, CC, T]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ + SplitMatchSeqObservable.build[Self, I, K, O, CC]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $caseListExpr, + $handlerMapExpr + ) + } => + '{ + SplitMatchSeqTypeObservable.build[Self, I, K, O, CC, T]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $caseListExpr, + $handlerMapExpr, + $casePfExpr + ) + } + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatchSeq` instead of creating new SplitMatchSeqObservable explicitly" + ) + } + } + + private def handleTypeApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, K: Type, O: Type, O1 >: O: Type, CC[_]: Type, T: Type]( + matchSplitObservableExpr: Expr[SplitMatchSeqTypeObservable[Self, I, K, O, CC, T]], + handleFnExpr: Expr[Function2[T, Signal[T], O1]] + )( + using quotes: Quotes + ): Expr[SplitMatchSeqObservable[Self, I, K, O1, CC]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ + SplitMatchSeqTypeObservable.build[Self, I, K, O, CC, T]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $caseListExpr, + $handlerMapExpr, + $tCaseExpr + ) + } => + innerHandleCaseImpl( + keyFnExpr, + distinctComposeExpr, + duplicateKeysConfigExpr, + observableExpr, + caseListExpr, + handlerMapExpr, + tCaseExpr, + handleFnExpr + ) + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatchSeq` instead of creating new SplitMatchSeqObservable explicitly" + ) + } + } + + private def handleValueImpl[Self[+_] <: Observable[_]: Type, I: Type, K: Type, O: Type, CC[_]: Type, V: Type]( + matchSplitObservableExpr: Expr[SplitMatchSeqObservable[Self, I, K, O, CC]], + casePfExpr: Expr[PartialFunction[V, V]] + )( + using quotes: Quotes + ): Expr[SplitMatchSeqValueObservable[Self, I, K, O, CC, V]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ + SplitMatchSeqObservable.build[Self, I, K, O, CC]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $caseListExpr, + $handlerMapExpr + ) + } => + '{ + SplitMatchSeqValueObservable.build[Self, I, K, O, CC, V]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $caseListExpr, + $handlerMapExpr, + $casePfExpr + ) + } + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatchSeq` instead of creating new SplitMatchSeqObservable explicitly" + ) + } + } + + private def handleValueApplyImpl[Self[+_] <: Observable[_]: Type, I: Type, K: Type, O: Type, O1 >: O: Type, CC[_]: Type, V: Type]( + matchValueObservableExpr: Expr[SplitMatchSeqValueObservable[Self, I, K, O, CC, V]], + handleFnExpr: Expr[Function2[V, Signal[V], O1]] + )( + using quotes: Quotes + ): Expr[SplitMatchSeqObservable[Self, I, K, O1, CC]] = { + import quotes.reflect.* + + matchValueObservableExpr match { + case '{ + SplitMatchSeqValueObservable.build[Self, I, K, O, CC, V]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $caseListExpr, + $handlerMapExpr, + $tCaseExpr + ) + } => + innerHandleCaseImpl( + keyFnExpr, + distinctComposeExpr, + duplicateKeysConfigExpr, + observableExpr, + caseListExpr, + handlerMapExpr, + tCaseExpr, + handleFnExpr + ) + case other => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatchSeq` instead of creating new SplitMatchSeqObservable explicitly" + ) + } + } + + private def innerHandleCaseImpl[Self[+_] <: Observable[_]: Type, I: Type, K: Type, O: Type, O1 >: O: Type, CC[_]: Type, A: Type, B: Type]( + keyFnExpr: Expr[Function1[I, K]], + distinctComposeExpr: Expr[Function1[Signal[I], Signal[I]]], + duplicateKeysConfigExpr: Expr[DuplicateKeysConfig], + observableExpr: Expr[BaseObservable[Self, CC[I]]], + caseListExpr: Expr[List[PartialFunction[Any, Any]]], + handlerMapExpr: Expr[Map[Int, Function2[Any, Any, O]]], + casePfExpr: Expr[PartialFunction[A, B]], + handleFnExpr: Expr[Function2[B, Signal[B], O1]] + )( + using quotes: Quotes + ): Expr[SplitMatchSeqObservable[Self, I, K, O1, CC]] = { + import quotes.reflect.* + + val caseExprList = MacrosUtilities.exprOfListToListOfExpr(caseListExpr) + + val nextCaseExprList = + casePfExpr.asExprOf[PartialFunction[Any, Any]] :: caseExprList + + val nextCaseListExpr = MacrosUtilities.listOfExprToExprOfList(nextCaseExprList) + + '{ + SplitMatchSeqObservable.build[Self, I, K, O1, CC]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $nextCaseListExpr, + ($handlerMapExpr + ($handlerMapExpr.size -> $handleFnExpr + .asInstanceOf[Function2[Any, Any, O1]])) + ) + } + } + + private inline def customDistinctCompose[I]( + distinctCompose: Signal[I] => Signal[I] + )( + dataSignal: Signal[(I, Int, Any)] + ): Signal[(I, Int, Any)] = { + val iSignal = dataSignal.map(_._1) + val otherSignal = dataSignal.map(data => data._2 -> data._3) + distinctCompose(iSignal).combineWith(otherSignal.distinct) + } + + private inline def customKey[I, K]( + keyFn: I => K + )( + input: (I, Int, Any) + ): (Int, K) = { + val (i, idx, _) = input + idx -> keyFn(i) + } + + private inline def toSplittableSeqObservable[Self[+_] <: Observable[_], I, K, O, CC[_]]( + parentObservable: BaseObservable[Self, CC[(I, Int, Any)]], + keyFn: I => K, + distinctCompose: Signal[I] => Signal[I], + duplicateKeysConfig: DuplicateKeysConfig, + handlerMap: Map[Int, Function2[Any, Any, O]], + splittable: Splittable[CC] + ): Signal[CC[O]] = { + parentObservable + .matchStreamOrSignal( + ifStream = _.split( + key = customKey(keyFn), + distinctCompose = customDistinctCompose(distinctCompose), + duplicateKeys = duplicateKeysConfig + ) { case ((idx, _), (_, _, b), dataSignal) => + val bSignal = dataSignal.map(_._3) + handlerMap.apply(idx).apply(b, bSignal) + }(splittable), + ifSignal = _.split( + key = customKey(keyFn), + distinctCompose = customDistinctCompose(distinctCompose), + duplicateKeys = duplicateKeysConfig + ) { case ((idx, _), (_, _, b), dataSignal) => + val bSignal = dataSignal.map(_._3) + handlerMap.apply(idx).apply(b, bSignal) + }(splittable) + ) + } + + private def observableImpl[Self[+_] <: Observable[_]: Type, I: Type, K: Type, O: Type, CC[_]: Type]( + matchSplitObservableExpr: Expr[SplitMatchSeqObservable[Self, I, K, O, CC]] + )( + using quotes: Quotes + ): Expr[Signal[CC[O]]] = { + import quotes.reflect.* + + matchSplitObservableExpr match { + case '{ SplitMatchSeqObservable.build[Self, I, K, O, CC]($_, $_, $_, $_, Nil, $_) } => + report.errorAndAbort( + "Macro expansion failed, need at least one handleCase" + ) + case '{ + SplitMatchSeqObservable.build[Self, I, K, O, CC]( + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $observableExpr, + $caseListExpr, + $handlerMapExpr + ) + } => + Expr.summon[Splittable[CC]] match { + case None => report.errorAndAbort( + "Macro expansion failed, cannot find Splittable instance of " + MacrosUtilities.ShowType.nameOf[CC] + ) + case Some(splittableExpr) => + '{ + toSplittableSeqObservable( + $observableExpr + .map { icc => + $splittableExpr.map( + icc, + i => { + val (idx, b) = ${ MacrosUtilities.innerObservableImpl('i, caseListExpr) } + (i, idx, b) + } + ) + } + .asInstanceOf[BaseObservable[Self, CC[(I, Int, Any)]]], + $keyFnExpr, + $distinctComposeExpr, + $duplicateKeysConfigExpr, + $handlerMapExpr, + $splittableExpr + ) + } + } + case _ => + report.errorAndAbort( + "Macro expansion failed, please use `splitMatchSeq` instead of creating new SplitMatchSeqObservable explicitly" + ) + } + } + +} diff --git a/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqObservable.scala new file mode 100644 index 00000000..7c6f78c9 --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqObservable.scala @@ -0,0 +1,21 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.core.{Observable, BaseObservable} +import scala.annotation.compileTimeOnly +import com.raquo.airstream.core.Signal + +final case class SplitMatchSeqObservable[Self[+_] <: Observable[_] , I, K, O, CC[_]] private (private val underlying: Unit) extends AnyVal + +object SplitMatchSeqObservable { + + @compileTimeOnly("`splitMatchSeq` without `toSignal` is illegal") + def build[Self[+_] <: Observable[_] , I, K, O, CC[_]]( + keyFn: Function1[I, K], + distinctCompose: Function1[Signal[I], Signal[I]], + duplicateKeysConfig: DuplicateKeysConfig, + observable: BaseObservable[Self, CC[I]], + caseList: List[PartialFunction[Any, Any]], + handlerMap: Map[Int, Function2[Any, Any, O]] + ): SplitMatchSeqObservable[Self, I, K, O, CC] = throw new UnsupportedOperationException("`splitMatchSeq` without `toSignal` is illegal") + +} diff --git a/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqTypeObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqTypeObservable.scala new file mode 100644 index 00000000..558eaf16 --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqTypeObservable.scala @@ -0,0 +1,22 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.core.{Observable, BaseObservable} +import scala.annotation.compileTimeOnly +import com.raquo.airstream.core.Signal + +final case class SplitMatchSeqTypeObservable[Self[+_] <: Observable[_] , I, K, O, CC[_], T] private (private val underlying: Unit) extends AnyVal + +object SplitMatchSeqTypeObservable { + + @compileTimeOnly("`splitMatchSeq` without `toSignal` is illegal") + def build[Self[+_] <: Observable[_] , I, K, O, CC[_], T]( + keyFn: Function1[I, K], + distinctCompose: Function1[Signal[I], Signal[I]], + duplicateKeysConfig: DuplicateKeysConfig, + observable: BaseObservable[Self, CC[I]], + caseList: List[PartialFunction[Any, Any]], + handlerMap: Map[Int, Function2[Any, Any, O]], + tCast: PartialFunction[T, T] + ): SplitMatchSeqTypeObservable[Self, I, K, O, CC, T] = throw new UnsupportedOperationException("`splitMatchSeq` without `toSignal` is illegal") + +} diff --git a/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqValueObservable.scala b/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqValueObservable.scala new file mode 100644 index 00000000..68709f1f --- /dev/null +++ b/src/main/scala-3/com/raquo/airstream/split/SplitMatchSeqValueObservable.scala @@ -0,0 +1,21 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.core.{Observable, BaseObservable, Signal} +import scala.annotation.compileTimeOnly + +final case class SplitMatchSeqValueObservable[Self[+_] <: Observable[_] , I, K, O, CC[_], V] private (private val underlying: Unit) extends AnyVal + +object SplitMatchSeqValueObservable { + + @compileTimeOnly("`splitMatchSeq` without `toSignal` is illegal") + def build[Self[+_] <: Observable[_] , I, K, O, CC[_], V]( + keyFn: Function1[I, K], + distinctCompose: Function1[Signal[I], Signal[I]], + duplicateKeysConfig: DuplicateKeysConfig, + observable: BaseObservable[Self, CC[I]], + caseList: List[PartialFunction[Any, Any]], + handlerMap: Map[Int, Function2[Any, Any, O]], + tCast: PartialFunction[V, V] + ): SplitMatchSeqValueObservable[Self, I, K, O, CC, V] = throw new UnsupportedOperationException("`splitMatchSeq` without `toSignal` is illegal") + +} diff --git a/src/test/scala-3/com/raquo/airstream/split/SplitMatchSeqSpec.scala b/src/test/scala-3/com/raquo/airstream/split/SplitMatchSeqSpec.scala new file mode 100644 index 00000000..d17c2a6c --- /dev/null +++ b/src/test/scala-3/com/raquo/airstream/split/SplitMatchSeqSpec.scala @@ -0,0 +1,773 @@ +package com.raquo.airstream.split + +import com.raquo.airstream.UnitSpec +import com.raquo.airstream.core.{Observer, Signal, Transaction} +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.fixtures.{Effect, TestableOwner} +import com.raquo.airstream.ownership.{DynamicOwner, DynamicSubscription, ManualOwner, Subscription} +import com.raquo.airstream.split.DuplicateKeysConfig +import com.raquo.airstream.split.SplitMatchSeqMacros.* +import com.raquo.airstream.state.Var +import com.raquo.ew.JsArray +import org.scalatest.{Assertion, BeforeAndAfter} + +import scala.collection.{immutable, mutable} +import scala.scalajs.js +import com.raquo.airstream.state.Var.update + +class SplitMatchSeqSpec extends UnitSpec with BeforeAndAfter { + + sealed trait Foo { + def id: String + } + + case class FooC(id: String, version: Int) extends Foo + + object FooO extends Foo { + override val id: String = "object" + } + + enum FooE(val numOpt: Option[Int]) extends Foo { + override def id: String = numOpt.map(num => s"int_$num").getOrElse("int_-1") + + case FooE1 extends FooE(Some(0)) + case FooE2 extends FooE(Some(1)) + case FooE3 extends FooE(None) + } + + object FooE { + def unapply(fooE: FooE): Some[Option[Int]] = Some(fooE.numOpt) + } + + case class Bar(id: String) + + case class Element(id: String, fooSignal: Signal[Foo]) { + override def toString: String = s"Element($id, fooSignal)" + } + + private val originalDuplicateKeysConfig = DuplicateKeysConfig.default + + after { + DuplicateKeysConfig.setDefault(originalDuplicateKeysConfig) + } + + def withOrWithoutDuplicateKeyWarnings(code: => Assertion): Assertion = { + // This wrapper checks that behaviour is identical in both modes + DuplicateKeysConfig.setDefault(DuplicateKeysConfig.noWarnings) + withClue("DuplicateKeysConfig.shouldWarn=false")(code) + DuplicateKeysConfig.setDefault(DuplicateKeysConfig.warnings) + withClue("DuplicateKeysConfig.shouldWarn=true")(code) + } + + it("splits stream into signals") { + withOrWithoutDuplicateKeyWarnings { + val effects = mutable.Buffer[Effect[String]]() + + val bus = new EventBus[List[Foo]] + + val owner = new TestableOwner + + val stream = bus.stream + .splitMatchSeq(_.id) + .handleCase { + case FooE(Some(num)) => num + case FooE(None) => -1 + } { (initialNum, numSignal) => + effects += Effect("init-child", s"FooE($initialNum)") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + numSignal.foreach { num => + assert(initialNum == num, "Subsequent value does not match initial key") + effects += Effect("update-child", s"FooE($num)") + }(owner) + Bar(s"$initialNum") + } + .handleType[FooC] { (initialFooC, fooCSignal) => + effects += Effect("init-child", s"FooC(${initialFooC.id}-${initialFooC.version})") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + fooCSignal.foreach { fooC => + assert(initialFooC.id == fooC.id, "Subsequent value does not match initial key") + effects += Effect("update-child", s"FooC(${fooC.id}-${fooC.version})") + }(owner) + Bar(initialFooC.id) + } + .handleValue(FooO) { fooOSignal => + effects += Effect("init-child", s"FooO(${FooO.id})") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + fooOSignal.foreach { fooO => + assert(FooO.id == fooO.id, "Subsequent value does not match initial key") + effects += Effect("update-child", s"FooO(${fooO.id})") + }(owner) + Bar(FooO.id) + } + .toSignal + + stream.foreach { result => + effects += Effect("result", result.toString) + }(owner) + + effects shouldBe mutable.Buffer( + Effect("result", "List()") + ) + + effects.clear() + + // -- + + bus.writer.onNext(FooE.FooE1 :: FooC("a", 1) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child", "FooE(0)"), + Effect("update-child", "FooE(0)"), + Effect("init-child", "FooC(a-1)"), + Effect("update-child", "FooC(a-1)"), + Effect("result", "List(Bar(0), Bar(a))") + ) + + effects.clear() + + // -- + + bus.writer.onNext(FooC("a", 2) :: FooE.FooE3 :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child", "FooE(-1)"), + Effect("update-child", "FooE(-1)"), + Effect("result", "List(Bar(a), Bar(-1))"), + Effect("update-child", "FooC(a-2)") + ) + + effects.clear() + + // -- + + bus.writer.onNext(FooE.FooE2 :: FooC("a", 3) :: FooC("b", 1) :: FooO :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child", "FooE(1)"), + Effect("update-child", "FooE(1)"), + Effect("init-child", "FooC(b-1)"), + Effect("update-child", "FooC(b-1)"), + Effect("init-child", "FooO(object)"), + Effect("update-child", "FooO(object)"), + Effect("result", "List(Bar(1), Bar(a), Bar(b), Bar(object))"), + Effect("update-child", "FooC(a-3)") + ) + + effects.clear() + + // -- + + bus.writer.onNext(FooC("b", 1) :: FooO :: FooC("a", 3) :: FooE.FooE2 :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b), Bar(object), Bar(a), Bar(1))") + ) + + effects.clear() + + // -- + + bus.writer.onNext(FooE.FooE2 :: FooC("b", 2) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(1), Bar(b))"), + Effect("update-child", "FooC(b-2)") + ) + + effects.clear() + + // -- + + bus.writer.onNext(FooC("b", 2) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b))") // output is a stream, not signal + ) + } + } + + it("split signal into signals") { + withOrWithoutDuplicateKeyWarnings { + val effects = mutable.Buffer[Effect[String]]() + + val myVar = Var[List[Foo]](FooC("initial", 1) :: FooE.FooE1 :: FooO :: Nil) + + val owner = new TestableOwner + + val signal = myVar.signal.splitMatchSeq(_.id) + .handleCase { + case FooE(Some(num)) => num + case FooE(None) => -1 + } { (initialNum, numSignal) => + effects += Effect("init-child", s"FooE($initialNum)") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + numSignal.foreach { num => + assert(initialNum == num, "Subsequent value does not match initial key") + effects += Effect("update-child", s"FooE($num)") + }(owner) + Bar(s"$initialNum") + } + .handleType[FooC] { (initialFooC, fooCSignal) => + effects += Effect("init-child", s"FooC(${initialFooC.id}-${initialFooC.version})") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + fooCSignal.foreach { fooC => + assert(initialFooC.id == fooC.id, "Subsequent value does not match initial key") + effects += Effect("update-child", s"FooC(${fooC.id}-${fooC.version})") + }(owner) + Bar(initialFooC.id) + } + .handleValue(FooO) { fooOSignal => + effects += Effect("init-child", s"FooO(${FooO.id})") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + fooOSignal.foreach { fooO => + assert(FooO.id == fooO.id, "Subsequent value does not match initial key") + effects += Effect("update-child", s"FooO(${fooO.id})") + }(owner) + Bar(FooO.id) + } + .toSignal + + signal.foreach { result => + effects += Effect("result", result.toString) + }(owner) + + effects shouldBe mutable.Buffer( + Effect("init-child", "FooC(initial-1)"), + Effect("update-child", "FooC(initial-1)"), + Effect("init-child", "FooE(0)"), + Effect("update-child", "FooE(0)"), + Effect("init-child", "FooO(object)"), + Effect("update-child", "FooO(object)"), + Effect("result", "List(Bar(initial), Bar(0), Bar(object))") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooO :: FooC("a", 1) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child", "FooC(a-1)"), + Effect("update-child", "FooC(a-1)"), + Effect("result", "List(Bar(object), Bar(a))") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("a", 2) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(a))"), + Effect("update-child", "FooC(a-2)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("a", 3) :: FooE.FooE2 :: FooC("b", 1) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child", "FooE(1)"), + Effect("update-child", "FooE(1)"), + Effect("init-child", "FooC(b-1)"), + Effect("update-child", "FooC(b-1)"), + Effect("result", "List(Bar(a), Bar(1), Bar(b))"), + Effect("update-child", "FooC(a-3)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("b", 1) :: FooC("a", 3) :: FooE.FooE2 :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b), Bar(a), Bar(1))") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooE.FooE3 :: FooC("b", 2) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child", "FooE(-1)"), + Effect("update-child", "FooE(-1)"), + Effect("result", "List(Bar(-1), Bar(b))"), + Effect("update-child", "FooC(b-2)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("b", 2) :: FooE.FooE3 :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b), Bar(-1))") + ) + + //effects.clear() + } + } + + it("split signal - raw semantics") { + withOrWithoutDuplicateKeyWarnings { + val effects = mutable.Buffer[Effect[String]]() + + val myVar = Var[List[Foo]](FooC("initial", 1) :: FooE.FooE1 :: FooO :: Nil) + + val owner = new TestableOwner + + // #Note: `identity` here means we're not using `distinct` to filter out redundancies in fooSignal + // We test like this to make sure that the underlying splitting machinery works correctly without this crutch + val signal = myVar.signal.splitMatchSeq(_.id, distinctCompose = identity) + .handleCase { + case FooE(Some(num)) => num + case FooE(None) => -1 + } { (initialNum, numSignal) => + effects += Effect(s"init-child-$initialNum", s"FooE($initialNum)") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + numSignal.foreach { num => + assert(initialNum == num, "Subsequent value does not match initial key") + effects += Effect(s"update-child-$num", s"FooE($num)") + }(owner) + Bar(s"$initialNum") + } + .handleType[FooC] { (initialFooC, fooCSignal) => + effects += Effect(s"init-child-${initialFooC.id}", s"FooC(${initialFooC.id}-${initialFooC.version})") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + fooCSignal.foreach { fooC => + assert(initialFooC.id == fooC.id, "Subsequent value does not match initial key") + effects += Effect(s"update-child-${initialFooC.id}", s"FooC(${fooC.id}-${fooC.version})") + }(owner) + Bar(initialFooC.id) + } + .handleValue(FooO) { fooOSignal => + effects += Effect(s"init-child-${FooO.id}", s"FooO(${FooO.id})") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + fooOSignal.foreach { fooO => + assert(FooO.id == fooO.id, "Subsequent value does not match initial key") + effects += Effect(s"update-child-${fooO.id}", s"FooO(${fooO.id})") + }(owner) + Bar(FooO.id) + } + .toSignal + + signal.foreach { result => + effects += Effect("result", result.toString) + }(owner) + + effects shouldBe mutable.Buffer( + Effect("init-child-initial", "FooC(initial-1)"), + Effect("update-child-initial", "FooC(initial-1)"), + Effect("init-child-0", "FooE(0)"), + Effect("update-child-0", "FooE(0)"), + Effect("init-child-object", "FooO(object)"), + Effect("update-child-object", "FooO(object)"), + Effect("result", "List(Bar(initial), Bar(0), Bar(object))") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooO :: FooC("a", 1) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child-a", "FooC(a-1)"), + Effect("update-child-a", "FooC(a-1)"), + Effect("result", "List(Bar(object), Bar(a))"), + Effect("update-child-object", "FooO(object)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("a", 2) :: FooO :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(a), Bar(object))"), + Effect("update-child-object", "FooO(object)"), + Effect("update-child-a", "FooC(a-2)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooE.FooE2 :: FooC("a", 3) :: FooE.FooE3 :: FooC("b", 1) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child-1", "FooE(1)"), + Effect("update-child-1", "FooE(1)"), + Effect("init-child--1", "FooE(-1)"), + Effect("update-child--1", "FooE(-1)"), + Effect("init-child-b", "FooC(b-1)"), + Effect("update-child-b", "FooC(b-1)"), + Effect("result", "List(Bar(1), Bar(a), Bar(-1), Bar(b))"), + Effect("update-child-a", "FooC(a-3)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("b", 1) :: FooC("a", 3) :: FooE.FooE2 :: FooE.FooE3 :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b), Bar(a), Bar(1), Bar(-1))"), + Effect("update-child-a", "FooC(a-3)"), + Effect("update-child-1", "FooE(1)"), + Effect("update-child--1", "FooE(-1)"), + Effect("update-child-b", "FooC(b-1)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooE.FooE2 :: FooC("b", 1) :: FooC("a", 4) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(1), Bar(b), Bar(a))"), + Effect("update-child-a", "FooC(a-4)"), + Effect("update-child-1", "FooE(1)"), + Effect("update-child-b", "FooC(b-1)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("b", 2) :: FooC("a", 4) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b), Bar(a))"), + Effect("update-child-a", "FooC(a-4)"), + Effect("update-child-b", "FooC(b-2)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("b", 3) :: FooC("a", 5) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b), Bar(a))"), + Effect("update-child-a", "FooC(a-5)"), + Effect("update-child-b", "FooC(b-3)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("b", 4) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b))"), + Effect("update-child-b", "FooC(b-4)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("b", 4) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b))"), + Effect("update-child-b", "FooC(b-4)") + ) + } + } + + it("split signal - raw semantics and life cycle") { + withOrWithoutDuplicateKeyWarnings { + val effects = mutable.Buffer[Effect[String]]() + + val myVar = Var[List[Foo]](FooC("initial", 1) :: FooO :: FooE.FooE1 :: Nil) + + val outerDynamicOwner = new DynamicOwner(() => throw new Exception("split outer dynamic owner accessed after it was killed")) + + val innerDynamicOwnerE = new DynamicOwner(() => throw new Exception("split inner dynamic owner accessed after it was killed")) + + val innerDynamicOwnerC = new DynamicOwner(() => throw new Exception("split inner dynamic owner accessed after it was killed")) + + val innerDynamicOwnerO = new DynamicOwner(() => throw new Exception("split inner dynamic owner accessed after it was killed")) + + // #Note: important to activate now, we're testing this (see comments below) + outerDynamicOwner.activate() + innerDynamicOwnerE.activate() + innerDynamicOwnerC.activate() + innerDynamicOwnerO.activate() + + // #Note: `identity` here means we're not using `distinct` to filter out redundancies in fooSignal + // We test like this to make sure that the underlying splitting machinery works correctly without this crutch + val signal = myVar.signal.splitMatchSeq(_.id, distinctCompose = identity) + .handleCase { + case FooE(Some(num)) => num + case FooE(None) => -1 + } { (initialNum, numSignal) => + effects += Effect(s"init-child-$initialNum", s"FooE($initialNum)") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + Transaction { _ => + DynamicSubscription.subscribeCallback( + innerDynamicOwnerE, + owner => numSignal.foreach { num => + assert(initialNum == num, "Subsequent value does not match initial key") + effects += Effect(s"update-child-$num", s"FooE($num)") + }(owner) + ) + } + Bar(s"$initialNum") + } + .handleType[FooC] { (initialFooC, fooCSignal) => + effects += Effect(s"init-child-${initialFooC.id}", s"FooC(${initialFooC.id}-${initialFooC.version})") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + DynamicSubscription.subscribeCallback( + innerDynamicOwnerC, + owner => fooCSignal.foreach { fooC => + assert(initialFooC.id == fooC.id, "Subsequent value does not match initial key") + effects += Effect(s"update-child-${initialFooC.id}", s"FooC(${fooC.id}-${fooC.version})") + }(owner) + ) + Bar(initialFooC.id) + } + .handleValue(FooO) { fooOSignal => + effects += Effect(s"init-child-${FooO.id}", s"FooO(${FooO.id})") + // @Note keep foreach or addObserver here – this is important. + // It tests that SplitSignal does not cause an infinite loop trying to evaluate its initialValue. + DynamicSubscription.subscribeCallback( + innerDynamicOwnerO, + owner => fooOSignal.foreach { fooO => + assert(FooO.id == fooO.id, "Subsequent value does not match initial key") + effects += Effect(s"update-child-${fooO.id}", s"FooO(${fooO.id})") + }(owner) + ) + Bar(FooO.id) + } + .toSignal + + DynamicSubscription.subscribeCallback( + outerDynamicOwner, + owner => signal.foreach { result => + effects += Effect("result", result.toString) + }(owner) + ) + + effects shouldBe mutable.Buffer( + Effect("init-child-initial", "FooC(initial-1)"), + Effect("update-child-initial", "FooC(initial-1)"), + Effect("init-child-object", "FooO(object)"), + Effect("update-child-object", "FooO(object)"), + Effect("init-child-0", "FooE(0)"), + Effect("result", "List(Bar(initial), Bar(object), Bar(0))"), + Effect("update-child-0", "FooE(0)") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooE.FooE1 :: FooC("b", 1) :: FooO :: FooC("a", 3) :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child-b", "FooC(b-1)"), + Effect("update-child-b", "FooC(b-1)"), + Effect("init-child-a", "FooC(a-3)"), + Effect("update-child-a", "FooC(a-3)"), + Effect("result", "List(Bar(0), Bar(b), Bar(object), Bar(a))"), + Effect("update-child-object", "FooO(object)"), + Effect("update-child-0", "FooE(0)") + ) + + effects.clear() + + // -- + + outerDynamicOwner.deactivate() + innerDynamicOwnerO.deactivate() + innerDynamicOwnerE.deactivate() + innerDynamicOwnerC.deactivate() + + effects shouldBe mutable.Buffer() + + // -- + + outerDynamicOwner.activate() + innerDynamicOwnerC.activate() + + effects shouldBe mutable.Buffer( + // #Note `initial` is here because our code created an inner subscription for `initial` + // and kept it alive even after the element was removed. This inner signal itself will + // not receive any updates until "initial" key is added to the inputs again (actually + // it might cause issues in this pattern if this happens), but this inner signal's + // current value is still sent to the observer when we re-activate its dynamic owner. + Effect("result", "List(Bar(0), Bar(b), Bar(object), Bar(a))"), + Effect("update-child-initial", "FooC(initial-1)"), + Effect("update-child-b", "FooC(b-1)"), + Effect("update-child-a", "FooC(a-3)"), + ) + + effects.clear() + + // -- + + innerDynamicOwnerO.activate() + + effects shouldBe mutable.Buffer( + Effect("update-child-object", "FooO(object)") + ) + + effects.clear() + + // -- + + innerDynamicOwnerE.activate() + + effects shouldBe mutable.Buffer( + Effect("update-child-0", "FooE(0)") + ) + + // -- + + effects.clear() + + myVar.writer.onNext(FooC("b", 2) :: FooC("a", 3) :: FooO :: Nil) + + // This assertion makes sure that `resetOnStop` is set correctly in `drop(1, resetOnStop = false)` + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b), Bar(a), Bar(object))"), + Effect("update-child-b", "FooC(b-2)"), + Effect("update-child-a", "FooC(a-3)"), + Effect("update-child-object", "FooO(object)") + ) + + effects.clear() + + // -- + + outerDynamicOwner.deactivate() + + effects shouldBe mutable.Buffer() + + // -- + + outerDynamicOwner.activate() + + effects shouldBe mutable.Buffer( + Effect("result", "List(Bar(b), Bar(a), Bar(object))") + ) + + effects.clear() + + // -- + + myVar.writer.onNext(FooC("a", 4) :: FooC("b", 3) :: FooE.FooE2 :: Nil) + + effects shouldBe mutable.Buffer( + Effect("init-child-1", "FooE(1)"), + Effect("result", "List(Bar(a), Bar(b), Bar(1))"), + Effect("update-child-b", "FooC(b-3)"), + Effect("update-child-a", "FooC(a-4)"), + Effect("update-child-1", "FooE(1)") + ) + + //effects.clear() + } + } + + it("split signal - duplicate keys") { + + val myVar = Var[List[Foo]](FooC("initial", 1) :: Nil) + + val owner = new TestableOwner + + // #Note: `identity` here means we're not using `distinct` to filter out redundancies in fooSignal + // We test like this to make sure that the underlying splitting machinery works correctly without this crutch + val signal = myVar.signal.splitMatchSeq(_.id, distinctCompose = identity) + .handleCase { + case FooE(Some(num)) => num + case FooE(None) => -1 + } { (initialNum, _) => + Bar(s"$initialNum") + } + .handleType[FooC] { (initialFooC, _) => + Bar(initialFooC.id) + } + .handleValue(FooO) { _ => + Bar(FooO.id) + } + .toSignal + + // -- + + signal.addObserver(Observer.empty)(owner) + + // -- + + myVar.writer.onNext(FooC("object", 1) :: FooC("int_", 1) :: FooO :: FooE.FooE2 :: Nil) + + // -- + + DuplicateKeysConfig.setDefault(DuplicateKeysConfig.noWarnings) + + myVar.writer.onNext(FooC("object", 1) :: FooC("object", 1) :: FooC("int_", 1) :: FooO :: FooE.FooE2 :: FooO :: Nil) + + // This should warn, but not throw + + // #TODO[Test] we aren't actually testing that this is logging to the console. + // I'm not sure how to do this without over-complicating things. + // The console warning is printed into the test output, we can at least see it there if / when we look + + DuplicateKeysConfig.setDefault(DuplicateKeysConfig.warnings) + + myVar.writer.onNext(FooC("object", 1) :: FooC("int_", 1) :: FooO :: FooE.FooE2 :: Nil) + + myVar.writer.onNext(FooC("object", 1) :: FooC("object", 1) :: FooC("int_", 1) :: FooO :: FooE.FooE2 :: FooO :: Nil) + } + + it("split list / vector / set / js.array / immutable.seq / collection.seq / option compiles") { + // Having this test pass on all supported Scala versions is important to ensure that the implicits are actually usable. + { + (new EventBus[List[Foo]]).events.splitMatchSeq(_.id).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[Vector[Foo]]).events.splitMatchSeq(_.id).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[Set[Foo]]).events.splitMatchSeq(_.id).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[js.Array[Foo]]).events.splitMatchSeq(_.id).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[immutable.Seq[Foo]]).events.splitMatchSeq(_.id).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[collection.Seq[Foo]]).events.splitMatchSeq(_.id).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[collection.Seq[Foo]]).events.splitMatchSeq(_.id).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + } + // And now the same, but with `distinctCompose = identity`, because that somehow affects implicit resolution in Scala 3.0.0 + { + (new EventBus[List[Foo]]).events.splitMatchSeq(_.id, identity).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[Vector[Foo]]).events.splitMatchSeq(_.id, identity).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[Set[Foo]]).events.splitMatchSeq(_.id, identity).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[js.Array[Foo]]).events.splitMatchSeq(_.id, identity).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[immutable.Seq[Foo]]).events.splitMatchSeq(_.id, identity).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[collection.Seq[Foo]]).events.splitMatchSeq(_.id, identity).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + (new EventBus[collection.Seq[Foo]]).events.splitMatchSeq(_.id, identity).handleCase{ case e: FooE => e }((_, _) => 10).handleType[FooC]((_, _) => 20).handleValue(FooO)(_ => 30).toSignal + } + } + +}