Browse Source
Many changes
Many changes
Updated store code - wrapped fold function in Task Added JSON logging functionality to store middleware Initial attempt at creating filter combinator for fx observables Made ListStore code use Effects Made a router using the store pattern Misc updates to fx monix implicitsmaster
Rohan Sircar
3 years ago
15 changed files with 518 additions and 115 deletions
-
6build.sbt
-
9src/main/scala/nova/monadic_sfx/Main.scala
-
46src/main/scala/nova/monadic_sfx/MainApp.scala
-
36src/main/scala/nova/monadic_sfx/MainModule.scala
-
2src/main/scala/nova/monadic_sfx/Types.scala
-
103src/main/scala/nova/monadic_sfx/implicits/JavaFxMonixObservables.scala
-
3src/main/scala/nova/monadic_sfx/implicits/MenuItem.scala
-
16src/main/scala/nova/monadic_sfx/ui/MyFxApp.scala
-
107src/main/scala/nova/monadic_sfx/ui/components/router/FXRouter.scala
-
81src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListStore.scala
-
20src/main/scala/nova/monadic_sfx/ui/components/todo/TodoListView.scala
-
63src/main/scala/nova/monadic_sfx/util/Misc.scala
-
76src/main/scala/nova/monadic_sfx/util/reactive/Middlewares.scala
-
64src/main/scala/nova/monadic_sfx/util/reactive/Store.scala
-
1src/main/scala/nova/monadic_sfx/util/reactive/package.scala
@ -1,7 +1,41 @@ |
|||||
package nova.monadic_sfx |
package nova.monadic_sfx |
||||
|
|
||||
|
import scala.concurrent.duration._ |
||||
|
|
||||
|
import _root_.monix.bio.Task |
||||
|
import cats.implicits._ |
||||
|
import io.odin._ |
||||
|
import io.odin.config._ |
||||
|
import io.odin.syntax._ |
||||
import nova.monadic_sfx.actors.ActorModule |
import nova.monadic_sfx.actors.ActorModule |
||||
import nova.monadic_sfx.http.HttpModule |
import nova.monadic_sfx.http.HttpModule |
||||
import nova.monadic_sfx.ui.UiModule |
import nova.monadic_sfx.ui.UiModule |
||||
|
import nova.monadic_sfx.util.reactive.Middlewares |
||||
|
|
||||
|
trait MainModule extends ActorModule with UiModule with HttpModule { |
||||
|
def routerLogger(defaultLogger: Logger[Task], storeLogger: Logger[Task]) = |
||||
|
enclosureRouting[Task]( |
||||
|
"nova.monadic_sfx.util.reactive.Middlewares" -> storeLogger, |
||||
|
"nova.monadic_sfx.util.reactive.Store" -> storeLogger |
||||
|
) |
||||
|
.withFallback(defaultLogger) |
||||
|
.withAsync() |
||||
|
|
||||
trait MainModule extends ActorModule with UiModule with HttpModule |
|
||||
|
def makeLogger = |
||||
|
for { |
||||
|
defaultLogger <- consoleLogger[Task]() |
||||
|
.withAsync(timeWindow = 1.millis) |+| fileLogger[Task]( |
||||
|
"application.log" |
||||
|
).withAsync() |
||||
|
middlewareLogger <- |
||||
|
consoleLogger[ |
||||
|
Task |
||||
|
](formatter = Middlewares.format) |
||||
|
.withMinimalLevel(Level.Trace) |
||||
|
.withAsync() |+| fileLogger[Task]( |
||||
|
"stores.log", |
||||
|
formatter = Middlewares.format |
||||
|
).withAsync() |
||||
|
routerLogger <- routerLogger(defaultLogger, middlewareLogger) |
||||
|
} yield (routerLogger) |
||||
|
} |
@ -0,0 +1,107 @@ |
|||||
|
package nova.monadic_sfx.ui.components.router |
||||
|
|
||||
|
import enumeratum._ |
||||
|
import io.circe.Encoder |
||||
|
import io.circe.generic.JsonCodec |
||||
|
import io.odin.Logger |
||||
|
import monix.bio.Task |
||||
|
import nova.monadic_sfx.util.IOUtils |
||||
|
import nova.monadic_sfx.util.reactive.Reducer |
||||
|
import nova.monadic_sfx.util.reactive.Store |
||||
|
import scalafx.scene.Parent |
||||
|
import scalafx.scene.control.Label |
||||
|
|
||||
|
object FXRouter { |
||||
|
|
||||
|
final case class State[P](page: P) |
||||
|
|
||||
|
@JsonCodec |
||||
|
sealed abstract class Action[T] |
||||
|
// final case object Init extends Action |
||||
|
final case class Replace[T](p: T) extends Action[T] |
||||
|
|
||||
|
type FXStore[P] = Store[Action[P], State[P]] |
||||
|
|
||||
|
// def resolver2 = resolver.lift.andThen(_.getOrElse(notFound)) |
||||
|
|
||||
|
// def resolver: PartialFunction[P <: Enum[P]][P, Parent] = { |
||||
|
// case Home => new TextField |
||||
|
// } |
||||
|
|
||||
|
} |
||||
|
|
||||
|
class FXRouter[P <: EnumEntry]( |
||||
|
)(implicit E: Encoder[P]) { |
||||
|
import FXRouter._ |
||||
|
|
||||
|
def store(initialPage: P, logger: Logger[Task]) = |
||||
|
Task.deferAction(implicit s => |
||||
|
Store.createL[Action[P], State[P]]( |
||||
|
Replace(initialPage), |
||||
|
State(initialPage), |
||||
|
Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _) |
||||
|
// Seq(actionLoggerMiddleware(logger, "RouterStore")) |
||||
|
) |
||||
|
) |
||||
|
|
||||
|
def reducer( |
||||
|
state: State[P], |
||||
|
action: Action[P] |
||||
|
): (State[P], Option[Task[Action[P]]]) = |
||||
|
action match { |
||||
|
// case Init => (state, None) |
||||
|
case Replace(p) => |
||||
|
(state.copy(page = p), None) |
||||
|
} |
||||
|
|
||||
|
def render( |
||||
|
resolver: P => Task[Parent] |
||||
|
)(implicit store: FXStore[P]) = |
||||
|
store.mapEval { case (_, FXRouter.State(p)) => IOUtils.toTask(resolver(p)) } |
||||
|
|
||||
|
def link( |
||||
|
page: P, |
||||
|
store: FXStore[P] |
||||
|
) = { |
||||
|
store.onNext(Replace(page)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
object BrainNotWorking { |
||||
|
|
||||
|
@JsonCodec |
||||
|
sealed trait Page extends EnumEntry |
||||
|
object Page extends Enum[Page] { |
||||
|
val values = findValues |
||||
|
final case object Home extends Page |
||||
|
final case class UserHome(id: Int) extends Page |
||||
|
} |
||||
|
|
||||
|
def resolver: PartialFunction[Page, Task[Parent]] = { |
||||
|
case Page.Home => |
||||
|
Task(new Label { |
||||
|
text = "HomePage" |
||||
|
}) |
||||
|
case Page.UserHome(id0) => |
||||
|
Task(new Label { |
||||
|
text = s"User Home, Id = $id0" |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
val router = new FXRouter[Page] |
||||
|
} |
||||
|
|
||||
|
// case class State() |
||||
|
// object RouterStore { |
||||
|
|
||||
|
// sealed trait Action |
||||
|
// case object Init extends Action |
||||
|
|
||||
|
// def reducer(state: State, action: Action) = |
||||
|
// action match { |
||||
|
// case Init => state |
||||
|
|
||||
|
// } |
||||
|
// def apply() = |
||||
|
// Store.createL[Action, State](Init, State(), Reducer(reducer _), Seq.empty) |
||||
|
// } |
@ -0,0 +1,63 @@ |
|||||
|
package nova.monadic_sfx.util |
||||
|
|
||||
|
import scalafx.beans.property.ObjectProperty |
||||
|
import scalafx.beans.property.ReadOnlyObjectProperty |
||||
|
import scalafx.beans.value.ObservableValue |
||||
|
|
||||
|
object Misc { |
||||
|
|
||||
|
implicit final class MyRichObservable[A, C](val self: ObservableValue[A, C]) |
||||
|
extends AnyVal { |
||||
|
def filter(f: A => Boolean): ReadOnlyObjectProperty[A] = |
||||
|
Method.filter(self)(f) |
||||
|
def filterNull: ReadOnlyObjectProperty[A] = Method.filterNull(self) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
object Method { |
||||
|
type Observable[A] = ObservableValue[A, _] |
||||
|
|
||||
|
def filter[B]( |
||||
|
a: Observable[B] |
||||
|
)(f: B => Boolean): ReadOnlyObjectProperty[B] = { |
||||
|
|
||||
|
val prop = ObjectProperty[B](a.value) |
||||
|
|
||||
|
def changeHandler() = |
||||
|
prop.synchronized { |
||||
|
if (f(a.value)) { |
||||
|
prop.value = a.value |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
a onChange changeHandler() |
||||
|
prop |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Simply creates a new observable that mirrors the source observable but |
||||
|
* doesn't emit null values. JavaFX likes to work with null values in scene |
||||
|
* nodes/properties (shrugs) and observables by default emit null values |
||||
|
* that can cause crashes. ScalaFX does not offer any *fixes* for this |
||||
|
* |
||||
|
* @param a |
||||
|
* @return |
||||
|
*/ |
||||
|
def filterNull[B]( |
||||
|
a: Observable[B] |
||||
|
): ReadOnlyObjectProperty[B] = { |
||||
|
|
||||
|
val prop = ObjectProperty[B](a.value) |
||||
|
|
||||
|
def changeHandler() = |
||||
|
prop.synchronized { |
||||
|
if (a.value != null) { |
||||
|
prop.value = a.value |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
a onChange changeHandler() |
||||
|
prop |
||||
|
} |
||||
|
|
||||
|
} |
@ -1,27 +1,75 @@ |
|||||
package nova.monadic_sfx.util.reactive |
package nova.monadic_sfx.util.reactive |
||||
|
|
||||
|
import java.time.LocalDateTime |
||||
|
|
||||
|
import io.circe.Encoder |
||||
|
import io.circe.Printer |
||||
|
import io.circe.generic.JsonCodec |
||||
|
import io.circe.syntax._ |
||||
import io.odin.Logger |
import io.odin.Logger |
||||
|
import io.odin.LoggerMessage |
||||
|
import io.odin.formatter.options.PositionFormat |
||||
|
import io.odin.formatter.options.ThrowableFormat |
||||
|
import io.odin.meta.Render |
||||
import monix.bio.Task |
import monix.bio.Task |
||||
import monix.reactive.Observable |
import monix.reactive.Observable |
||||
import nova.monadic_sfx.util.IOUtils._ |
|
||||
|
|
||||
// object Middleware { |
// object Middleware { |
||||
// def apply[A,M,T](ob: Observable[(A,M)], cb: (A,M) => T): Observable[(A,M)] = ob |
// def apply[A,M,T](ob: Observable[(A,M)], cb: (A,M) => T): Observable[(A,M)] = ob |
||||
// } |
// } |
||||
|
|
||||
|
@JsonCodec |
||||
|
final case class StoreInfo[A]( |
||||
|
name: String, |
||||
|
action: A, |
||||
|
time: LocalDateTime = LocalDateTime.now() |
||||
|
) |
||||
|
|
||||
|
object StoreInfo { |
||||
|
val printer = Printer.noSpaces |
||||
|
implicit def render[T: Encoder]: Render[StoreInfo[T]] = |
||||
|
new Render[StoreInfo[T]] { |
||||
|
override def render(m: StoreInfo[T]): String = printer.print(m.asJson) |
||||
|
} |
||||
|
} |
||||
|
|
||||
object Middlewares { |
object Middlewares { |
||||
|
|
||||
|
// val encoder: Encoder[LoggerMessage] = |
||||
|
// Encoder.forProduct1("message")(m => m.message.value) |
||||
|
|
||||
|
val format = create(ThrowableFormat.Default, PositionFormat.Full) |
||||
|
|
||||
|
def create( |
||||
|
throwableFormat: ThrowableFormat, |
||||
|
positionFormat: PositionFormat |
||||
|
): io.odin.formatter.Formatter = { |
||||
|
// val encoder: Encoder[LoggerMessage] = |
||||
|
// Encoder.forProduct1("message")(m => m.message.value) |
||||
|
(msg: LoggerMessage) => msg.message.value |
||||
|
} |
||||
|
|
||||
def actionStateLoggerMiddleware[A, M]( |
def actionStateLoggerMiddleware[A, M]( |
||||
logger: Logger[Task] |
logger: Logger[Task] |
||||
): Middleware[A, M] = |
|
||||
(obs: Observable[(A, M)]) => |
|
||||
obs.doOnNext { |
|
||||
case (a, m) => toTask(logger.debug(s"Received action $a with state $m")) |
|
||||
} |
|
||||
|
): Task[Middleware[A, M]] = |
||||
|
Task.deferAction(implicit s => |
||||
|
Task((obs: Observable[(A, M)]) => |
||||
|
obs.doOnNextF { |
||||
|
case (a, m) => |
||||
|
logger.debug(s"Received action $a with state $m") |
||||
|
} |
||||
|
) |
||||
|
) |
||||
|
|
||||
def actionLoggerMiddleware[A, M]( |
|
||||
logger: Logger[Task] |
|
||||
): Middleware[A, M] = |
|
||||
(obs: Observable[(A, M)]) => |
|
||||
obs.doOnNext { |
|
||||
case (a, _) => toTask(logger.debug(s"Received action $a ")) |
|
||||
} |
|
||||
|
def actionLoggerMiddleware[A: Encoder, M]( |
||||
|
logger: Logger[Task], |
||||
|
name: String |
||||
|
): Task[Middleware[A, M]] = |
||||
|
Task.deferAction(implicit s => |
||||
|
Task((obs: Observable[(A, M)]) => |
||||
|
obs.doOnNextF { |
||||
|
case (a, _) => |
||||
|
logger.debug(StoreInfo(name, a)) |
||||
|
} |
||||
|
) |
||||
|
) |
||||
} |
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue