diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e62285c..9dfda19 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,11 +94,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p util/target target .js/target core/native/target documentation/target core/js/target core/jvm/target .jvm/target .native/target io/js/target io/jvm/target bench/target project/target + run: mkdir -p util/target target reactify/jvm/target .js/target core/native/target documentation/target reactify/native/target reactify/js/target core/js/target core/jvm/target .jvm/target .native/target io/js/target io/jvm/target bench/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar util/target target .js/target core/native/target documentation/target core/js/target core/jvm/target .jvm/target .native/target io/js/target io/jvm/target bench/target project/target + run: tar cf targets.tar util/target target reactify/jvm/target .js/target core/native/target documentation/target reactify/native/target reactify/js/target core/js/target core/jvm/target .jvm/target .native/target io/js/target io/jvm/target bench/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') diff --git a/build.sbt b/build.sbt index e976d28..82897bc 100644 --- a/build.sbt +++ b/build.sbt @@ -38,6 +38,8 @@ ThisBuild / developers := List(tlGitHubDev("darkfrog26", "Matt Hicks")) // Dependency versions val collectionCompatVersion: String = "2.11.0" +val reactifyVersion: String = "4.1.0" + val scalaTestVersion: String = "3.2.16" val scalaCheckVersion: String = "3.2.14.0" @@ -52,7 +54,16 @@ val jsoniterJavaVersion: String = "0.9.23" val circeVersion: String = "0.14.2" val uPickleVersion: String = "2.0.0" -lazy val root = tlCrossRootProject.aggregate(core.js, core.jvm, core.native, io.js, io.jvm) +lazy val root = tlCrossRootProject.aggregate( + core.js, + core.jvm, + core.native, + io.js, + io.jvm, + reactify.js, + reactify.jvm, + reactify.native +) lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Full) @@ -104,6 +115,19 @@ lazy val io = crossProject(JSPlatform, JVMPlatform) ) .dependsOn(core) +lazy val reactify = crossProject(JSPlatform, JVMPlatform, NativePlatform) + .crossType(CrossType.Full) + .settings( + name := "fabric-reactify", + mimaPreviousArtifacts := Set.empty, + libraryDependencies ++= Seq( + "com.outr" %%% "reactify" % reactifyVersion, + "org.scalatest" %%% "scalatest" % scalaTestVersion % Test, + "org.scalatestplus" %% "scalacheck-1-16" % scalaCheckVersion % Test + ) + ) + .dependsOn(core) + lazy val bench = project .in(file("bench")) .enablePlugins(JmhPlugin) diff --git a/reactify/shared/src/main/scala/fabric/react/ReactParent.scala b/reactify/shared/src/main/scala/fabric/react/ReactParent.scala new file mode 100644 index 0000000..7560be2 --- /dev/null +++ b/reactify/shared/src/main/scala/fabric/react/ReactParent.scala @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package fabric.react + +import fabric.{obj, Json} +import fabric.rw._ +import reactify._ + +trait ReactParent { + private val _json: Var[Json] = Var(load().getOrElse(obj())) + private val list: Var[List[RWVar[_]]] = Var(Nil) + + def json: Val[Json] = _json + + protected def modify(f: Json => Json): Unit = synchronized { + val modified = f(json()) + _json @= modified + } + + protected def reactive[T: RW](name: String, default: => T): Var[T] = synchronized { + val rwv = RWVar[T](name, () => default) + list @= rwv :: list() + rwv.v + } + + protected def load(): Option[Json] + + case class RWVar[T](name: String, default: () => T)(implicit rw: RW[T]) { + val v: Var[T] = Var(load()) + + v.attachAndFire { value => + modify { json => + json.merge(obj(name -> value.asJson)) + } + } + + protected def load(): T = json().get(name) match { + case Some(j) => j.as[T] + case None => default() + } + } +} diff --git a/reactify/shared/src/test/scala/spec/ReactParentSpec.scala b/reactify/shared/src/test/scala/spec/ReactParentSpec.scala new file mode 100644 index 0000000..c2b70e7 --- /dev/null +++ b/reactify/shared/src/test/scala/spec/ReactParentSpec.scala @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package spec + +import fabric._ +import fabric.react.ReactParent +import reactify._ +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class ReactParentSpec extends AnyWordSpec with Matchers { + "ReactParent" should { + "properly handle initialization of Person" in { + Person.name() should be("John Doe") + Person.age() should be(21) + Person.json() should be( + obj( + "name" -> "John Doe", + "age" -> 21 + ) + ) + } + "change the name and age" in { + Person.name @= "Jane Doe" + Person.age @= 20 + Person.json() should be( + obj( + "name" -> "Jane Doe", + "age" -> 20 + ) + ) + } + } +} + +object Person extends ReactParent { + val name: Var[String] = reactive[String]("name", "") + val age: Var[Int] = reactive[Int]("age", 0) + + override protected def load(): Option[Json] = Some( + obj( + "name" -> "John Doe", + "age" -> 21 + ) + ) +}