From 04df08dd6fc7d173eb748178ed4c96dcf6f98d42 Mon Sep 17 00:00:00 2001 From: Julien Eluard Date: Tue, 5 Jun 2018 09:44:29 +0200 Subject: [PATCH] Improved figwheel layout --- deps.edn | 4 +- .../extensions/piplette/extension.edn | 52 ++++++++++ figwheel/README.md | 1 + .../assets/extensions/demo/extension.edn | 19 ++++ figwheel/resources/public/index.html | 53 +++++++++++ figwheel/src/pluto/figwheel/index.cljs | 94 +++++++++++++++++++ src/pluto/reader.cljc | 63 +++++++------ src/pluto/reader/reference.cljc | 11 ++- test/pluto/reader_test.cljc | 29 ++---- 9 files changed, 272 insertions(+), 54 deletions(-) create mode 100644 examples/resources/extensions/piplette/extension.edn create mode 100644 figwheel/README.md create mode 100644 figwheel/resources/public/assets/extensions/demo/extension.edn create mode 100644 figwheel/resources/public/index.html create mode 100644 figwheel/src/pluto/figwheel/index.cljs diff --git a/deps.edn b/deps.edn index 15a138d..552275f 100644 --- a/deps.edn +++ b/deps.edn @@ -4,11 +4,11 @@ reagent {:mvn/version "0.8.0"} re-frame {:mvn/version "0.10.5"}} :paths ["src" "resources"] - :aliases {:figwheel {:extra-paths ["target" "figwheel-resources" "examples/src" "examples/resources"] + :aliases {:figwheel {:extra-paths ["target" "figwheel/resources" "figwheel/src"] :extra-deps {com.bhauman/figwheel-main {:mvn/version "0.1.0-SNAPSHOT"} com.bhauman/rebel-readline-cljs {:mvn/version "0.1.1"} binaryage/devtools {:mvn/version "0.9.10"}} - :main-opts ["-m" "figwheel.main" "-c" "pluto.demo" "-r"]} + :main-opts ["-m" "figwheel.main" "-c" "pluto.figwheel.index" "-r"]} :test-clj {:extra-paths ["test"] :extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" :sha "5fb4fc46ad0bf2e0ce45eba5b9117a2e89166479"}} diff --git a/examples/resources/extensions/piplette/extension.edn b/examples/resources/extensions/piplette/extension.edn new file mode 100644 index 0000000..a29e084 --- /dev/null +++ b/examples/resources/extensions/piplette/extension.edn @@ -0,0 +1,52 @@ +{meta + {:name "Piplette" + :description "A light client for Peepeth" + :documentation "..."} + + lifecycle + {:on-activated [@fetch-all-posts]} + + events/fetch-all-posts + [@ethereum.logs {:address "0xfa28ec7198028438514b49a3cf353bca5541ce1d" + :topics ["PeepEth()"] + :inputs [{:name :hash :type :string}] ;; Allows to decode transaction data + :on-log [@fetch-ipfs]}] ;; A map of decoded data will be injected + + events/fetch-ipfs + (let [{:keys [hash]} properties] + [@ipfs.get {:hash hash + :on-success [@db.append {:path [:all-posts]}]}]) + + queries/all-posts + [@db.get {:path [:all-posts] + :limit 20}] + + views/post + ;; TODO get account details + ;; handle threads + (let [{:keys [content untrustedAddress untrustedTimestamp parentID]} properties] + [view {} + [view {} + [text {} + untrustedAddress] + [text {} + untrustedTimestamp]] + [text {} + content]]) + + styles/screen + {:background-color #?(:android :green + :ios :red + :web :white)} + + i18n/title + {:en "Peepeth !!"} + + hooks/main + [screen {:style @style/screen} + [toolbar {} + [text {} + @title]] + (let [posts @all-posts] + [list {:data posts + :template [@post]}])]} diff --git a/figwheel/README.md b/figwheel/README.md new file mode 100644 index 0000000..1533f2c --- /dev/null +++ b/figwheel/README.md @@ -0,0 +1 @@ +This folder contains file needed during figwheel development sessions. diff --git a/figwheel/resources/public/assets/extensions/demo/extension.edn b/figwheel/resources/public/assets/extensions/demo/extension.edn new file mode 100644 index 0000000..8c8e6f3 --- /dev/null +++ b/figwheel/resources/public/assets/extensions/demo/extension.edn @@ -0,0 +1,19 @@ +{:meta + {:name "Simple Demo" + :description "A simple demo of extension" + :documentation "Nothing. Just see a text with dynamic random color."} + + :hooks/main + {:name "" + :description "" + :view #status/view [:views/main]} + + :views/main + [view {} + [text {} "Hello"] + (let [cond? #status/query [:random-boolean]] + (if cond? + [text {:style {:color "green"}} + "World?"] + [text {:style {:color "red"}} + "World?"]))]} diff --git a/figwheel/resources/public/index.html b/figwheel/resources/public/index.html new file mode 100644 index 0000000..c4f65af --- /dev/null +++ b/figwheel/resources/public/index.html @@ -0,0 +1,53 @@ + + + + + + +
+
+ Load demo extension from + + +
+
+ +
+
+
+ + + + diff --git a/figwheel/src/pluto/figwheel/index.cljs b/figwheel/src/pluto/figwheel/index.cljs new file mode 100644 index 0000000..0488e11 --- /dev/null +++ b/figwheel/src/pluto/figwheel/index.cljs @@ -0,0 +1,94 @@ +(ns pluto.figwheel.index + (:require [clojure.string :as string] + [pluto.components.html :as html] + [pluto.reader :as reader] + [pluto.storage :as storage] + [pluto.storage.http :as http] + [pluto.storage.ipfs :as ipfs] + [reagent.core :as reagent] + [reagent.dom :as dom] + [re-frame.core :as re-frame] + [re-frame.loggers :as re-frame.loggers] + [devtools.core :as devtools])) + +(enable-console-print!) +(devtools/install!) + +(def warn (js/console.warn.bind js/console)) +(re-frame.loggers/set-loggers! + {:warn (fn [& args] + (cond + (= "re-frame: overwriting" (first args)) nil + :else (apply warn args)))}) + +(defonce do-timer (js/setInterval #(re-frame/dispatch [:random (zero? (rand-int 2))]) 1000)) + +(re-frame/reg-event-db + :random + (fn [db [_ b]] + (assoc db :random b))) + +(re-frame/reg-sub + :random-boolean + :random) + +(defn render [h el] + (reagent/render h el)) + +(defn main-hook + "A simple hook for :hooks/main" + [m] + [:div + (let [{:views/keys [main]} m] + main)]) + +(defn errors-list [v] + [:div + [:div "Errors"] + (into [:ul] + (for [{:keys [type] :as m} v] + [:li + [:span [:b (str type)] (pr-str (dissoc m :type))]]))]) + +(defn storage-for [type] + (condp = type + "url" (http/HTTPStorage.) + "ipfs" (ipfs/IPFSStorage. "https://cors.io/?https://gateway.ipfs.io"))) + +(defn fetch [uri cb] + (let [[type id] (string/split uri ":")] + (storage/fetch + (storage-for type) + {:value id} cb))) + +(defn parse [m] + (reader/parse {:components html/components + :capacities + {:queries #{:random-boolean} + :events #{}} + :valid-hooks {:hooks/main + {:extra-properties #{} + :type :view}}} + m)) + +(defn render-extension [m el el-errors] + (let [{:keys [data errors]} (parse m)] + (when errors + (render (errors-list errors) el-errors)) + (render (main-hook data) el))) + +(defn read-extension [o el el-errors] + (let [{:keys [data errors]} (reader/read (:content (first o)))] + (render-extension data el el-errors))) + +(defn render-result [{:keys [type value]} el el-errors] + (case type + :error (set! (.-innerHTML el-errors) value) + (read-extension value el el-errors))) + +(defn ^:export load-and-render + [s el el-errors] + (dom/unmount-component-at-node el) + (dom/unmount-component-at-node el-errors) + (re-frame/clear-subscription-cache!) + (fetch s #(render-result % el el-errors))) diff --git a/src/pluto/reader.cljc b/src/pluto/reader.cljc index a5a8a85..226babd 100644 --- a/src/pluto/reader.cljc +++ b/src/pluto/reader.cljc @@ -56,34 +56,39 @@ (when-let [v @errors] {:errors v})))) -(def valid-namespaces #{"extension" "hooks" "views" "events" "queries" "styles" "i18n"}) +(def valid-extension-keys #{:meta}) +(def valid-namespaces #{"hooks" "views" "events" "queries" "styles" "i18n"}) -(defn validate-keys [{:keys [valid-hooks valid-extensions]} s] - (let [keys (set (filter (comp empty? namespace) s)) - keys-with-ns (set/difference (set s) keys) - namespaces (set (map namespace keys-with-ns)) - extra-namespaces (set/difference namespaces valid-namespaces) - hooks-keys (set/difference (set (filter #(= "hooks" (namespace %)) keys-with-ns)) valid-hooks) - extension-keys (set/difference (set (filter #(= "extension" (namespace %)) keys-with-ns)) valid-extensions)] +(defn validate-keys [{:keys [valid-hooks]} s] + (let [keys (set (filter (comp empty? namespace) s)) + extra-extension-keys (set/difference keys valid-extension-keys) + keys-with-ns (set/difference (set s) keys) + namespaces (set (map namespace keys-with-ns)) + extra-namespaces (set/difference namespaces valid-namespaces) + hooks-keys (set/difference (set (filter #(= "hooks" (namespace %)) keys-with-ns)) (map key valid-hooks))] (cond-> nil - (seq keys) (assoc :no-namespace keys) - (seq extra-namespaces) (assoc :invalid-namespaces extra-namespaces) - (seq hooks-keys) (assoc :invalid-hooks hooks-keys) - (seq extension-keys) (assoc :invalid-extensions extension-keys)))) + (seq extra-extension-keys) (assoc :type :invalid-extension-keys :value extra-extension-keys) + (seq extra-namespaces) (assoc :type :invalid-namespaces :value extra-namespaces) + (seq hooks-keys) (assoc :type :invalid-hooks :value hooks-keys)))) (defn accumulate-errors [m s] (update m :errors concat s)) +(defn merge-errors [m errors] + (cond-> m + (seq errors) (accumulate-errors errors))) + (declare parse-view) (defn parse-hiccup-children [opts children] (reduce #(let [{:keys [data errors]} (parse-view opts %2)] - (cond-> (update %1 :data conj data) - (seq errors) (accumulate-errors errors))) + (merge-errors (update %1 :data conj data) + errors)) {:data []} children)) (defn parse-hiccup-element [{:keys [components] :as opts} o] ;; TODO permissions + ;; TODO handle errors (cond (or (symbol? o) (utils/primitive? o)) {:data o} (vector? o) @@ -93,8 +98,7 @@ ;; Reduce parsed children to a single map and wrap them in a hiccup element ;; whose component has been translated to the local platform (update m :data #(apply conj [(or component element) properties] %))) - (nil? component) (accumulate-errors [{:type :unknown-component :element element}]))) - :else 3)) + (nil? component) (accumulate-errors [{:type :unknown-component :value element}]))))) (defn parse-view [opts o] (cond @@ -113,35 +117,38 @@ * :errors a collection of errors" (fn [_ k _] (namespace k))) -(defmethod parse-value "extension" [_ _ v] v) +(defmethod parse-value "hooks" [opts _ v] v) (defmethod parse-value "views" [opts _ v] (parse-view opts v)) -;; TODO extension, events, queries, i18n, styles -(defmethod parse-value :default [_ k _] {:errors [{:type :unknown-element-type :value k}]}) +;; TODO events, queries, i18n, styles +(defmethod parse-value :default [_ _ _] {:errors [{:type :unknown-key}]}) (defn merge-parsed-value "Merge result of parse-value into a map. :data is updated to its parsed value :errors are accumulated" [opts m k v] - (let [{:keys [data errors]} (parse-value opts k v)] - (cond-> (assoc-in m [:data k] data) - (seq errors) (accumulate-errors (map #(assoc % :key k) errors))))) + (if (namespace k) + (let [{:keys [data errors]} (parse-value opts k v)] ;; TODO skip extension, hooks + (merge-errors (assoc-in m [:data k] data) + (map #(assoc % :key k) errors))) + m)) ;; TODO use specs for validation? Depends on opts, need to perform replacement during walk -;; specs would allow to generate UIs and help with testing - +;; specs would allow to generate UIs and help with testing + (defn parse "Parse an extension definition map as encapsulated in :data key of the map returned by read. `opts` is a map defining: - * `valid-hooks` a set of valid hooks + * `valid-hooks` a map of valid hook definitions Returns a map defining: * :data a map * :permissions a vector of required permissions - * :errors a vector of errors map triggered during parse" + * :errors a vector of errors maps triggered during parse" [opts m] (let [errors (validate-keys opts (keys m))] - (cond-> (reduce-kv #(merge-parsed-value opts %1 %2 %3) {} m) - (seq errors) (accumulate-errors (map (fn [error] {:type (key error) :value (val error)}) errors))))) + (merge-errors (reduce-kv #(merge-parsed-value opts %1 %2 %3) {} m) + errors))) +;; TODO replace all refs to query, event and view here \ No newline at end of file diff --git a/src/pluto/reader/reference.cljc b/src/pluto/reader/reference.cljc index 115b47c..304284b 100644 --- a/src/pluto/reader/reference.cljc +++ b/src/pluto/reader/reference.cljc @@ -3,7 +3,7 @@ ;; Record used to track references identified by a set of predefined tag literal -(defrecord Reference [tag value]) +(defrecord Reference [type value]) (defn create [type value] (Reference. type value)) @@ -11,11 +11,14 @@ (defmulti resolve "" - (fn [tag _] tag)) + (fn [m {:keys [type]}] type)) -(defmethod resolve :view [_ value] {:data value}) ;; TODO properly handle local refs and globally whitelisted refs +(defmethod resolve :view [m {:keys [value]}] + (if-let [view (get m value)] + {:data view} + {:errors [{:type :unknown-view :value value}]})) ;; TODO properly handle local refs and globally whitelisted refs ;; TODO prevent infinite loops due to self refs ? ;; TODO other reference types -(defmethod resolve :default [tag value] {:errors [{:type :unknown-reference-tag :tag tag :value value}]}) +(defmethod resolve :default [m {:keys [type value]}] {:errors [{:type :unknown-reference-type :value value}]}) diff --git a/test/pluto/reader_test.cljc b/test/pluto/reader_test.cljc index dd32b11..ea4be72 100644 --- a/test/pluto/reader_test.cljc +++ b/test/pluto/reader_test.cljc @@ -31,13 +31,10 @@ \"World\"]))]}")))) (deftest validate-keys - (is (= {:no-namespace #{:key}} (reader/validate-keys {} #{:views/id :key}))) - (is (= {:invalid-namespaces #{"typo"}} (reader/validate-keys {} #{:typo/id}))) - (is (= {:invalid-extensions #{:extension/unknown}} - (reader/validate-keys {:valid-extensions #{:extension/meta :extension/lifecycle}} - #{:extension/meta :extension/unknown}))) - (is (= {:invalid-hooks #{:hooks/unknown}} - (reader/validate-keys {:valid-hooks #{:hooks/main}} #{:hooks/main :hooks/unknown})))) + (is (= {:type :invalid-extension-keys, :value #{:key}} (reader/validate-keys {} #{:views/id :key}))) + (is (= {:type :invalid-namespaces :value #{"typo"}} (reader/validate-keys {} #{:typo/id}))) + (is (= {:type :invalid-hooks :value #{:hooks/unknown}} + (reader/validate-keys {:valid-hooks {:hooks/main {}}} #{:hooks/main :hooks/unknown})))) (deftest parse-hiccup-children (is (= {:data (list [:text {} ""])} (reader/parse-hiccup-children {:components {'text :text}} (list ['text {} ""]))))) @@ -45,28 +42,20 @@ (deftest parse (is (= {} (reader/parse {} {}))) (is (= {:data {:views/main ['text {} "Hello"]} - :errors (list {:type :unknown-component :element 'text :key :views/main})} + :errors (list {:type :unknown-component :value 'text :key :views/main})} (reader/parse {} {:views/main ['text {} "Hello"]}))) (is (= {:data {:views/main [:text {} "Hello"]}} (reader/parse {:components {'text :text}} {:views/main ['text {} "Hello"]}))) (is (= {:data {:views/main [:text {} "Hello"]}} (reader/parse {:components {'text :text}} {:views/main ['text {} "Hello"]})))) - #_ -(deftest parse-references - (is (= {:data {:views/main [:pluto.reader-test/main]}} (reader/parse {} {:views/main (Reference. :view [::main])})))) +(not (= {:data {:views/main nil}} :errors ({:type :unsupported-type, :value "string", :key :views/main}) {:data {:views/main nil}, :errors ({:type :unsupported-type, :value "string", :key :views/main})})) (deftest parse-blocks (is (= {:data {:views/main [blocks/let-block {:env {'s "Hello"}} [:text {} 's]]}} (reader/parse {:components {'text :text}} {:views/main (list 'let ['s "Hello"] ['text {} 's])}))) (is (= {:data {:views/main [blocks/when-block {:test 'cond} [:text {} ""]]}} (reader/parse {:components {'text :text}} {:views/main (list 'when 'cond ['text {} ""])}))) - (is (= {:data {:views/main [blocks/when-block {:test 'cond} [:text {} ""]]}} - (reader/parse {:components {'text :text}} {:views/main (list 'when "string" ['text {} ""])}))) - - #_ - (is (= nil (reader/parse {:components {'text :text}} {:views/main (list 'let ['cond? (Reference. :query [::query])] ['text])}))) - #_ - (is (= nil (reader/parse {:components {'text :text}} {:views/main (list 'let ['cond? (Reference. :query [::query])] - (list 'when 'cond? - ['text {} "World"]))})))) + (is (= {:data {:views/main nil} + :errors '({:type :unsupported-type :value "string" :key :views/main})} + (reader/parse {:components {'text :text}} {:views/main (list 'when "string" ['text {} ""])}))))