Merge branch '0.1.6'

This commit is contained in:
anatoly 2015-12-10 00:37:32 -05:00
commit e36d524176
48 changed files with 1425 additions and 588 deletions

3
.gitignore vendored
View file

@ -4,8 +4,9 @@
pom.xml pom.xml
pom.xml.asc pom.xml.asc
.repl* .repl*
dev-resources/ dev/resources/public/js/*
figwheel_server.log figwheel_server.log
build.xml
*.jar *.jar
*.class *.class
/.lein-* /.lein-*

View file

@ -35,6 +35,9 @@ _**Alan J. Perlis** from [Structure and Interpretation of Computer Programs](htt
- [Plugging into (reset)](#plugging-into-reset) - [Plugging into (reset)](#plugging-into-reset)
- [Suspendable Example Application](#suspendable-example-application) - [Suspendable Example Application](#suspendable-example-application)
- [Affected States](#affected-states) - [Affected States](#affected-states)
- [ClojureScript is Clojure](doc/clojurescript.md)
- [Logging](#logging) - [Logging](#logging)
- [Mount and Develop!](#mount-and-develop) - [Mount and Develop!](#mount-and-develop)
- [Running New York Stock Exchange](#running-new-york-stock-exchange) - [Running New York Stock Exchange](#running-new-york-stock-exchange)
@ -152,7 +155,7 @@ this `app-config`, being top level, can be used in other namespaces, including t
(defstate conn :start (create-connection app-config)) (defstate conn :start (create-connection app-config))
``` ```
[here](https://github.com/tolitius/mount/blob/master/test/app/nyse.clj) [here](dev/clj/app/nyse.clj)
is an example of a Datomic connection that "depends" on a similar `app-config`. is an example of a Datomic connection that "depends" on a similar `app-config`.
## Value of values ## Value of values
@ -191,7 +194,7 @@ Besides scalar values, lifecycle functions can take anonymous functions, partial
(defstate private-f :start pf) (defstate private-f :start pf)
``` ```
Check out [fun-with-values-test](https://github.com/tolitius/mount/blob/0.1.5/test/check/fun_with_values_test.clj) for more details. Check out [fun-with-values-test](test/mount/test/fun_with_values.cljc) for more details.
## The Importance of Being Reloadable ## The Importance of Being Reloadable
@ -207,7 +210,7 @@ dev=> (mount/start)
While it is not always necessary, mount lificycle can be easily hooked up to [tools.namespace](https://github.com/clojure/tools.namespace), While it is not always necessary, mount lificycle can be easily hooked up to [tools.namespace](https://github.com/clojure/tools.namespace),
to make the whole application reloadable with refreshing the app namespaces. to make the whole application reloadable with refreshing the app namespaces.
Here is a [dev.clj](https://github.com/tolitius/mount/blob/master/dev/dev.clj) as an example, that sums up to: Here is a [dev.clj](dev/dev.clj) as an example, that sums up to:
```clojure ```clojure
(defn go [] (defn go []
@ -244,7 +247,7 @@ dev=> (reset)
:ready :ready
``` ```
You can see examples of start and stop flows in the [example app](https://github.com/tolitius/mount#mount-and-develop). You can see examples of start and stop flows in the [example app](README.md#mount-and-develop).
## Start and Stop Parts of Application ## Start and Stop Parts of Application
@ -260,7 +263,7 @@ In REPL or during testing it is often very useful to work with / start / stop _o
which will only start/stop `app-config` and `conn` (won't start any other states). which will only start/stop `app-config` and `conn` (won't start any other states).
Here is an [example](test/check/parts_test.clj) test that uses only two namespaces checking that the third one is not started. Here is an [example](test/mount/test/parts.cljc) test that uses only two namespaces checking that the third one is not started.
## Start an Application Without Certain States ## Start an Application Without Certain States
@ -275,7 +278,7 @@ The `start-without` function can do just that:
which will start an application without starting `feed-listener` and `nrepl` states. which will start an application without starting `feed-listener` and `nrepl` states.
Here is an [example](test/check/start_without_test.clj) test that excludes Datomic connection and nREPL from an application on start. Here is an [example](test/mount/test/start_without.cljc) test that excludes Datomic connection and nREPL from an application on start.
## Swapping Alternate Implementations ## Swapping Alternate Implementations
@ -298,7 +301,7 @@ One thing to note, whenever
is run after `start-with`, it rolls back to an original "state of states", i.e. `#'app.nyse/db` is `#'app.nyse/db` again. So subsequent calls to `(mount/start)` or even to `(mount/start-with {something else})` will start from a clean slate. is run after `start-with`, it rolls back to an original "state of states", i.e. `#'app.nyse/db` is `#'app.nyse/db` again. So subsequent calls to `(mount/start)` or even to `(mount/start-with {something else})` will start from a clean slate.
Here is an [example](test/check/start_with_test.clj) test that starts an app with mocking Datomic connection and nREPL. Here is an [example](test/mount/test/start_with.cljc) test that starts an app with mocking Datomic connection and nREPL.
## Stop an Application Except Certain States ## Stop an Application Except Certain States
@ -449,7 +452,7 @@ The way this is done is via an excellent [robert hooke](https://github.com/techn
## Mount and Develop! ## Mount and Develop!
`mount` comes with an example [app](https://github.com/tolitius/mount/tree/master/test/app) `mount` comes with an example [app](dev/clj/app)
that has 3 states: that has 3 states:
* `config`, loaded from the files and refreshed on each `(reset)` * `config`, loaded from the files and refreshed on each `(reset)`

10
circle.yml Normal file
View file

@ -0,0 +1,10 @@
machine:
java:
version: oraclejdk8
test:
override:
- lein do clean, test
- lein do clean, doo phantom test once
- lein do clean, cljsbuild once prod
- lein test2junit

View file

@ -1,4 +1,4 @@
(ns app.config (ns app.conf
(:require [mount.core :as mount :refer [defstate]] (:require [mount.core :as mount :refer [defstate]]
[clojure.edn :as edn] [clojure.edn :as edn]
[clojure.tools.logging :refer [info]])) [clojure.tools.logging :refer [info]]))
@ -9,5 +9,5 @@
slurp slurp
edn/read-string)) edn/read-string))
(defstate app-config (defstate config
:start (load-config "test/resources/config.edn")) :start (load-config "dev/resources/config.edn"))

View file

@ -1,9 +1,9 @@
(ns app (ns app.example
(:require [datomic.api :as d] (:require [datomic.api :as d]
[clojure.tools.nrepl.server :refer [start-server stop-server]] [clojure.tools.nrepl.server :refer [start-server stop-server]]
[mount.core :as mount :refer [defstate]] [mount.core :as mount :refer [defstate]]
[app.utils.datomic :refer [touch]] [app.utils.datomic :refer [touch]]
[app.config :refer [app-config]] [app.conf :refer [config]]
[app.nyse :as nyse])) [app.nyse :as nyse]))
;; example on creating a network REPL ;; example on creating a network REPL
@ -11,7 +11,7 @@
(start-server :bind host :port port)) (start-server :bind host :port port))
;; nREPL is just another simple state ;; nREPL is just another simple state
(defstate nrepl :start (start-nrepl (:nrepl app-config)) (defstate nrepl :start (start-nrepl (:nrepl config))
:stop (stop-server nrepl)) :stop (stop-server nrepl))
;; datomic schema ;; datomic schema

View file

@ -2,7 +2,7 @@
(:require [mount.core :as mount :refer [defstate]] (:require [mount.core :as mount :refer [defstate]]
[datomic.api :as d] [datomic.api :as d]
[clojure.tools.logging :refer [info]] [clojure.tools.logging :refer [info]]
[app.config :refer [app-config]])) [app.conf :refer [config]]))
(defn- new-connection [conf] (defn- new-connection [conf]
(info "conf: " conf) (info "conf: " conf)
@ -17,5 +17,5 @@
(.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop) (.release conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
(d/delete-database uri))) (d/delete-database uri)))
(defstate conn :start (new-connection app-config) (defstate conn :start (new-connection config)
:stop (disconnect app-config conn)) :stop (disconnect config conn))

View file

@ -26,10 +26,10 @@
:resume (if (status :suspended) ">> resuming"))) :resume (if (status :suspended) ">> resuming")))
(defn log-status [f & args] (defn log-status [f & args]
(let [{:keys [ns name] :as state} (second args) (let [{:keys [var] :as state} (second args)
action (f-to-action f)] action (f-to-action f)]
(when-let [taking-over-the-world (whatcha-doing? state action)] (when-let [taking-over-the-world (whatcha-doing? state action)]
(info (str taking-over-the-world ".. " (ns-resolve ns name)))) (info (str taking-over-the-world ".. " var)))
(apply f args))) (apply f args)))
(defonce lifecycle-fns (defonce lifecycle-fns

View file

@ -0,0 +1,25 @@
(ns app.audit-log
(:require [datascript.core :as d]
[cljs-time.core :refer [now]])
(:require-macros [mount.core :refer [defstate]]))
(defstate log :start (d/create-conn {}))
(defn audit [db source & msg]
(d/transact! @db [{:db/id -1
:source source
:timestamp (now)
:msg (apply str msg)}]))
(defn find-source-logs [db source]
(d/q '{:find [?t ?msg]
:in [$ ?s]
:where [[?e :source ?s]
[?e :timestamp ?t]
[?e :msg ?msg]]}
@@db source))
(defn find-all-logs [db]
(->> (map :e (d/datoms @@db :aevt :timestamp))
dedupe
(d/pull-many @@db '[:timestamp :source :msg])))

9
dev/cljs/app/conf.cljs Normal file
View file

@ -0,0 +1,9 @@
(ns app.conf
(:require [app.audit-log :refer [audit log]])
(:require-macros [mount.core :refer [defstate]]))
(defn load-config [path]
(audit log :app-conf "loading config from '" path "' (at least pretending)")
{:system-a {:uri "ws://echo.websocket.org/"}})
(defstate config :start (load-config "resources/config.end"))

26
dev/cljs/app/example.cljs Normal file
View file

@ -0,0 +1,26 @@
(ns app.example
(:require [mount.core :as mount]
[app.conf]
[app.websockets]
[app.audit-log :refer [log find-all-logs]]
[cljs-time.format :refer [unparse formatters]]
[hiccups.runtime :as hiccupsrt])
(:require-macros [hiccups.core :as hiccups :refer [html]]))
(defn format-log-event [{:keys [timestamp source msg]}]
(str (unparse (formatters :date-hour-minute-second-fraction) timestamp)
" → [" (name source) "]: " msg))
(defn show-log []
(.write js/document
(html [:ul (doall (for [e (find-all-logs log)]
[:li (format-log-event e)]))])))
(mount/start)
;; time to establish a websocket connection before disconnecting
(js/setTimeout #(mount/stop-except "#'app.audit-log/log") 500)
;; time to close a connection to show it in audit
(js/setTimeout #(show-log) 1000)

View file

@ -0,0 +1,22 @@
(ns app.websockets
(:require [app.conf :refer [config]]
[app.audit-log :refer [audit log]])
(:require-macros [mount.core :refer [defstate]]))
(defn ws-status [ws]
{:url (.-url ws) :ready-state (.-readyState ws)})
(defn connect [uri]
(let [ws (js/WebSocket. uri)]
(audit log :system-a "connecting to " (ws-status ws))
(set! (.-onopen ws) #(audit log :system-a "opened " (ws-status ws)))
(set! (.-onclose ws) #(audit log :system-a "closed " (ws-status ws)))
ws))
(defn disconnect [ws]
(audit log :system-a "closing " (ws-status @ws))
(.close @ws)
(audit log :system-a "disconnecting " (ws-status @ws)))
(defstate system-a :start (connect (get-in @config [:system-a :uri]))
:stop (disconnect system-a))

View file

@ -10,18 +10,15 @@
[clojure.string :as str] [clojure.string :as str]
[clojure.test :as test] [clojure.test :as test]
[clojure.tools.namespace.repl :as tn] [clojure.tools.namespace.repl :as tn]
[check.parts-test]
[check.start-with-test]
[check.suspend-resume-test]
[mount.core :as mount] [mount.core :as mount]
[app.utils.logging :refer [with-logging-status]] [app.utils.logging :refer [with-logging-status]]
[app :refer [create-nyse-schema find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time [app.example :refer [create-nyse-schema find-orders add-order]])) ;; <<<< replace this your "app" namespace(s) you want to be available at REPL time
(defn start [] (defn start []
(with-logging-status) (with-logging-status)
(mount/start #'app.config/app-config (mount/start #'app.conf/config
#'app.nyse/conn #'app.nyse/conn
#'app/nrepl)) ;; example on how to start app with certain states #'app.example/nrepl)) ;; example on how to start app with certain states
(defn stop [] (defn stop []
(mount/stop)) (mount/stop))
@ -45,3 +42,5 @@
[] []
(stop) (stop)
(tn/refresh :after 'dev/go)) (tn/refresh :after 'dev/go))
(mount/in-clj-mode)

View file

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<script src="js/compiled/mount.js" type="text/javascript"></script>
</body>
</html>

View file

@ -1,4 +1,3 @@
(defn dev (defn dev []
[]
(require 'dev) (require 'dev)
(in-ns 'dev)) (in-ns 'dev))

120
doc/clojurescript.md Normal file
View file

@ -0,0 +1,120 @@
## Managing state in ClojureScript
- [The "Why"](#the-why)
- [Mount Modes](#mount-modes)
- [Just Clojure Mode](#just-clojure-mode)
- [Clojure and ClojureScript Mode](#clojure-_and_-clojurescript-mode)
- [Mounting that ClojureScript](#mounting-that-clojurescript)
- [Using States](#using-states)
- [Thanks](#thanks)
In case you need to manage state in ClojureScript using mount, _all_ the mount Clojure features are supported in ClojureScript.
Which means all the mount Clojure [documentation](../README.md) is the mount ClojureScript documentation.
With a slight change in [_mode_](clojurescript.md#mount-modes) ( no change in _mood_ though, just the _mode_ :)).
### The "Why"
Since [reader conditionals](http://clojure.org/reader#The%20Reader--Reader%20Conditionals) were added in Clojure 1.7,
it became a lot easier to target both platforms with lots of code reuse. You might have noticed
that most of mount code lives in `.cljc` files.
The way mount is designed it "mounts" itself to a solid Clojure [namespace API](http://clojure.org/namespaces),
and while `.cljc` helps a lot with targeting Clojure and ClojureScript, JavaScript VM is vastly different from JVM.
Since JavaScript mostly tagrets browsers, mobile devices and IoT,
it is quite importand to [compress](https://github.com/clojure/clojurescript/wiki/Advanced-Compilation) the final result.
Which means that Clojure namespaces API are not that well supported in ClojureScript, since they get renamed and optimized
during compilation + of course no native namespace support on the JavaScript side
(but that is somewhat solved with [Google Closure](https://closure-library.googlecode.com/git-history/docs/local_closure_goog_base.js.source.html#line428)).
But. When developing an application in Clojure and ClojureScript, it would only make sense if the API for any library
would be _identical_ for both platforms. It should be transparent for developers whether they use a library in Clojure or ClojureScript.
It is not possible for all libraries (i.e. concurrency, reified Vars, etc.), but we should try to make it possible for most.
### Mount Modes
Mount has two modes `clj` and `cljc`.
#### Just Clojure Mode
`clj` mode is _default_, and all the APIs are exactly the same as they are in the mount Clojure [documentation](../README.md).
#### Clojure _and_ ClojureScript Mode
`cljc` mode is is not default, but it is easy to switch to it:
```clojure
(mount/in-cljc-mode)
```
this sets mount into the `cljc` mode. In this mode mount supports _both_: Clojure and ClojureScript with one difference
from the default `clj` mode:
> all states are "_derefable_"
which means in order to use them, you'd need to `@` it. That's where the difference between two modes end.
Again, `cljc` mode API is _consistent across both_ Clojure and ClojureScript.
While initially it may sound strange, this approach has very nice properties:
* Mentally something that you defer (`@`) is associated with a state behind it
* The whole system may start lazily without an explicit call `(mount/start)`
* States may have watchers which is just an idea at this point, but it could be quite useful
Now as the theory is laid out...
### Mounting that ClojureScript
Let's look at the example [ClojureScript app](../dev/cljs/app) that uses mount to manage several states:
* [Datascript](https://github.com/tonsky/datascript) Database
* Websocket Connection
* Configuration
In order to run it, just compile `cljs` (in `:advanced` mode, because why not? :)) with:
```clojure
$ lein do clean, cljsbuild once prod
Compiling ClojureScript...
Compiling "dev/resources/public/js/compiled/mount.js" from ["src" "dev/cljs"]...
Successfully compiled "dev/resources/public/js/compiled/mount.js" in 23.966 seconds.
```
And just open a browser at `file:///[path-to-mount]/mount/dev/resources/public/index.html`:
<img src="img/mount.cljs.example.png" width="400">
The flow behind the app is quite simple:
* load config
* open a WebSocket connection
* keep an audit log in Datascript
* call `(mount/stop)` to disconnect
#### Using States
A good example of derefing state is here in [websockets.cljs](https://github.com/tolitius/mount/blob/0825ad2ed085b73b7ae989b4382ce4e0376e4be3/dev/cljs/app/websockets.cljs#L21):
```clojure
(ns app.websockets
(:require [app.conf :refer [config]]
[app.audit-log :refer [audit log]])
(:require-macros [mount.core :refer [defstate]]))
;; ...
(defstate system-a :start (connect (get-in @config [:system-a :uri]))
:stop (disconnect system-a))
```
notice how config is deferef'ed `@config` in order to use its state. It of course does not have to be deref'ed here, and
can be just passed along to the `connect` function to be `@`ed there instead.
### Thanks
I'd like to thank these good people for brainstorming and supporting the idea of Mount in ClojureScript universe:
[@DomKM](https://github.com/DomKM), [@yogthos](https://github.com/yogthos) and [@edvorg](https://github.com/edvorg)

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View file

@ -6,13 +6,56 @@
:source-paths ["src"] :source-paths ["src"]
:dependencies [[org.clojure/clojure "1.7.0"]] :dependencies [] ;; for visual clarity
:profiles {:dev {:source-paths ["dev" "test/app"] :profiles {:dev {:source-paths ["dev" "dev/clj"]
:dependencies [[yesql "0.5.1"] :dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.170"]
[datascript "0.13.3"]
[hiccups "0.3.0"]
[com.andrewmcveigh/cljs-time "0.3.14"]
[ch.qos.logback/logback-classic "1.1.3"] [ch.qos.logback/logback-classic "1.1.3"]
[org.clojure/tools.logging "0.3.1"] [org.clojure/tools.logging "0.3.1"]
[robert/hooke "1.3.0"] [robert/hooke "1.3.0"]
[org.clojure/tools.namespace "0.2.11"] [org.clojure/tools.namespace "0.2.11"]
[org.clojure/tools.nrepl "0.2.11"] [org.clojure/tools.nrepl "0.2.11"]
[com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]}}) [com.datomic/datomic-free "0.9.5327" :exclusions [joda-time]]]
:plugins [[lein-cljsbuild "1.1.1"]
[lein-doo "0.1.6"]
[lein-figwheel "0.5.0-2"]
[test2junit "1.1.3"]]
:test2junit-output-dir ~(or (System/getenv "CIRCLE_TEST_REPORTS") "target/test2junit")
:clean-targets ^{:protect false} [:target-path
[:cljsbuild :builds :dev :compiler :output-dir]
[:cljsbuild :builds :prod :compiler :output-to]]
:cljsbuild {
:builds {:dev
{:source-paths ["src" "dev/cljs"]
:figwheel true
:compiler {:main app.example
:asset-path "js/compiled/out"
:output-to "dev/resources/public/js/compiled/mount.js"
:output-dir "dev/resources/public/js/compiled/out"
:optimizations :none
:source-map true
:source-map-timestamp true}}
:test
{:source-paths ["src" "dev/cljs" "test"]
:compiler {:main mount.test
;; :asset-path "js/compiled/out"
:output-to "dev/resources/public/js/compiled/mount.js"
:output-dir "dev/resources/public/js/compiled/test"
:optimizations :none
:source-map true
:source-map-timestamp true}}
:prod
{:source-paths ["src" "dev/cljs"]
:compiler {:output-to "dev/resources/public/js/compiled/mount.js"
:optimizations :advanced
:pretty-print false}}}}}
:test {:source-paths ["dev" "test/clj" "test"]}})

View file

@ -1,220 +0,0 @@
(ns mount.core
(:require [mount.tools.macro :as macro]))
(defonce ^:private mount-state 42)
(defonce ^:private -args (atom :no-args)) ;; mostly for command line args and external files
(defonce ^:private state-seq (atom 0))
(defonce ^:private state-order (atom {}))
(defonce ^:private running (atom {})) ;; to clean dirty states on redefs
;; supporting tools.namespace: (disable-reload!)
(alter-meta! *ns* assoc ::load false) ;; to exclude the dependency
(defn- make-state-seq [state]
(or (@state-order state)
(let [nseq (swap! state-seq inc)]
(swap! state-order assoc state nseq)
nseq)))
(deftype NotStartedState [state]
Object
(toString [this]
(str "'" state "' is not started (to start all the states call mount/start)")))
;;TODO validate the whole lifecycle
(defn- validate [{:keys [start stop suspend resume] :as lifecycle}]
(cond
(not start) (throw
(IllegalArgumentException. "can't start a stateful thing without a start function. (i.e. missing :start fn)"))
(and suspend (not resume)) (throw
(IllegalArgumentException. "suspendable state should have a resume function (i.e. missing :resume fn)"))))
(defn- with-ns [ns name]
(str ns "/" name))
(defn- pounded? [f]
(let [pound "(fn* [] "] ;;TODO: think of a better (i.e. typed) way to distinguish #(f params) from (fn [params] (...)))
(.startsWith (str f) pound)))
(defn- unpound [f]
(if (pounded? f)
(nth f 2) ;; magic 2 is to get the body => ["fn*" "[]" "(fn body)"]
f))
(defn- cleanup-if-dirty
"in case a namespace is recompiled without calling (mount/stop),
a running state instance will still be running.
this function stops this 'lost' state instance.
it is meant to be called by defstate before defining a new state"
[state]
(when-let [stop (@running state)]
(stop)))
(defmacro defstate [state & body]
(let [[state params] (macro/name-with-attributes state body)
{:keys [start stop suspend resume] :as lifecycle} (apply hash-map params)]
(validate lifecycle)
(cleanup-if-dirty (with-ns *ns* state))
(let [s-meta (cond-> {:mount-state mount-state
:order (make-state-seq (with-ns *ns* state))
:start `(fn [] ~start)
:status #{:stopped}}
stop (assoc :stop `(fn [] ~(unpound stop)))
suspend (assoc :suspend `(fn [] ~suspend))
resume (assoc :resume `(fn [] ~resume)))]
`(defonce ~(with-meta state (merge (meta state) s-meta))
(NotStartedState. ~(str state))))))
(defn- record! [{:keys [ns name]} f done]
(let [state (f)]
(swap! done conj (ns-resolve ns name))
state))
(defn- up [var {:keys [ns name start stop resume status] :as state} done]
(when-not (:started status)
(let [s (try (if (:suspended status)
(record! state resume done)
(record! state start done))
(catch Throwable t
(throw (RuntimeException. (str "could not start [" name "] due to") t))))]
(intern ns (symbol name) s)
(swap! running assoc (with-ns ns name) stop)
(alter-meta! var assoc :status #{:started}))))
(defn- down [var {:keys [ns name stop status] :as state} done]
(when (some status #{:started :suspended})
(when stop
(try
(record! state stop done)
(catch Throwable t
(throw (RuntimeException. (str "could not stop [" name "] due to") t)))))
(intern ns (symbol name) (NotStartedState. name)) ;; (!) if a state does not have :stop when _should_ this might leak
(swap! running dissoc (with-ns ns name))
(alter-meta! var assoc :status #{:stopped})))
(defn- sigstop [var {:keys [ns name suspend resume status] :as state} done]
(when (and (:started status) resume) ;; can't have suspend without resume, but the reverse is possible
(when suspend ;; don't suspend if there is only resume function (just mark it :suspended?)
(let [s (try (record! state suspend done)
(catch Throwable t
(throw (RuntimeException. (str "could not suspend [" name "] due to") t))))]
(intern ns (symbol name) s)))
(alter-meta! var assoc :status #{:suspended})))
(defn- sigcont [var {:keys [ns name start resume status] :as state} done]
(when (instance? NotStartedState var)
(throw (RuntimeException. (str "could not resume [" name "] since it is stoppped (i.e. not suspended)"))))
(when (:suspended status)
(let [s (try (record! state resume done)
(catch Throwable t
(throw (RuntimeException. (str "could not resume [" name "] due to") t))))]
(intern ns (symbol name) s)
(alter-meta! var assoc :status #{:started}))))
;;TODO args might need more thinking
(defn args [] @-args)
(defn mount-state? [var]
(= (-> var meta :mount-state)
mount-state))
(defn find-all-states []
(->> (all-ns)
(mapcat ns-interns)
(map second)
(filter mount-state?)))
;;TODO ns based for now. need to be _state_ based
(defn- add-deps [{:keys [ns] :as state} all]
(let [refers (ns-refers ns)
any (set all)
deps (filter (comp any val) refers)]
(assoc state :deps deps)))
(defn states-with-deps []
(let [all (find-all-states)]
(->> (map (comp #(add-deps % all)
#(select-keys % [:name :order :ns :status])
meta)
all)
(sort-by :order))))
(defn- bring [states fun order]
(let [done (atom [])]
(->> states
(sort-by (comp :order meta) order)
(map #(fun % (meta %) done))
dorun)
@done))
(defn- merge-lifecycles
"merges with overriding _certain_ non existing keys.
i.e. :suspend is in a 'state', but not in a 'substitute': it should be overriden with nil
however other keys of 'state' (such as :ns,:name,:order) should not be overriden"
([state sub]
(merge-lifecycles state nil sub))
([state origin {:keys [start stop suspend resume status]}]
(assoc state :origin origin
:status status
:start start :stop stop :suspend suspend :resume resume)))
(defn- rollback! [state]
(let [{:keys [origin]} (meta state)]
(when origin
(alter-meta! state #(merge-lifecycles % origin)))))
(defn- substitute! [state with]
(let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :status])
origin (meta state)
sub (meta with)]
(alter-meta! with assoc :sub? true)
(alter-meta! state #(merge-lifecycles % (lifecycle-fns origin) sub))))
(defn- unsub [state]
(when (-> (meta state) :sub?)
(alter-meta! state dissoc :sub?)))
(defn- all-without-subs []
(remove (comp :sub? meta) (find-all-states)))
(defn start [& states]
(let [states (or (seq states) (all-without-subs))]
{:started (bring states up <)}))
(defn stop [& states]
(let [states (or states (find-all-states))
_ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with"
stopped (bring states down >)]
(dorun (map rollback! states)) ;; restore to origin from "start-with"
{:stopped stopped}))
(defn stop-except [& states]
(let [all (set (find-all-states))
states (remove (set states) all)]
(apply stop states)))
(defn start-with-args [xs & states]
(reset! -args xs)
(if (first states)
(start states)
(start)))
(defn start-with [with]
(doseq [[from to] with]
(substitute! from to))
(start))
(defn start-without [& states]
(if (first states)
(let [app (set (all-without-subs))
without (remove (set states) app)]
(apply start without))
(start)))
(defn suspend [& states]
(let [states (or (seq states) (all-without-subs))]
{:suspended (bring states sigstop <)}))
(defn resume [& states]
(let [states (or (seq states) (all-without-subs))]
{:resumed (bring states sigcont <)}))

271
src/mount/core.cljc Normal file
View file

@ -0,0 +1,271 @@
(ns mount.core
#?(:clj (:require [mount.tools.macro :refer [on-error throw-runtime] :as macro])
:cljs (:require [mount.tools.macro :as macro]
[mount.tools.cljs :as cljs]))
#?(:cljs (:require-macros [mount.tools.macro :refer [on-error throw-runtime]])))
(defonce ^:private -args (atom :no-args)) ;; mostly for command line args and external files
(defonce ^:private state-seq (atom 0))
(defonce ^:private mode (atom :clj))
(defonce ^:private meta-state (atom {}))
(defonce ^:private running (atom {})) ;; to clean dirty states on redefs
;; supporting tools.namespace: (disable-reload!)
#?(:clj
(alter-meta! *ns* assoc ::load false)) ;; to exclude the dependency
(defn- make-state-seq [state]
(or (:order (@meta-state state))
(swap! state-seq inc)))
(deftype NotStartedState [state]
Object
(toString [this]
(str "'" state "' is not started (to start all the states call mount/start)")))
;;TODO validate the whole lifecycle
(defn- validate [{:keys [start stop suspend resume] :as lifecycle}]
(cond
(not start) (throw-runtime "can't start a stateful thing without a start function. (i.e. missing :start fn)")
(and suspend
(not resume)) (throw-runtime "suspendable state should have a resume function (i.e. missing :resume fn)")))
(defn- with-ns [ns name]
(str "#'" ns "/" name))
(defn- pounded? [f]
(let [pound "(fn* [] "] ;;TODO: think of a better (i.e. typed) way to distinguish #(f params) from (fn [params] (...)))
#?(:clj (.startsWith (str f) pound)
:cljs (cljs/starts-with? (str f) pound))))
(defn unpound [f]
(if (pounded? f)
(nth f 2) ;; magic 2 is to get the body => ["fn*" "[]" "(fn body)"]
f))
(defn- cleanup-if-dirty
"in case a namespace is recompiled without calling (mount/stop),
a running state instance will still be running.
this function stops this 'lost' state instance.
it is meant to be called by defstate before defining a new state"
[state]
(when-let [stop (@running state)]
(stop)))
#?(:clj
(defn current-state [state]
(let [{:keys [inst var]} (@meta-state state)]
(if (= @mode :cljc)
@inst
(var-get var))))
:cljs
(defn current-state [state]
(-> (@meta-state state) :inst deref)))
#?(:clj
(defn alter-state! [{:keys [var inst]} value]
(if (= @mode :cljc)
(reset! inst value)
(alter-var-root var (constantly value))))
:cljs
(defn alter-state! [{:keys [inst]} value]
(reset! inst value)))
(defn- update-meta! [path v]
(swap! meta-state assoc-in path v))
(defn- record! [state-name f done]
(let [state (f)]
(swap! done conj state-name)
state))
(defn- up [state {:keys [start stop resume status] :as current} done]
(when-not (:started status)
(let [s (on-error (str "could not start [" state "] due to")
(if (:suspended status)
(record! state resume done)
(record! state start done)))]
(alter-state! current s)
(swap! running assoc state stop)
(update-meta! [state :status] #{:started}))))
(defn- down [state {:keys [stop status] :as current} done]
(when (some status #{:started :suspended})
(when stop
(on-error (str "could not stop [" state "] due to")
(record! state stop done)))
(alter-state! current (NotStartedState. state)) ;; (!) if a state does not have :stop when _should_ this might leak
(swap! running dissoc state)
(update-meta! [state :status] #{:stopped})))
(defn- sigstop [state {:keys [resume suspend status] :as current} done]
(when (and (:started status) resume) ;; can't have suspend without resume, but the reverse is possible
(when suspend ;; don't suspend if there is only resume function (just mark it :suspended?)
(let [s (on-error (str "could not suspend [" state "] due to")
(record! state suspend done))]
(alter-state! current s)))
(update-meta! [state :status] #{:suspended})))
(defn- sigcont [state {:keys [resume status] :as current} done]
(when (:suspended status)
(let [s (on-error (str "could not resume [" state "] due to")
(record! state resume done))]
(alter-state! current s)
(update-meta! [state :status] #{:started}))))
(deftype DerefableState [name]
#?(:clj clojure.lang.IDeref
:cljs IDeref)
(#?(:clj deref
:cljs -deref)
[_]
(let [{:keys [status inst] :as state} (@meta-state name)]
(when-not (:started status)
(up name state (atom #{})))
@inst)))
(defmacro defstate [state & body]
(let [[state params] (macro/name-with-attributes state body)
{:keys [start stop suspend resume] :as lifecycle} (apply hash-map params)
state-name (with-ns #?(:clj *ns*
:cljs (cljs/this-ns)) state) ;; on cljs side (cljs.analyzer/*cljs-ns*) may do it, but still might not be good for :advanced
order (make-state-seq state-name)
sym (str state)]
(validate lifecycle)
(cleanup-if-dirty state-name)
(let [s-meta (cond-> {:order order
:start `(fn [] ~start)
:status #{:stopped}}
stop (assoc :stop `(fn [] ~stop))
suspend (assoc :suspend `(fn [] ~suspend))
resume (assoc :resume `(fn [] ~resume)))]
`(do
(def ~state (DerefableState. ~state-name))
((var update-meta!) [~state-name] (assoc ~s-meta :inst (atom (NotStartedState. ~state-name))
:var (var ~state)))
(var ~state)))))
(defn in-cljc-mode []
(reset! mode :cljc))
(defn in-clj-mode []
(reset! mode :clj))
;;TODO args might need more thinking
(defn args [] @-args)
(defn- find-all-states []
(keys @meta-state))
;;TODO ns based for now. need to be _state_ based
#_(defn- add-deps [{:keys [ns] :as state} all]
(let [refers (ns-refers ns)
any (set all)
deps (filter (comp any val) refers)]
(assoc state :deps deps)))
#_(defn states-with-deps []
(let [all (find-all-states)]
(->> (map (comp #(add-deps % all)
#(select-keys % [:name :order :ns :status])
meta)
all)
(sort-by :order))))
#?(:clj
(defn- var-to-str [v]
(str v)))
#?(:cljs
(defn var-to-str [v]
(if (var? v)
(let [{:keys [ns name]} (meta v)]
(with-ns ns name))
v)))
(defn- bring [states fun order]
(let [done (atom [])]
(as-> states $
(map var-to-str $)
(select-keys @meta-state $)
(sort-by (comp :order val) order $)
(doseq [[k v] $] (fun k v done)))
@done))
(defn- merge-lifecycles
"merges with overriding _certain_ non existing keys.
i.e. :suspend is in a 'state', but not in a 'substitute': it should be overriden with nil
however other keys of 'state' (such as :ns,:name,:order) should not be overriden"
([state sub]
(merge-lifecycles state nil sub))
([state origin {:keys [start stop suspend resume status]}]
(assoc state :origin origin
:status status
:start start :stop stop :suspend suspend :resume resume)))
(defn- rollback! [state]
(let [{:keys [origin] :as sub} (@meta-state state)]
(when origin
(update-meta! [state] (merge-lifecycles sub origin)))))
(defn- substitute! [state with]
(let [lifecycle-fns #(select-keys % [:start :stop :suspend :resume :status])
origin (@meta-state state)
sub (@meta-state with)]
(update-meta! [with :sub?] true)
(update-meta! [state] (merge-lifecycles origin (lifecycle-fns origin) sub))))
(defn- unsub [state]
(when (-> (@meta-state state) :sub?)
(update-meta! [state :sub?] nil)))
(defn- all-without-subs []
(remove (comp :sub? @meta-state) (find-all-states)))
(defn start [& states]
(let [states (or (seq states) (all-without-subs))]
{:started (bring states up <)}))
(defn stop [& states]
(let [states (or states (find-all-states))
_ (dorun (map unsub states)) ;; unmark substitutions marked by "start-with"
stopped (bring states down >)]
(dorun (map rollback! states)) ;; restore to origin from "start-with"
{:stopped stopped}))
(defn stop-except [& states]
(let [all (set (find-all-states))
states (map var-to-str states)
states (remove (set states) all)]
(apply stop states)))
(defn start-with-args [xs & states]
(reset! -args xs)
(if (first states)
(start states)
(start)))
(defn start-with [with]
(doseq [[from to] with]
(substitute! (var-to-str from)
(var-to-str to)))
(start))
(defn start-without [& states]
(if (first states)
(let [app (set (all-without-subs))
states (map var-to-str states)
without (remove (set states) app)]
(apply start without))
(start)))
(defn suspend [& states]
(let [states (or (seq states) (all-without-subs))]
{:suspended (bring states sigstop <)}))
(defn resume [& states]
(let [states (or (seq states) (all-without-subs))]
{:resumed (bring states sigcont <)}))

View file

@ -0,0 +1,9 @@
(ns mount.tools.cljs
(:require [cljs.analyzer :as ana]
[goog.string :as gstring]))
(defn this-ns []
ana/*cljs-ns*)
(defn starts-with? [s pre]
(gstring/startsWith s pre))

View file

@ -1,5 +1,17 @@
(ns mount.tools.macro) (ns mount.tools.macro)
(defmacro on-error [msg f]
`(try
~f
(catch #?(:clj Throwable
:cljs :default) t#
(throw #?(:clj (RuntimeException. ~msg t#)
:cljs (js/Error (str ~msg (.-stack t#))))))))
(defmacro throw-runtime [msg]
`(throw #?(:clj (RuntimeException. ~msg)
:cljs (js/Error (str ~msg)))))
;; this is a one to one copy from https://github.com/clojure/tools.macro ;; this is a one to one copy from https://github.com/clojure/tools.macro
;; to avoid a lib dependency for a single function ;; to avoid a lib dependency for a single function

View file

@ -1,13 +0,0 @@
(ns check.cleanup_dirty_states_test
(:require [mount.core :as mount]
[app]
[clojure.test :refer :all]))
(deftest cleanup-dirty-states
(let [_ (mount/start)]
(is (not (.isClosed (:server-socket app/nrepl))))
(require 'app :reload)
(mount/start) ;; should not result in "BindException Address already in use" since the clean up will stop the previous instance
(is (not (.isClosed (:server-socket app/nrepl))))
(mount/stop)
(is (instance? mount.core.NotStartedState app/nrepl))))

View file

@ -1,55 +0,0 @@
(ns check.fun-with-values-test
(:require [mount.core :as mount :refer [defstate]]
[clojure.test :refer :all]))
(defn f [n]
(fn [m]
(+ n m)))
(defn g [a b]
(+ a b))
(defn- pf [n]
(+ 41 n))
(defn fna []
42)
(defstate scalar :start 42)
(defstate fun :start #(inc 41))
(defstate with-fun :start (inc 41))
(defstate with-partial :start (partial g 41))
(defstate f-in-f :start (f 41))
(defstate f-no-args-value :start (fna))
(defstate f-no-args :start fna)
(defstate f-args :start g)
(defstate f-value :start (g 41 1))
(defstate private-f :start pf)
(defn with-fun-and-values [f]
(mount/start #'check.fun-with-values-test/scalar
#'check.fun-with-values-test/fun
#'check.fun-with-values-test/with-fun
#'check.fun-with-values-test/with-partial
#'check.fun-with-values-test/f-in-f
#'check.fun-with-values-test/f-args
#'check.fun-with-values-test/f-no-args-value
#'check.fun-with-values-test/f-no-args
#'check.fun-with-values-test/private-f
#'check.fun-with-values-test/f-value)
(f)
(mount/stop))
(use-fixtures :each with-fun-and-values)
(deftest fun-with-values
(is (= scalar 42))
(is (= (fun) 42))
(is (= with-fun 42))
(is (= (with-partial 1) 42))
(is (= (f-in-f 1) 42))
(is (= f-no-args-value 42))
(is (= (f-no-args) 42))
(is (= (f-args 41 1) 42))
(is (= (private-f 1) 42))
(is (= f-value 42)))

View file

@ -1,17 +0,0 @@
(ns check.parts-test
(:require [mount.core :as mount :refer [defstate] :as m]
[app.nyse :refer [conn]]
[clojure.test :refer :all]))
(defstate should-not-start :start #(constantly 42))
(defn with-parts [f]
(m/start #'app.config/app-config #'app.nyse/conn)
(f)
(m/stop))
(use-fixtures :each with-parts)
(deftest start-only-parts
(is (instance? datomic.peer.LocalConnection conn))
(is (instance? mount.core.NotStartedState should-not-start)))

View file

@ -1,14 +0,0 @@
(ns check.private-fun-test
(:require [mount.core :as mount :refer [defstate]]
[check.fun-with-values-test :refer [private-f]]
[clojure.test :refer :all]))
(defn with-fun-and-values [f]
(mount/start #'check.fun-with-values-test/private-f)
(f)
(mount/stop))
(use-fixtures :each with-fun-and-values)
(deftest fun-with-valuesj
(is (= (private-f 1) 42)))

View file

@ -1,47 +0,0 @@
(ns check.start-with-test
(:require [mount.core :as mount :refer [defstate]]
[app.config :refer [app-config]]
[app.nyse :refer [conn]]
[app :refer [nrepl]]
[clojure.test :refer :all]))
(defstate test-conn :start 42
:stop #(constantly 0))
(defstate test-nrepl :start [])
(deftest start-with
(testing "should start with substitutes"
(let [_ (mount/start-with {#'app.nyse/conn #'check.start-with-test/test-conn
#'app/nrepl #'check.start-with-test/test-nrepl})]
(is (map? app-config))
(is (vector? nrepl))
(is (= conn 42))
(mount/stop)))
(testing "should not start the substitute itself"
(let [_ (mount/start-with {#'app.nyse/conn #'check.start-with-test/test-conn})]
(is (instance? mount.core.NotStartedState test-conn))
(is (= conn 42))
(mount/stop)))
(testing "should start normally after start-with"
(let [_ (mount/start)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (= test-conn 42))
(is (vector? test-nrepl))
(mount/stop)))
(testing "should start-without normally after start-with"
(let [_ (mount/start-without #'check.start-with-test/test-conn
#'check.start-with-test/test-nrepl)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (instance? mount.core.NotStartedState test-conn))
(is (instance? mount.core.NotStartedState test-nrepl))
(mount/stop))))

View file

@ -1,18 +0,0 @@
(ns check.start-without-test
(:require [mount.core :as m]
[app.config :refer [app-config]]
[app.nyse :refer [conn]]
[app :refer [nrepl]]
[clojure.test :refer :all]))
(defn without [f]
(m/start-without #'app.nyse/conn #'app/nrepl)
(f)
(m/stop))
(use-fixtures :each without)
(deftest start-without-states
(is (map? app-config))
(is (instance? mount.core.NotStartedState nrepl))
(is (instance? mount.core.NotStartedState conn)))

View file

@ -1,31 +0,0 @@
(ns check.stop-except-test
(:require [mount.core :as mount :refer [defstate]]
[app.config :refer [app-config]]
[app.nyse :refer [conn]]
[app :refer [nrepl]]
[clojure.test :refer :all]))
(deftest stop-except
(testing "should stop all except nrepl"
(let [_ (mount/start)
_ (mount/stop-except #'app.nyse/conn #'app.config/app-config)]
(is (map? app-config))
(is (instance? datomic.peer.LocalConnection conn))
(is (instance? mount.core.NotStartedState nrepl))
(mount/stop)))
(testing "should start normally after stop-except"
(let [_ (mount/start)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(mount/stop)))
(testing "should stop all normally after stop-except"
(let [_ (mount/start)
_ (mount/stop-except #'app.nyse/conn #'app.config/app-config)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState app-config))
(is (instance? mount.core.NotStartedState conn))
(is (instance? mount.core.NotStartedState nrepl)))))

View file

@ -1,141 +0,0 @@
(ns check.suspend-resume-test
(:require [mount.core :as mount :refer [defstate]]
[app.config :refer [app-config]]
[app.nyse :refer [conn]]
[app :refer [nrepl]]
[clojure.test :refer :all]))
(defn koncat [k s]
(-> (name k)
(str "-" (name s))
keyword))
(defn start [s] (koncat s :started))
(defn stop [s] (koncat s :stopped))
(defn suspend [s] (koncat s :suspended))
(defn resume [s] (koncat s :resumed))
(defstate web-server :start (start :w)
:stop (stop :w)
:suspend (suspend :w)
:resume (resume :w))
(defstate q-listener :start (start :q)
:stop (stop :q)
:suspend (suspend :q)
:resume (resume :q))
(defstate randomizer :start (rand-int 42))
(deftest suspendable-lifecycle
(testing "should suspend _only suspendable_ states that are currently started"
(let [_ (mount/start)
_ (mount/suspend)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-suspended))
(mount/stop)))
(testing "should resume _only suspendable_ states that are currently suspended"
(let [_ (mount/start)
_ (mount/stop #'app/nrepl)
_ (mount/suspend)
_ (mount/resume)]
(is (map? app-config))
(is (instance? mount.core.NotStartedState nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-resumed))
(mount/stop)))
(testing "should start all the states, except the ones that are currently suspended, should resume them instead"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start)]
(is (map? app-config))
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-resumed))
(mount/stop)))
(testing "should stop all: started and suspended"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState app-config))
(is (instance? mount.core.NotStartedState nrepl))
(is (instance? mount.core.NotStartedState conn))
(is (instance? mount.core.NotStartedState web-server)))))
(deftest suspendable-start-with
(testing "when replacing a non suspendable state with a suspendable one,
the later should be able to suspend/resume,
the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
(let [_ (mount/start-with {#'app/nrepl #'check.suspend-resume-test/web-server})
_ (mount/suspend)]
(is (= nrepl :w-suspended))
(is (instance? mount.core.NotStartedState web-server))
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? clojure.tools.nrepl.server.Server nrepl))
(is (= web-server :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
;; it also messy, because usually :stop function refers the _original_ state by name (i.e. #(disconnect conn))
;; (unchanged/not substituted in its lexical scope), and original state won't be started
(testing "when replacing a suspendable state with a non suspendable one,
the later should not be suspendable,
the original should still be suspendable and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start-with {#'check.suspend-resume-test/web-server #'check.suspend-resume-test/randomizer})
_ (mount/suspend)]
(is (integer? web-server))
(is (instance? mount.core.NotStartedState randomizer))
(mount/stop)
(mount/start)
(mount/suspend)
(is (integer? randomizer))
(is (= web-server :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a non suspendable started one,
the later should not be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start-with {#'check.suspend-resume-test/web-server #'app.nyse/conn}) ;; TODO: good to WARN on started states during "start-with"
_ (mount/suspend)]
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-suspended)) ;; since the "conn" does not have a resume method, so web-server was not started
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? datomic.peer.LocalConnection conn))
(is (= web-server :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a suspendable one,
the later should be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start-with {#'check.suspend-resume-test/web-server #'check.suspend-resume-test/q-listener})] ;; TODO: good to WARN on started states during "start-with"
(is (= q-listener :q-suspended))
(is (= web-server :q-resumed))
(mount/suspend)
(is (= q-listener :q-suspended))
(is (= web-server :q-suspended))
(mount/stop)
(is (instance? mount.core.NotStartedState web-server))
(is (instance? mount.core.NotStartedState q-listener))
(mount/start)
(mount/suspend)
(is (= q-listener :q-suspended))
(is (= web-server :w-suspended))
(mount/stop))))

13
test/clj/app/conf.clj Normal file
View file

@ -0,0 +1,13 @@
(ns app.conf
(:require [mount.core :as mount :refer [defstate]]
[clojure.edn :as edn]
[clojure.tools.logging :refer [info]]))
(defn load-config [path]
(info "loading config from" path)
(-> path
slurp
edn/read-string))
(defstate config
:start (load-config "dev/resources/config.edn"))

65
test/clj/app/example.clj Normal file
View file

@ -0,0 +1,65 @@
(ns app.example
(:require [datomic.api :as d]
[clojure.tools.nrepl.server :refer [start-server stop-server]]
[mount.core :as mount :refer [defstate]]
[app.utils.datomic :refer [touch]]
[app.conf :refer [config]]
[app.nyse :as nyse]))
;; example on creating a network REPL
(defn- start-nrepl [{:keys [host port]}]
(start-server :bind host :port port))
;; nREPL is just another simple state
(defstate nrepl :start (start-nrepl (:nrepl @config))
:stop (stop-server @nrepl))
;; datomic schema
(defn create-schema [conn]
(let [schema [{:db/id #db/id [:db.part/db]
:db/ident :order/symbol
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/index true
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/bid
:db/valueType :db.type/bigdec
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/qty
:db/valueType :db.type/long
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}
{:db/id #db/id [:db.part/db]
:db/ident :order/offer
:db/valueType :db.type/bigdec
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}]]
@(d/transact conn schema)))
(defn add-order [ticker bid offer qty] ;; can take connection as param
@(d/transact @nyse/conn [{:db/id (d/tempid :db.part/user)
:order/symbol ticker
:order/bid bid
:order/offer offer
:order/qty qty}]))
(defn find-orders [ticker] ;; can take connection as param
(let [orders (d/q '[:find ?e :in $ ?ticker
:where [?e :order/symbol ?ticker]]
(d/db @nyse/conn) ticker)]
(touch @nyse/conn orders)))
(defn create-nyse-schema []
(create-schema @nyse/conn))
;; example of an app entry point
(defn -main [& args]
(mount/start))

21
test/clj/app/nyse.clj Normal file
View file

@ -0,0 +1,21 @@
(ns app.nyse
(:require [mount.core :as mount :refer [defstate]]
[datomic.api :as d]
[clojure.tools.logging :refer [info]]
[app.conf :refer [config]]))
(defn- new-connection [conf]
(info "conf: " conf)
(let [uri (get-in @conf [:datomic :uri])]
(info "creating a connection to datomic:" uri)
(d/create-database uri)
(d/connect uri)))
(defn disconnect [conf conn]
(let [uri (get-in @conf [:datomic :uri])]
(info "disconnecting from " uri)
(.release @conn) ;; usually it's not released, here just to illustrate the access to connection on (stop)
(d/delete-database uri)))
(defstate conn :start (new-connection config)
:stop (disconnect config conn))

View file

@ -0,0 +1,11 @@
(ns app.utils.datomic
(:require [datomic.api :as d]))
(defn entity [conn id]
(d/entity (d/db conn) id))
(defn touch [conn results]
"takes 'entity ids' results from a query
e.g. '#{[272678883689461] [272678883689462] [272678883689459] [272678883689457]}'"
(let [e (partial entity conn)]
(map #(-> % first e d/touch) results)))

View file

@ -0,0 +1,46 @@
(ns app.utils.logging ;; << change to your namespace/path
(:require [mount.core]
[robert.hooke :refer [add-hook clear-hooks]]
[clojure.string :refer [split]]
[clojure.tools.logging :refer [info]]))
(alter-meta! *ns* assoc ::load false)
(defn- f-to-action [f]
(let [fname (-> (str f)
(split #"@")
first)]
(case fname
"mount.core$up" :up
"mount.core$down" :down
"mount.core$sigstop" :suspend
"mount.core$sigcont" :resume
:noop)))
(defn whatcha-doing? [{:keys [status suspend]} action]
(case action
:up (if (status :suspended) ">> resuming"
(if-not (status :started) ">> starting"))
:down (if (or (status :started) (status :suspended)) "<< stopping")
:suspend (if (and (status :started) suspend) "<< suspending")
:resume (if (status :suspended) ">> resuming")))
(defn log-status [f & args]
(let [{:keys [var] :as state} (second args)
action (f-to-action f)]
(when-let [taking-over-the-world (whatcha-doing? state action)]
(info (str taking-over-the-world ".. " var)))
(apply f args)))
(defonce lifecycle-fns
#{#'mount.core/up
#'mount.core/down
#'mount.core/sigstop
#'mount.core/sigcont})
(defn without-logging-status []
(doall (map #(clear-hooks %) lifecycle-fns)))
(defn with-logging-status []
(without-logging-status)
(doall (map #(add-hook % log-status) lifecycle-fns)))

35
test/mount/test.cljc Normal file
View file

@ -0,0 +1,35 @@
(ns mount.test
(:require
#?@(:cljs [[cljs.test :as t]
[doo.runner :refer-macros [doo-tests]]]
:clj [[clojure.test :as t]])
mount.core
mount.test.fun-with-values
mount.test.private-fun
mount.test.parts
mount.test.cleanup-dirty-states
mount.test.stop-except
mount.test.start-without
mount.test.start-with
mount.test.suspend-resume
))
(mount.core/in-cljc-mode)
#?(:cljs
;; (doo.runner/do-all-tests)
(doo-tests
'mount.test.fun-with-values
'mount.test.private-fun
'mount.test.parts
'mount.test.cleanup-dirty-states
'mount.test.stop-except
'mount.test.start-without
'mount.test.start-with
'mount.test.suspend-resume
))
(defn run-tests []
(t/run-all-tests #"mount.test.*"))

View file

@ -0,0 +1,34 @@
(ns mount.test.cleanup-dirty-states
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]
[app.websockets :refer [system-a]]
[app.conf :refer [config]]
[app.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]
[app.example]])
[mount.test.helper :refer [dval helper forty-two]]))
#?(:clj
(deftest cleanup-dirty-states
(let [_ (mount/start)]
(is (not (.isClosed (:server-socket (dval app.example/nrepl)))))
(require 'app.example :reload)
(mount/start) ;; should not result in "BindException Address already in use" since the clean up will stop the previous instance
(is (not (.isClosed (:server-socket (dval app.example/nrepl)))))
(mount/stop)
(is (instance? mount.core.NotStartedState (dval app.example/nrepl))))))
#?(:cljs
(deftest cleanup-dirty-states
(let [_ (mount/start #'mount.test.helper/helper)]
(is (= :started (dval helper)))
(is (= 42 @forty-two))
(.require js/goog "mount.test.helper") ;; should have run :stop of `helper`
;; (is (= :cleaned @forty-two)) ;; TODO: figure out how to reload a namespace properly
;; (is (instance? mount.core.NotStartedState (dval helper)))
(mount/start #'mount.test.helper/helper)
(is (= :started (dval helper)))
(mount/stop)
(is (instance? mount.core.NotStartedState (dval helper))))))

View file

@ -0,0 +1,59 @@
(ns mount.test.fun-with-values
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]])))
(defn f [n]
(fn [m]
(+ n m)))
(defn g [a b]
(+ a b))
(defn- pf [n]
(+ 41 n))
(defn fna []
42)
(defstate scalar :start 42)
(defstate fun :start #(inc 41))
(defstate with-fun :start (inc 41))
(defstate with-partial :start (partial g 41))
(defstate f-in-f :start (f 41))
(defstate f-no-args-value :start (fna))
(defstate f-no-args :start fna)
(defstate f-args :start g)
(defstate f-value :start (g 41 1))
(defstate private-f :start pf)
(defn start-states []
(mount/start #'mount.test.fun-with-values/scalar
#'mount.test.fun-with-values/fun
#'mount.test.fun-with-values/with-fun
#'mount.test.fun-with-values/with-partial
#'mount.test.fun-with-values/f-in-f
#'mount.test.fun-with-values/f-args
#'mount.test.fun-with-values/f-no-args-value
#'mount.test.fun-with-values/f-no-args
#'mount.test.fun-with-values/private-f
#'mount.test.fun-with-values/f-value))
(use-fixtures :once
#?(:cljs {:before start-states
:after mount/stop}
:clj #((start-states) (%) (mount/stop))))
(deftest fun-with-values
(is (= @scalar 42))
(is (= (@fun) 42))
(is (= @with-fun 42))
(is (= (@with-partial 1) 42))
(is (= (@f-in-f 1) 42))
(is (= @f-no-args-value 42))
(is (= (@f-no-args) 42))
(is (= (@f-args 41 1) 42))
(is (= (@private-f 1) 42))
(is (= @f-value 42)))

View file

@ -0,0 +1,18 @@
(ns mount.test.helper
(:require
#?@(:cljs [[mount.core :as mount :refer-macros [defstate]]]
:clj [[mount.core :as mount :refer [defstate]]])))
(defn dval
"returns a value of DerefableState without deref'ing it"
[d]
(-> (@@(var mount.core/meta-state)
#?(:clj (.name d)
:cljs (.-name d)))
:inst
deref))
(def forty-two (atom 42))
(defstate helper :start :started
:stop (reset! forty-two :cleaned))

View file

@ -0,0 +1,36 @@
(ns mount.test.parts
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]
[app.websockets :refer [system-a]]
[app.conf :refer [config]]
[app.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]
[app.nyse :refer [conn]]])
[mount.test.helper :refer [dval]]))
(defstate should-not-start :start (constantly 42))
#?(:clj
(defn with-parts [f]
(mount/start #'app.conf/config #'app.nyse/conn)
(f)
(mount/stop)))
(use-fixtures :once
#?(:cljs {:before #(mount/start #'app.conf/config #'app.audit-log/log)
:after mount/stop}
:clj with-parts))
#?(:clj
(deftest start-only-parts
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (instance? mount.core.NotStartedState (dval should-not-start)))))
#?(:cljs
(deftest start-only-parts
(is (instance? datascript.db/DB @(dval log)))
(is (map? (dval config)))
(is (instance? mount.core.NotStartedState (dval should-not-start)))
(is (instance? mount.core.NotStartedState (dval system-a)))))

View file

@ -0,0 +1,18 @@
(ns mount.test.private-fun
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]])
[mount.test.fun-with-values :refer [private-f]]))
(use-fixtures :once
#?(:cljs {:before #(mount/start #'mount.test.fun-with-values/private-f)
:after mount/stop}
:clj #((mount/start #'mount.test.fun-with-values/private-f)
(%)
(mount/stop))))
(deftest fun-with-values
(is (= (@private-f 1) 42)))

View file

@ -0,0 +1,93 @@
(ns mount.test.start-with
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]
[app.websockets :refer [system-a]]
[app.conf :refer [config]]
[app.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]
[app.conf :refer [config]]
[app.nyse :refer [conn]]
[app.example :refer [nrepl]]])
[mount.test.helper :refer [dval helper]]))
(defstate test-conn :start 42
:stop (constantly 0))
(defstate test-nrepl :start [])
#?(:cljs
(deftest start-with
(testing "should start with substitutes"
(let [_ (mount/start-with {#'app.websockets/system-a #'mount.test.start-with/test-conn
#'mount.test.helper/helper #'mount.test.start-with/test-nrepl})]
(is (map? (dval config)))
(is (vector? (dval helper)))
(is (= (dval system-a) 42))
(is (instance? datascript.db/DB @(dval log)))
(mount/stop)))
(testing "should not start the substitute itself"
(let [_ (mount/start-with {#'app.websockets/system-a #'mount.test.start-with/test-conn})]
(is (instance? mount.core.NotStartedState (dval test-conn)))
(is (= 42 (dval system-a)))
(mount/stop)))
(testing "should start normally after start-with"
(let [_ (mount/start)]
(is (map? (dval config)))
(is (instance? datascript.db/DB @(dval log)))
(is (instance? js/WebSocket (dval system-a)))
(is (= 42 (dval test-conn)))
(is (vector? (dval test-nrepl)))
(is (= :started (dval helper)))
(mount/stop)))
(testing "should start-without normally after start-with"
(let [_ (mount/start-without #'mount.test.start-with/test-conn
#'mount.test.start-with/test-nrepl)]
(is (map? (dval config)))
(is (instance? datascript.db/DB @(dval log)))
(is (instance? js/WebSocket (dval system-a)))
(is (= :started (dval helper)))
(is (instance? mount.core.NotStartedState (dval test-conn)))
(is (instance? mount.core.NotStartedState (dval test-nrepl)))
(mount/stop)))))
#?(:clj
(deftest start-with
(testing "should start with substitutes"
(let [_ (mount/start-with {#'app.nyse/conn #'mount.test.start-with/test-conn
#'app.example/nrepl #'mount.test.start-with/test-nrepl})]
(is (map? (dval config)))
(is (vector? (dval nrepl)))
(is (= (dval conn) 42))
(mount/stop)))
(testing "should not start the substitute itself"
(let [_ (mount/start-with {#'app.nyse/conn #'mount.test.start-with/test-conn})]
(is (instance? mount.core.NotStartedState (dval test-conn)))
(is (= (dval conn) 42))
(mount/stop)))
(testing "should start normally after start-with"
(let [_ (mount/start)]
(is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval test-conn) 42))
(is (vector? (dval test-nrepl)))
(mount/stop)))
(testing "should start-without normally after start-with"
(let [_ (mount/start-without #'mount.test.start-with/test-conn
#'mount.test.start-with/test-nrepl)]
(is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (instance? mount.core.NotStartedState (dval test-conn)))
(is (instance? mount.core.NotStartedState (dval test-nrepl)))
(mount/stop)))))

View file

@ -0,0 +1,37 @@
(ns mount.test.start-without
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]
[app.websockets :refer [system-a]]
[app.conf :refer [config]]
[app.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]
[app.conf :refer [config]]
[app.nyse :refer [conn]]
[app.example :refer [nrepl]]])
[mount.test.helper :refer [dval helper]]))
#?(:clj
(defn without [f]
(mount/start-without #'app.nyse/conn #'app.example/nrepl)
(f)
(mount/stop)))
(use-fixtures :once
#?(:cljs {:before #(mount/start-without #'mount.test.helper/helper #'app.websockets/system-a)
:after mount/stop}
:clj without))
#?(:clj
(deftest start-without-states
(is (map? (dval config)))
(is (instance? mount.core.NotStartedState (dval nrepl)))
(is (instance? mount.core.NotStartedState (dval conn)))))
#?(:cljs
(deftest start-without-states
(is (map? (dval config)))
(is (instance? datascript.db/DB @(dval log)))
(is (instance? mount.core.NotStartedState (dval helper)))
(is (instance? mount.core.NotStartedState (dval system-a)))))

View file

@ -0,0 +1,66 @@
(ns mount.test.stop-except
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]
[app.websockets :refer [system-a]]
[app.conf :refer [config]]
[app.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]
[app.conf :refer [config]]
[app.nyse :refer [conn]]
[app.example :refer [nrepl]]])
[mount.test.helper :refer [dval helper]]))
#?(:cljs
(deftest stop-except
(testing "should stop all except nrepl"
(let [_ (mount/start)
_ (mount/stop-except #'app.audit-log/log #'mount.test.helper/helper)]
(is (= :started (dval helper)))
(is (instance? datascript.db/DB @(dval log)))
(is (instance? mount.core.NotStartedState (dval config)))
(is (instance? mount.core.NotStartedState (dval system-a)))
(mount/stop)))
(testing "should start normally after stop-except"
(let [_ (mount/start)]
(is (map? (dval config)))
(is (instance? js/WebSocket (dval system-a)))
(is (instance? datascript.db/DB @(dval log)))
(mount/stop)))
(testing "should stop all normally after stop-except"
(let [_ (mount/start)
_ (mount/stop-except #'app.audit-log/log #'mount.test.helper/helper)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState (dval config)))
(is (instance? mount.core.NotStartedState (dval log)))
(is (instance? mount.core.NotStartedState (dval system-a)))))))
#?(:clj
(deftest stop-except
(testing "should stop all except nrepl"
(let [_ (mount/start)
_ (mount/stop-except #'app.nyse/conn #'app.conf/config)]
(is (map? (dval config)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (instance? mount.core.NotStartedState (dval nrepl)))
(mount/stop)))
(testing "should start normally after stop-except"
(let [_ (mount/start)]
(is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(mount/stop)))
(testing "should stop all normally after stop-except"
(let [_ (mount/start)
_ (mount/stop-except #'app.nyse/conn #'app.conf/config)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState (dval config)))
(is (instance? mount.core.NotStartedState (dval conn)))
(is (instance? mount.core.NotStartedState (dval nrepl)))))))

View file

@ -0,0 +1,210 @@
(ns mount.test.suspend-resume
(:require
#?@(:cljs [[cljs.test :as t :refer-macros [is are deftest testing use-fixtures]]
[mount.core :as mount :refer-macros [defstate]]
[app.websockets :refer [system-a]]
[app.conf :refer [config]]
[app.audit-log :refer [log]]]
:clj [[clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]
[app.conf :refer [config]]
[app.nyse :refer [conn]]
[app.example :refer [nrepl]]])
[mount.test.helper :refer [dval]]))
(defn koncat [k s]
(-> (name k)
(str "-" (name s))
keyword))
(defn start [s] (koncat s :started))
(defn stop [s] (koncat s :stopped))
(defn suspend [s] (koncat s :suspended))
(defn resume [s] (koncat s :resumed))
(defstate web-server :start (start :w)
:stop (stop :w)
:suspend (suspend :w)
:resume (resume :w))
(defstate q-listener :start (start :q)
:stop (stop :q)
:suspend (suspend :q)
:resume (resume :q))
(defstate randomizer :start (rand-int 42))
#?(:cljs
(deftest suspendable-lifecycle
(testing "should suspend _only suspendable_ states that are currently started"
(let [_ (mount/start)
_ (mount/suspend)]
(is (map? (dval config)))
(is (instance? datascript.db/DB @(dval log)))
(is (instance? js/WebSocket (dval system-a)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
(testing "should resume _only suspendable_ states that are currently suspended"
(let [_ (mount/start)
_ (mount/stop #'app.websockets/system-a)
_ (mount/suspend)
_ (mount/resume)]
(is (map? (dval config)))
(is (instance? mount.core.NotStartedState (dval system-a)))
(is (instance? datascript.db/DB @(dval log)))
(is (= (dval web-server) :w-resumed))
(mount/stop)))
(testing "should start all the states, except the ones that are currently suspended, should resume them instead"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start)]
(is (map? (dval config)))
(is (instance? js/WebSocket (dval system-a)))
(is (instance? datascript.db/DB @(dval log)))
(is (= (dval web-server) :w-resumed))
(mount/stop)))
(testing "should stop all: started and suspended"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState (dval config)))
(is (instance? mount.core.NotStartedState (dval system-a)))
(is (instance? mount.core.NotStartedState (dval log)))
(is (instance? mount.core.NotStartedState (dval web-server)))))))
#?(:cljs
(deftest suspendable-start-with
(testing "when replacing a non suspendable state with a suspendable one,
the later should be able to suspend/resume,
the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
(let [_ (mount/start-with {#'app.websockets/system-a #'mount.test.suspend-resume/web-server})
_ (mount/suspend)]
(is (= (dval system-a) :w-suspended))
(is (instance? mount.core.NotStartedState (dval web-server)))
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? js/WebSocket (dval system-a)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))))
#?(:clj
(deftest suspendable-lifecycle
(testing "should suspend _only suspendable_ states that are currently started"
(let [_ (mount/start)
_ (mount/suspend)]
(is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
(testing "should resume _only suspendable_ states that are currently suspended"
(let [_ (mount/start)
_ (mount/stop #'app.example/nrepl)
_ (mount/suspend)
_ (mount/resume)]
(is (map? (dval config)))
(is (instance? mount.core.NotStartedState (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-resumed))
(mount/stop)))
(testing "should start all the states, except the ones that are currently suspended, should resume them instead"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start)]
(is (map? (dval config)))
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-resumed))
(mount/stop)))
(testing "should stop all: started and suspended"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/stop)]
(is (instance? mount.core.NotStartedState (dval config)))
(is (instance? mount.core.NotStartedState (dval nrepl)))
(is (instance? mount.core.NotStartedState (dval conn)))
(is (instance? mount.core.NotStartedState (dval web-server)))))))
#?(:clj
(deftest suspendable-start-with
(testing "when replacing a non suspendable state with a suspendable one,
the later should be able to suspend/resume,
the original should not be suspendable after resume and preserve its lifecycle fns after rollback/stop"
(let [_ (mount/start-with {#'app.example/nrepl #'mount.test.suspend-resume/web-server})
_ (mount/suspend)]
(is (= (dval nrepl) :w-suspended))
(is (instance? mount.core.NotStartedState (dval web-server)))
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? clojure.tools.nrepl.server.Server (dval nrepl)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
;; it also messy, because usually :stop function refers the _original_ state by name (i.e. #(disconnect conn))
;; (unchanged/not substituted in its lexical scope), and original state won't be started
(testing "when replacing a suspendable state with a non suspendable one,
the later should not be suspendable,
the original should still be suspendable and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start-with {#'mount.test.suspend-resume/web-server #'mount.test.suspend-resume/randomizer})
_ (mount/suspend)]
(is (integer? (dval web-server)))
(is (instance? mount.core.NotStartedState (dval randomizer)))
(mount/stop)
(mount/start)
(mount/suspend)
(is (integer? (dval randomizer)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a non suspendable started one,
the later should not be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start-with {#'mount.test.suspend-resume/web-server #'app.nyse/conn}) ;; TODO: good to WARN on started states during "start-with"
_ (mount/suspend)]
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-suspended)) ;; since the "conn" does not have a resume method, so web-server was not started
(mount/stop)
(mount/start)
(mount/suspend)
(is (instance? datomic.peer.LocalConnection (dval conn)))
(is (= (dval web-server) :w-suspended))
(mount/stop)))
;; this is a messy use case, but can still happen especially at REPL time
(testing "when replacing a suspended state with a suspendable one,
the later should be suspendable,
the original should still be suspended and preserve its lifecycle fns after the rollback/stop"
(let [_ (mount/start)
_ (mount/suspend)
_ (mount/start-with {#'mount.test.suspend-resume/web-server
#'mount.test.suspend-resume/q-listener})] ;; TODO: good to WARN on started states during "start-with"
(is (= (dval q-listener) :q-suspended))
(is (= (dval web-server) :q-resumed))
(mount/suspend)
(is (= (dval q-listener) :q-suspended))
(is (= (dval web-server) :q-suspended))
(mount/stop)
(is (instance? mount.core.NotStartedState (dval web-server)))
(is (instance? mount.core.NotStartedState (dval q-listener)))
(mount/start)
(mount/suspend)
(is (= (dval q-listener) :q-suspended))
(is (= (dval web-server) :w-suspended))
(mount/stop)))))

View file

@ -0,0 +1,59 @@
(ns mount.test.var.fun-with-values
(:require [clojure.test :as t :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]))
(defn f [n]
(fn [m]
(+ n m)))
(defn g [a b]
(+ a b))
(defn- pf [n]
(+ 41 n))
(defn fna []
42)
(defstate scalar :start 42)
(defstate fun :start #(inc 41))
(defstate with-fun :start (inc 41))
(defstate with-partial :start (partial g 41))
(defstate f-in-f :start (f 41))
(defstate f-no-args-value :start (fna))
(defstate f-no-args :start fna)
(defstate f-args :start g)
(defstate f-value :start (g 41 1))
(defstate private-f :start pf)
(defn start-states []
(mount/in-clj-mode)
(require :reload 'mount.test.var.fun-with-values)
(mount/start #'mount.test.var.fun-with-values/scalar
#'mount.test.var.fun-with-values/fun
#'mount.test.var.fun-with-values/with-fun
#'mount.test.var.fun-with-values/with-partial
#'mount.test.var.fun-with-values/f-in-f
#'mount.test.var.fun-with-values/f-args
#'mount.test.var.fun-with-values/f-no-args-value
#'mount.test.var.fun-with-values/f-no-args
#'mount.test.var.fun-with-values/private-f
#'mount.test.var.fun-with-values/f-value))
(defn stop-states []
(mount/stop)
(mount/in-cljc-mode))
(use-fixtures :once #((start-states) (%) (stop-states)))
(deftest fun-with-values
(is (= scalar 42))
(is (= (fun) 42))
(is (= with-fun 42))
(is (= (with-partial 1) 42))
(is (= (f-in-f 1) 42))
(is (= f-no-args-value 42))
(is (= (f-no-args) 42))
(is (= (f-args 41 1) 42))
(is (= (private-f 1) 42))
(is (= f-value 42)))

View file

@ -0,0 +1,17 @@
(ns mount.test.var.private-fun
(:require [clojure.test :refer [is are deftest testing use-fixtures]]
[mount.core :as mount :refer [defstate]]
[mount.test.var.fun-with-values :refer [private-f]]))
(defn in-clj-mode [f]
(mount/in-clj-mode)
(require :reload 'mount.test.var.fun-with-values 'mount.test.var.private-fun)
(mount/start #'mount.test.var.fun-with-values/private-f)
(f)
(mount/stop)
(mount/in-cljc-mode))
(use-fixtures :once in-clj-mode)
(deftest fun-with-values
(is (= (private-f 1) 42)))