re-frame à la spec

290 Views

March 29, 18

スライド概要

Introduction to ClojureScript SPA framework "re-frame" and its integration with clojure.spec.

profile-image

「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Python, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬

シェア

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

re-frame à la spec

2.

Self-introduction lagénorhynque /laʒenɔʁɛ k ̃ / カマイルカ (defprofile lagénorhynque :name "Kent OHASHI" :languages [Clojure Haskell Python Scala English français Deutsch русский] :interests [programming language-learning mathematics] :contributing [github.com/japan-clojurians/clojure-site-ja])

3.

1. Example App 2. Introduction to re-frame 3. Integration with clojure.spec

4.

Example App rock-paper-scissors game

5.

source code lagenorhynque/rock-paper-scissors inspired by Elm開発における思考フロー

6.

demo $ git clone git@github.com:lagenorhynque/rock-paper-scissors.git $ cd rock-paper-scissors $ lein figwheel dev or from Emacs: cider-jack-in-clojurescript

7.

Game start

8.

Select your hand

9.

Result: lose

10.

Select your hand

11.

Result: draw

12.

Select your hand

13.

Result: win

14.

Introduction to re-frame

15.

re-frame A Reagent Framework For Writing SPAs, in Clojurescript.

16.

features based on Reagent data-oriented design purely functional for the most part

17.

similar frameworks Elm Architecture Redux (React) ngrx (Angular)

18.

Reagent React Reagent JSX (React element) Hiccup-like DSL React component ClojureScript function props args of component function state reagent.core/atom cf. ClojureScript & ReagentでReact⼊⾨してみた

19.

re-frame's data ow (a.k.a. "dominoes") 1. event dispatch 2. event handling 3. effect handling 4. query 5. view 6. DOM

20.

1. event dispatch dispatch synchronously to [::events/initialize-db] (defn ^:export init [] (re-frame/dispatch-sync [::events/initialize-db]) (dev-setup) (mount-root)) src/cljs/rock_paper_scissors/core.cljs#L19-L22

21.

2. event handling event handler for ::initialize-db (re-frame/reg-event-db ::initialize-db (fn [_ _] db/default-db)) src/cljs/rock_paper_scissors/events.cljs#L6-L9

22.

3. effect handling effect handler for :db (reg-fx :db (fn [value] (if-not (identical? @app-db value) (reset! app-db value)))) src/re_frame/fx.cljc#L162-L166

23.

4. query subscription for ::scene (re-frame/reg-sub ::scene (fn [db _] (:scene db))) src/cljs/rock_paper_scissors/subs.cljs#L5-L8

24.

5. view subscribe to [::subs/scene] (defn main-panel [] (case @(re-frame/subscribe [::subs/scene]) ::db/start [start-button "Game Start"] ::db/now-playing [hands] ::db/over [:div [:h1 @(re-frame/subscribe [::subs/you-enemy-hands])] [result] [start-button "Next Game"]])) src/cljs/rock_paper_scissors/views.cljs#L26-L33

25.

6. DOM

26.
[beta]
1'. event dispatch
dispatch to [::events/next-game]
(defn start-button [label]
[:input {:type "button"
:on-click #(re-frame/dispatch [::events/next-game])
:value label}])

src/cljs/rock_paper_scissors/views.cljs#L8-L11

27.

2'. event handling event handler for ::next-game (re-frame/reg-event-db ::next-game (fn [db _] (assoc db :scene ::db/now-playing))) src/cljs/rock_paper_scissors/events.cljs#L11-L14

28.

3'. effect handling effect handler for :db (reg-fx :db (fn [value] (if-not (identical? @app-db value) (reset! app-db value)))) src/re_frame/fx.cljc#L162-L166

29.

4'. query subscription for ::scene (re-frame/reg-sub ::scene (fn [db _] (:scene db))) src/cljs/rock_paper_scissors/subs.cljs#L5-L8

30.

5'. view subscribe to [::subs/scene] (defn main-panel [] (case @(re-frame/subscribe [::subs/scene]) ::db/start [start-button "Game Start"] ::db/now-playing [hands] ::db/over [:div [:h1 @(re-frame/subscribe [::subs/you-enemy-hands])] [result] [start-button "Next Game"]])) src/cljs/rock_paper_scissors/views.cljs#L26-L33

31.

6'. DOM

32.
[beta]
1''. event dispatch
dispatch to [::events/select-your-hand h]
(defn hands []
[:div (map (fn [h]
^{:key h}
[:input {:type "button"
:on-click
#(re-frame/dispatch
[::events/select-your-hand h])
:value h}])
[::rps/rock ::rps/paper ::rps/scissors])])

src/cljs/rock_paper_scissors/views.cljs#L13-L19

33.

2''. event handling event handler for ::events/select-your-hand (re-frame/reg-event-fx ::select-your-hand [(re-frame/inject-cofx ::cofx/select-enemy-hand)] (fn [{:keys [db enemy-hand]} [_ h]] {:db (assoc db :you h :enemy enemy-hand :scene ::db/over)})) src/cljs/rock_paper_scissors/events.cljs#L16-L23

34.

3''. effect handling coeffect handler for ::select-enemy-hand (re-frame/reg-cofx ::select-enemy-hand (fn [cofx _] (assoc cofx :enemy-hand (rps/->hand (rand-int 3))))) src/cljs/rock_paper_scissors/cofx.cljs#L5-L9

35.

effect handler for :db (reg-fx :db (fn [value] (if-not (identical? @app-db value) (reset! app-db value)))) src/re_frame/fx.cljc#L162-L166

36.

4''. query subscription for ::scene (re-frame/reg-sub ::scene (fn [db _] (:scene db))) src/cljs/rock_paper_scissors/subs.cljs#L5-L8

37.
[beta]
subscription for ::you-enemy
(re-frame/reg-sub
::you-enemy
(fn [db _]
(select-keys db [:you :enemy])))

src/cljs/rock_paper_scissors/subs.cljs#L10-L13
subscription for ::you-enemy-hands
(re-frame/reg-sub
::you-enemy-hands
:<- [::you-enemy]
(fn [{:keys [you enemy]} _]
(str (name you) "(YOU) VS " (name enemy) "(ENEMY)")))

src/cljs/rock_paper_scissors/subs.cljs#L15-L19

38.

subscription for ::fight-result (re-frame/reg-sub ::fight-result :<- [::you-enemy] (fn [{:keys [you enemy]} _] (rps/fight you enemy))) src/cljs/rock_paper_scissors/subs.cljs#L21-L25

39.

subscription for ::result-color (re-frame/reg-sub ::result-color :<- [::fight-result] (fn [r _] (case r ::rps/win "red" ::rps/lose "blue" ::rps/draw "gray"))) src/cljs/rock_paper_scissors/subs.cljs#L27-L34

40.

5''. view subscribe to [::subs/scene] subscribe to [::subs/you-enemy-hands] (defn main-panel [] (case @(re-frame/subscribe [::subs/scene]) ::db/start [start-button "Game Start"] ::db/now-playing [hands] ::db/over [:div [:h1 @(re-frame/subscribe [::subs/you-enemy-hands])] [result] [start-button "Next Game"]])) src/cljs/rock_paper_scissors/views.cljs#L26-L33

41.

subscribe to [::subs/fight-result] subscribe to [::subs/result-color] (defn result [] (let [r @(re-frame/subscribe [::subs/fight-result])] [:h1 {:style {:color @(re-frame/subscribe [::subs/result-color])}} r])) src/cljs/rock_paper_scissors/views.cljs#L21-L24

42.

6''. DOM

43.

Integration with clojure.spec

44.

speccing policy split directories/namespaces for specs use clojure.spec in development only spec domain logic spec db data

45.

split directories/namespaces for specs directory structure rock-paper-scissors ├── specs │ └── cljs │ └── rock_paper_scissors │ └── * │ └── specs.cljs ├── src │ └── cljs │ └── rock_paper_scissors │ └── *.cljs └── test └── cljs └── rock_paper_scissors ├── *_test.cljs └── runner.cljs

46.
[beta]
use clojure.spec in development only
cljsbuild settings
:cljsbuild
{:builds
[{:id
:source-paths
,,,}
{:id
:source-paths
,,,}
{:id
:source-paths
,,,}
]}

"dev"
["src/cljs" "specs/cljs"]
"min"
["src/cljs"]
"test"
["src/cljs" "specs/cljs" "test/cljs"]

project.clj#L40-L70

47.

