diff --git a/docs/concepts/Block.md b/docs/concepts/Block.md new file mode 100644 index 0000000..9c52ac8 --- /dev/null +++ b/docs/concepts/Block.md @@ -0,0 +1,55 @@ +--- +title: Block +sidebar_label: Block +--- + +# Let block + +## Destructuring + +### Sequential data structure + +Extract seq elements by index +Elements can be ignored with `_` +`:as symbol` denotes the whole sequence + +#### Examples + +```clojure +[[a _ c :as all] [1 2 3 4 5]] +;; a = 1 +;; c = 3 +;; all = [1 2 3 4 5] +``` + +### Associative data structure + +Extract map values by keys `{a :a}` +Optional values via `[a 5]` +No namespaced keyword support, no short syntax + +#### Examples + +```clojure +[{a :a a :b [c 4] :c :as all} {:a 1 :b 2 :d 3}] +;; a = 1 +;; b = 2 +;; c = 4 +;; all = {:a 1 :b 2 :d 3} +``` + +#### Nesting + +Both structure type destrucuring can be combined + +```clojure +[{[a1 _ a3] :a {d :d #{f 7} :f [[_ g2] 8}] :b [_ e2] :e :all all} + {:a [1 2 3] :b {:d 4 :e [5 6] :g [9 10]}}] +;; a1 = 1 +;; a3 = 3 +;; d = 4 +;; e2 = 5 +;; f = 7 +;; g2 = 10 +;; all = {:a [1 2 3] :b {:d 4 :e [5 6] :g [9 10]}} +``` diff --git a/src/pluto/reader/blocks.cljc b/src/pluto/reader/blocks.cljc index 8db94e4..3a8b1a2 100644 --- a/src/pluto/reader/blocks.cljc +++ b/src/pluto/reader/blocks.cljc @@ -1,11 +1,12 @@ (ns pluto.reader.blocks + (:refer-clojure :exclude [destructure]) (:require [clojure.walk :as walk] [pluto.reader.errors :as errors] [pluto.reader.reference :as reference] [re-frame.core :as re-frame])) (defmulti parse - "" + "Parse a block element. Return hiccup data." (fn [_ [type]] type)) (defn resolve-query [o] @@ -22,20 +23,86 @@ (cond (coll? child) (walk/prewalk-replace (resolve-queries env) child))) -(defn bindings->env [v] - (apply hash-map v)) +(defn symbol-afer-as? [bindings idx] + (and (pos? idx) (= :as (nth bindings (dec idx))))) +(declare destructure-assoc destructure-seq) -(defmethod parse 'let [_ [_ bindings & body]] +(defn indexed-bindings [bindings] + (into {} (map-indexed vector bindings))) + +(defn merge-seq-bindings [bindings s m idx value] + (cond + (or (= :as value) (= '_ value)) m + (symbol-afer-as? bindings idx) (assoc-in m [:data value] s) + (symbol? value) (assoc-in m [:data value] (nth s idx)) + ;; Recursive destructuring + (map? value) (errors/merge-results m (destructure-assoc value (nth s idx))) + (sequential? value) (errors/merge-results m (destructure-seq value (nth s idx))))) + +(defn bindings-size [bindings] + (let [size (count bindings)] + (if (some #{:as} bindings) + (- size 2) + size))) + +(defn destructure-seq [bindings s] + (cond + (or + (not (sequential? bindings)) + (not (every? #(or (symbol? %) (vector? %) (map? %) (= :as %)) bindings)) + (> (bindings-size bindings) (count s))) + {:errors [(errors/error ::errors/invalid-destructuring-format bindings)]} + :else + (reduce-kv #(merge-seq-bindings bindings s %1 %2 %3) {} (indexed-bindings bindings)))) + +(defn merge-assoc-bindings [s m k v] + (cond + (vector? v) (assoc-in m [:data k] (or ((first v) s) (second v))) + (symbol? k) (assoc-in m [:data k] (v s)) + (= :as k) (assoc-in m [:data v] s) + ;; Recursive destructuring + (map? k) (errors/merge-results m (destructure-assoc k (v s))) + (sequential? k) (errors/merge-results m (destructure-seq k (v s))))) + +(defn destructure-assoc [bindings s] + (cond + (or + (not (map? bindings)) + (not (every? #(or (symbol? %) (vector? %) (map? %) (= :as %)) (keys bindings)))) + {:errors [(errors/error ::errors/invalid-destructuring-format bindings)]} + :else + (reduce-kv #(merge-assoc-bindings s %1 %2 %3) {} bindings))) + +(defn destructure [bindings s] + (cond + (sequential? s) (destructure-seq bindings s) + (map? s) (destructure-assoc bindings s))) + +(defn merge-bindings [m k v] + (cond + (symbol? k) (assoc-in m [:data k] v) + (sequential? k) (errors/merge-results m (destructure-seq k v)) + (map? k) (errors/merge-results m (destructure-assoc k v)) + :else + {:errors [(errors/error ::errors/invalid-destructuring-format [k v])]})) + +(defn bindings->env [bindings] (cond (odd? (count bindings)) - {:errors [(errors/error ::errors/invalid-block bindings)]} + {:errors [(errors/error ::errors/invalid-destructuring-format bindings)]} :else - {:data - ;; TODO resolve query references only once, error if unknown - (let [m (bindings->env bindings) - child (last body)] - [let-block {:env m} child])})) + (reduce-kv merge-bindings + {} (apply hash-map bindings)))) + +(defmethod parse 'let [_ [_ bindings & body]] + (let [{:keys [data errors]} (bindings->env bindings)] + (errors/merge-errors + ;; TODO resolve query references only once, error if unknown + {:data + (let [child (last body)] + [let-block {:env data} child])} + errors))) (defn when-block [{:keys [test]} body] ;; TODO warning if test is not of boolean type diff --git a/src/pluto/reader/errors.cljc b/src/pluto/reader/errors.cljc index 037389f..136159c 100644 --- a/src/pluto/reader/errors.cljc +++ b/src/pluto/reader/errors.cljc @@ -9,6 +9,8 @@ ::invalid-property-name ::invalid-property-type ::invalid-property-value + ::missing-property-value + ::invalid-destructuring-format ::missing-keys ::unknown-reference ::unknown-component @@ -44,9 +46,14 @@ (update m :errors concat errors) m)) +(defn update-data [m data] + (if data + (update m :data merge data) + m)) + (defn merge-result [m {:keys [data errors]}] (-> m - (update :data merge data) + (update-data data) (update-errors errors))) (defn merge-results [& ms] diff --git a/src/pluto/reader/hooks.cljc b/src/pluto/reader/hooks.cljc index 37661d8..c516cfc 100644 --- a/src/pluto/reader/hooks.cljc +++ b/src/pluto/reader/hooks.cljc @@ -15,13 +15,17 @@ (= 'some.ref (reference->symbol '@views/some.ref)) ```" [ref] - (symbol (second ref))) + (when-let [s (second ref)] + (symbol s))) (defn property-value [{:keys [name]} hook] (get hook name)) (defmethod resolve-property :view [opts m def hook] - (views/parse opts (get m (reference->symbol (property-value def hook))))) + (let [o (property-value def hook)] + (if-let [s (reference->symbol o)] + (views/parse opts (get m s)) + {:errors [(errors/error ::errors/missing-property-value def)]}))) (defn resolve-property-value [f {:keys [name] :as def} hook] (if-let [o (property-value def hook)] diff --git a/test/pluto/reader/block_test.cljc b/test/pluto/reader/block_test.cljc index 960bc02..51c02f7 100644 --- a/test/pluto/reader/block_test.cljc +++ b/test/pluto/reader/block_test.cljc @@ -1,12 +1,64 @@ (ns pluto.reader.block-test + (:refer-clojure :exclude [destructure]) (:require [clojure.test :refer [is deftest]] [pluto.reader.errors :as errors] [pluto.reader.blocks :as blocks])) +(deftest destructure-seq + (is (= {:errors [{::errors/type ::errors/invalid-destructuring-format ::errors/value [1]}]} + (blocks/destructure-seq '[1] [1]))) + (is (= {:errors [{::errors/type ::errors/invalid-destructuring-format ::errors/value '[a b]}]} + (blocks/destructure-seq '[a b] [1]))) + (is (= {:data '{a 1}} (blocks/destructure-seq '[a] [1]))) + (is (= {:data '{a 1 c 3}} (blocks/destructure-seq '[a _ c] [1 2 3]))) + (is (= {} (blocks/destructure-seq '[_ _ _] [1 2 3]))) + (is (= {:data '{all [1 2 3]}} (blocks/destructure-seq '[_ _ _ :as all] [1 2 3])))) + +(deftest destructure-assoc + (is (= {:data '{a 1 b 2}} + (blocks/destructure-assoc '{a :a b :b} {:a 1 :b 2}))) + (is (= {:errors [{:pluto.reader.errors/type :pluto.reader.errors/invalid-destructuring-format, + :pluto.reader.errors/value {1 :a}}]} + (blocks/destructure-assoc '{1 :a} {:a 1}))) + (is (= {:errors [{:pluto.reader.errors/type :pluto.reader.errors/invalid-destructuring-format, + :pluto.reader.errors/value []}]} + (blocks/destructure-assoc [] {:a 1}))) + (is (= {:errors [{:pluto.reader.errors/type :pluto.reader.errors/invalid-destructuring-format, + :pluto.reader.errors/value '[a1 a2]}]} + (blocks/destructure-assoc '{[a1 a2] :a} {:a [1]}))) + (is (= {:data '{a 1 b 2 c 4 all {:a 1 :b 2 :d 3}}} + (blocks/destructure-assoc '{a :a b :b c [:c 4] :as all} {:a 1 :b 2 :d 3})))) + +(deftest destructure + (is (= {:data '{a 1}} (blocks/destructure '[a] [1]))) + (is (= {:data '{a 1 b 2}} (blocks/destructure '[a {b :b}] [1 {:b 2}]))) + (is (= {:data '{a 1 b 2 c 3}} (blocks/destructure '[a [b [c]]] [1 [2 [3]]]))) + (is (= {:data '{a 1 b 2}} (blocks/destructure '{a :a b :b} {:a 1 :b 2}))) + (is (= {:data '{a 1 b2 3}} (blocks/destructure '{a :a [_ b2] :b} {:a 1 :b [2 3]}))) + (is (= {:data '{a 1 c 3}} (blocks/destructure '{a :a {c :c} :b} {:a 1 :b {:c 3}}))) + (is (= {:data '{a1 1 a3 3 d 4 e2 6 f 7 g2 10 + all {:a [1 2 3] :b {:d 4 :e [5 6] :g [9 10]}}}} + (blocks/destructure '{[a1 _ a3] :a {d :d f [:f 7] [_ e2] :e [_ g2] :g} :b :as all} + {:a [1 2 3] :b {:d 4 :e [5 6] :g [9 10]}})))) + +(deftest bindings->env + (is (= {:data '{a 1}} (blocks/bindings->env '[a 1]))) + (is (= {:errors [{:pluto.reader.errors/type :pluto.reader.errors/invalid-destructuring-format + :pluto.reader.errors/value '[a 1 2]}]} + (blocks/bindings->env '[a 1 2]))) + (is (= {:errors [{:pluto.reader.errors/type :pluto.reader.errors/invalid-destructuring-format + :pluto.reader.errors/value '[1 2]}]} + (blocks/bindings->env '[1 2]))) + (is (= {:data '{a 1}} (blocks/bindings->env '[[a] [1]])))) + (deftest let-block (is (= {:data [blocks/let-block {:env {'s "Hello"}} 's]} (blocks/parse {} '(let [s "Hello"] s)))) (is (= {:data [blocks/let-block {:env {'s "Hello"}} ['test {} 's]]} (blocks/parse {} (list 'let ['s "Hello"] ['test {} 's])))) - (is (= {:errors [{::errors/type ::errors/invalid-block ::errors/value ['s "Hello" 1]}]} - (blocks/parse {} (list 'let ['s "Hello" 1] ['test {} 's]))))) + (is (= {:data [blocks/let-block {:env nil} + ['test {} 's]] + :errors [{::errors/type ::errors/invalid-destructuring-format ::errors/value ['s "Hello" 1]}]} + (blocks/parse {} (list 'let ['s "Hello" 1] ['test {} 's])))) + (is (= {:data [blocks/let-block {:env {'a 1}} ['test {} 's]]} + (blocks/parse {} (list 'let ['{a :a} {:a 1}] ['test {} 's]))))) diff --git a/test/pluto/reader/hooks_test.cljc b/test/pluto/reader/hooks_test.cljc index 8361cef..44037cd 100644 --- a/test/pluto/reader/hooks_test.cljc +++ b/test/pluto/reader/hooks_test.cljc @@ -18,6 +18,12 @@ (is (= {:errors [{::errors/type ::errors/invalid-property-value ::errors/value 1}]} (hooks/resolve-property {} {} {:name :keyword :type :keyword} {:keyword 1}))) + (is (= {:errors [{:pluto.reader.errors/type ::errors/missing-property-value + :pluto.reader.errors/value {:name :view :type :view}}]} + (hooks/resolve-property {:capacities {:components {'text :text}}} + {'views/id ['text {} ""]} + {:type :view :name :view} + {}))) (is (= {:data [:text {} ""]} (hooks/resolve-property {:capacities {:components {'text :text}}} {'views/id ['text {} ""]}