spec domain logic specs for rock-paper-scissors data (s/def ::hand #{::rps/rock ::rps/paper ::rps/scissors}) (s/def ::hand-num (s/int-in 0 3)) (s/def ::result #{::rps/win ::rps/lose ::rps/draw}) specs/cljs/rock_paper_scissors/rps/specs.cljs#L5-L9

48.

specs for rock-paper-scissors functions (s/fdef rps/<-hand :args (s/cat :hand ::hand) :ret ::hand-num) (s/fdef rps/->hand :args (s/cat :num ::hand-num) :ret ::hand) (s/fdef rps/fight :args (s/cat :you ::hand :enemy ::hand) :ret ::result) specs/cljs/rock_paper_scissors/rps/specs.cljs#L11L22

49.

spec db data (s/def ::you ::rps.specs/hand) (s/def ::enemy ::rps.specs/hand) (s/def ::scene #{::db/start ::db/now-playing ::db/over}) (s/def ::db (s/keys :req-un [::you ::enemy ::scene])) specs/cljs/rock_paper_scissors/db/specs.cljs#L6L12

50.

testing policy test domain logic test events via dispatch test subscriptions via subscribe do not test views

51.

test domain logic example-based tests with specs instrumented (t/use-fixtures :once {:before #(stest/instrument)}) (t/deftest test-fight (t/testing "rock-paper-scissors" (t/is (= ::sut/win (sut/fight ::sut/rock ::sut/scissors))) (t/is (= ::sut/lose (sut/fight ::sut/scissors ::sut/rock))) (t/is (= ::sut/draw (sut/fight ::sut/paper ::sut/paper))))) test/cljs/rock_paper_scissors/rps_test.cljs#L10-L17

52.

property-based tests using specs (tc/defspec prop-test-fight 1000 (let [fspec (s/get-spec #'sut/fight)] (prop/for-all [[you enemy] (-> fspec :args s/gen)] (s/valid? (:ret fspec) (sut/fight you enemy))))) test/cljs/rock_paper_scissors/rps_test.cljs#L33-L38

53.
[beta]
test events and subscriptions
(t/use-fixtures ; instrument specs
:once {:before #(stest/instrument)})
(defn test-fixtures []
(re-frame/reg-fx ; mock :db effect
:db
(fn [value] ; validate db with specs
(when-not (s/valid? ::db.specs/db value)
(throw (ex-info "db spec check failed"
(s/explain-data ::db.specs/db value))))
(if-not (identical? @app-db value)
(reset! app-db value)))))

test/cljs/rock_paper_scissors/events_test.cljs#L16L26

54.

(t/deftest test-initialize-db (re-frame.test/run-test-sync (test-fixtures) (re-frame/dispatch [::sut/initialize-db]) (t/is ::db/start @(re-frame/subscribe [::subs/scene])) (t/is {:you ::rps/rock :enemy ::rps/rock} @(re-frame/subscribe [::subs/you-enemy])))) test/cljs/rock_paper_scissors/events_test.cljs#L28L35

55.

(t/deftest test-select-your-hand (re-frame.test/run-test-sync (test-fixtures) (re-frame/reg-cofx ; mock ::cofx/select-enemy-hand coeffect ::cofx/select-enemy-hand (fn [cofx _] (assoc cofx :enemy-hand ::rps/rock))) (re-frame/dispatch [::sut/initialize-db]) (t/testing "draw" (re-frame/dispatch [::sut/next-game]) (re-frame/dispatch [::sut/select-your-hand ::rps/rock]) (t/is ::db/over @(re-frame/subscribe [::subs/scene])) (t/is "rock(YOU) VS rock(ENEMY)" @(re-frame/subscribe [::subs/you(t/is ::rps/draw @(re-frame/subscribe [::subs/fight-result])) (t/is "gray" @(re-frame/subscribe [::subs/result-color]))))) test/cljs/rock_paper_scissors/events_test.cljs#L44L73

56.

re-frame x clojure.spec unobtrusive use of clojure.spec only in development in separate directory/file/namespace focus on domain logic and db

57.

A little effort to write specs and tests can make our ClojureScript frontend life much happier!

58.

Further Reading example code lagenorhynque/rock-paper-scissors

59.

Reagent reagent-project/reagent: A minimalistic ClojureScript interface to React.js Guide to Reagent ClojureScript & ReagentでReact⼊⾨してみた Qiita

60.

re-frame Day8/re-frame: A Reagent Framework For Writing SPAs, in Clojurescript. Day8/re-frame-test: Cross platform (cljs and clj) utilities for testing re-frame applications Day8/re-frame-10x: A debugging dashboard for re-frame epochs. Comes with free x-ray glasses. Re-frame: The Guide to Building Blocks

61.

clojure.spec clojure.spec - Rationale and Overview clojure.spec - 論理的根拠と概要 spec Guide やってみる!clojure.spec Spectacular Future with clojure.spec