Moved to uniform types usage

This commit is contained in:
Julien Eluard 2018-10-07 17:17:02 +02:00
parent 229393d91e
commit ead2d95b27
No known key found for this signature in database
GPG Key ID: 6FD7DB5437FCBEF6
20 changed files with 297 additions and 486 deletions

View File

@ -57,8 +57,10 @@
(defn parse [m]
(reader/parse {:capacities {:components html/components
:queries #{:random-boolean}
:hooks {:main {:hook hook :properties {:view :view}}}
:events {:log
:hooks {:main
{:hook hook
:properties {:view :view}}}
:events {:alert
{:permissions [:read]
:value []}}}}
m))

View File

@ -4,14 +4,14 @@
:documentation "Nothing. Just see a text with dynamic random color."}
hooks/main.demo
{:view views/main}
{:view [views/main]}
views/main
(let [{name :name} properties]
[view {}
[button {:on-click [:alert {:value name}]}
"Hello"]
(let [{cond? :cond?} (query [:random-boolean])]
(let [{cond? :cond?} [:random-boolean]]
(if cond?
[text {:style {:color "green"}}
name]

View File

@ -1,11 +1,10 @@
(ns pluto.components.html
(:require [re-frame.core :as re-frame]))
(ns pluto.components.html)
(defn view [props & content]
(into [:div props] content))
(defn button [{:keys [on-click]} & content]
(into [:button {:on-click #(re-frame/dispatch on-click)}] content))
(into [:button {:on-click on-click}] content))
(defn text [props & content]
(into [:span props] content))
@ -14,9 +13,9 @@
:value view
:description ""
:examples []}
'button {:properties {}
'button {:properties {:on-click :event}
:value button
:examples []}
'text {:properties {:on-click :event}
'text {:properties {}
:value text
:examples []}})

View File

@ -10,12 +10,13 @@
# Activate
* based on hooks, inject views / trigger events"
(:refer-clojure :exclude [read])
(:require [clojure.set :as set]
[clojure.spec.alpha :as spec]
(:require [clojure.set :as set]
[clojure.spec.alpha :as spec]
[clojure.tools.reader.edn :as edn]
[pluto.reader.errors :as errors]
[pluto.reader.hooks :as hooks]
[pluto.utils :as utils]))
[pluto.reader.errors :as errors]
[pluto.reader.hooks :as hooks]
pluto.reader.views
[pluto.utils :as utils]))
(defn reader-error [ex]
(errors/error ::errors/reader-error (:ex-kind (ex-data ex))
@ -99,16 +100,16 @@
(defn ^:export parse
"Parse an extension definition map as encapsulated in :data key of the map returned by read.
`opts` is a map defining:
`ctx` is a map defining:
* `capacities` a map of valid supported capacities (hooks, queries, events)
Returns a map defining:
* :data a map of meta and parsed hooks
* :permissions a vector of required permissions
* :errors a vector of errors maps triggered during parse"
[opts m]
(let [errors (validate opts m)]
[ctx m]
(let [errors (validate ctx m)]
(errors/merge-results
(parse-meta ('meta m))
(hooks/parse opts m)
(hooks/parse ctx m)
{:errors errors})))

View File

@ -1,51 +1,47 @@
(ns pluto.reader.blocks
(:require [clojure.walk :as walk]
[re-frame.core :as re-frame]
[pluto.reader.destructuring :as destructuring]
[pluto.reader.errors :as errors]
[pluto.reader.reference :as reference]
[pluto.reader.permissions :as permissions]
[re-frame.core :as re-frame]))
[pluto.reader.types :as types]))
(defmulti parse
"Parse a block element. Return hiccup data."
(fn [_ [type]] type))
(defn resolve-query [[_ [sub-keyword sub-args :as sub]]]
(when-let [o (re-frame/subscribe sub)]
@o))
(defn resolve-query [query]
(let [{:keys [data]} (types/resolve {} {} :query query)]
(data)))
(defn- query? [binding-value]
(and (list? binding-value)
(= 'query (first binding-value))))
(vector? binding-value))
(defn resolve-binding-value [v]
;; TODO resolve query statically
(cond
(query? v) (resolve-query v)
(not (list? v)) v))
(defn resolve-binding-key [k v]
(cond
(symbol? k) k
:else (:data (destructuring/destructure k v))))
(if (symbol? k)
k
;; TODO handle errors
(:data (destructuring/destructure k v))))
(defn assoc-binding [m k v]
(let [resolved-value (resolve-binding-value v)]
(let [o (resolve-binding-key k resolved-value)]
(cond
(symbol? o)
(if (symbol? o)
(assoc m o resolved-value)
:else
(merge m o)))))
(defn resolve-bindings [env]
(reduce-kv assoc-binding {} env))
(defn let-block [{:keys [env]} child]
(cond
(coll? child) (walk/prewalk-replace (resolve-bindings env) child)))
(coll? child) (walk/prewalk-replace (reduce-kv assoc-binding {} env) child)))
(defn properties? [o]
(and (reference/reference? o) (= 'properties (reference/reference->symbol o))))
(= 'properties o))
(defn inject-new-bindings [m v]
(cond
@ -64,40 +60,20 @@
{:errors [(errors/error ::errors/invalid-destructuring-format [k av])]})))))
(defn bindings->env [bindings]
(cond
(odd? (count bindings))
(if (odd? (count bindings))
{:errors [(errors/error ::errors/invalid-destructuring-format bindings)]}
:else
(reduce-kv merge-bindings {} (apply hash-map bindings))))
(defn- restrict-get-in [path {:keys [read]}]
(when-not (permissions/allowed-path? path read)
(errors/error ::errors/forbidden-read-path path)))
(defn- restrict-queries
"Parse time enforcement of valid queries, checks that query
is exposed via host platform + enforces valid query vectors for
`:get-in` query."
[{:keys [permissions queries]} env]
(keep (fn [[_ env-value]]
(when (query? env-value)
(let [[_ [sub-name sub-args]] env-value]
(if (not (contains? queries sub-name))
(errors/error ::errors/query-not-exposed sub-name)
(when (= :get-in sub-name)
(restrict-get-in sub-args permissions))))))
env))
(defmethod parse 'let [{:keys [capacities]} [_ bindings & body]]
(let [{:keys [data errors]} (bindings->env bindings)
query-errors (restrict-queries capacities data)]
(errors/merge-errors
(let [{:keys [data errors]} (bindings->env bindings)]
;; TODO fail if some symbol are not defined in the env
;; TODO resolve query references only once, error if unknown
(merge
{:data
(let [child (last body)]
[let-block {:env data} child])}
(concat errors query-errors))))
(when errors
{:errors errors}))))
(defn when-block [{:keys [test]} body]
;; TODO warning if test is not of boolean type

View File

@ -1,20 +1,22 @@
(ns pluto.reader.destructuring
(:refer-clojure :exclude [destructure])
(:require [clojure.walk :as walk]
[pluto.reader.errors :as errors]
[pluto.reader.reference :as reference]
[pluto.reader.permissions :as permissions]
[re-frame.core :as re-frame]))
(defn symbol-afer-as? [bindings idx]
(and (pos? idx) (= :as (nth bindings (dec idx)))))
(:require [pluto.reader.errors :as errors]))
(declare destructure-assoc destructure-seq)
(defn indexed-bindings [bindings]
(into {} (map-indexed vector bindings)))
(defn- valid-bindings-form? [o]
(or (symbol? o) (vector? o) (map? o) (= :as o)))
(defn merge-seq-bindings [bindings s m idx value]
(defn- seq-bindings-size [bindings]
(let [size (count bindings)]
(if (some #{:as} bindings)
(- size 2)
size)))
(defn- symbol-afer-as? [bindings idx]
(and (pos? idx) (= :as (nth bindings (dec idx)))))
(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)
@ -23,26 +25,17 @@
(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 seq-bindings-size [bindings]
(let [size (count bindings)]
(if (some #{:as} bindings)
(- size 2)
size)))
(defn valid-bindings-form? [o]
(or (symbol? o) (vector? o) (map? o) (= :as o)))
(defn- valid-seq-format? [bindings s]
(and (sequential? bindings)
(every? valid-bindings-form? bindings)
(<= (seq-bindings-size bindings) (count s))))
(defn destructure-seq [bindings s]
(cond
(or
(not (sequential? bindings))
(not (every? valid-bindings-form? bindings))
(> (seq-bindings-size bindings) (count s)))
{:errors [(errors/error ::errors/invalid-destructuring-format {:type :sequential :data bindings})]}
:else
(reduce-kv #(merge-seq-bindings bindings s %1 %2 %3) {} (indexed-bindings bindings))))
(if (valid-seq-format? bindings s)
(reduce-kv #(merge-seq-bindings bindings s %1 %2 %3) {} (into {} (map-indexed vector bindings)))
{:errors [(errors/error ::errors/invalid-destructuring-format {:type :sequential :data bindings})]}))
(defn merge-assoc-bindings [s m k v]
(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))
@ -51,16 +44,19 @@
(map? k) (errors/merge-results m (destructure-assoc k (v s)))
(sequential? k) (errors/merge-results m (destructure-seq k (v s)))))
(defn- valid-assoc-format? [bindings]
(and (map? bindings)
(every? valid-bindings-form? (keys bindings))))
(defn destructure-assoc [bindings s]
(cond
(or
(not (map? bindings))
(not (every? valid-bindings-form? (keys bindings))))
{:errors [(errors/error ::errors/invalid-destructuring-format {:type :assoc :data bindings})]}
:else
(reduce-kv #(merge-assoc-bindings s %1 %2 %3) {} bindings)))
(if (valid-assoc-format? bindings)
(reduce-kv #(merge-assoc-bindings s %1 %2 %3) {} bindings)
{:errors [(errors/error ::errors/invalid-destructuring-format {:type :assoc :data bindings})]}))
(defn destructure
"Given a pattern and an associated data structure, return a map of either:
* :data, a map of extracted symbol / value pairs
* :errors, a vector of errors encountered during the destructuring"
[bindings s]
(cond
(sequential? s) (destructure-seq bindings s)

View File

@ -12,20 +12,17 @@
::invalid-type-value
::invalid-sequential-type
::invalid-assoc-type
::missing-property-name
::missing-property-value
::invalid-reference
::invalid-destructuring-format
::missing-keys
::unknown-reference
::unknown-component
::unknown-component-property
::invalid-view
::invalid-block
::unsupported-test-type
::forbidden-read-path
::forbidden-write-path
::query-not-exposed
::invalid-event-handler
::event-not-exposed})
::query-not-exposed})
(spec/def ::value any?)

View File

@ -1,104 +1,13 @@
(ns pluto.reader.hooks
(:require [clojure.string :as string]
[clojure.set :as set]
[pluto.reader.destructuring :as destructuring]
[pluto.reader.errors :as errors]
[pluto.reader.reference :as reference]
[pluto.reader.views :as views]))
(:require [clojure.string :as string]
[pluto.reader.errors :as errors]
[pluto.reader.types :as types]))
(defprotocol Hook
"Encapsulate hook lifecycle."
(hook-in [this id properties cofx] "Hook it into host app.")
(unhook [this id properties cofx] "Remove extension hook from app."))
(defmulti resolve-property
(fn [{:keys [type]} _ _ _]
(cond
(keyword? type) type
(:one-of type) :set
(set? type) :subset
(map? type) :map
(vector? type) :vector)))
(defn inject-properties [m properties]
(if-let [ps (get-in m [:env 'properties])]
(let [{:keys [data errors]} (destructuring/destructure ps properties)]
(errors/merge-errors
{:data
(-> (update m :env dissoc 'properties)
(update :env merge data))}
errors))
{:data m}))
(defn hiccup-with-properties [h properties]
(if (vector? h)
(let [[tag & properties-children] h
[props children] (views/resolve-properties-children properties-children)
{:keys [data]} (when properties
(inject-properties props properties))]
(apply conj (if data [tag data] [tag])
(map #(hiccup-with-properties % properties) children)))
h))
(defmethod resolve-property :view [def hook opts m]
(let [{:keys [data] :as m} (reference/resolve m def hook)
{:keys [data errors]} (views/parse opts data)]
;; TODO Might fail at runtime if destructuring is incorrect
(errors/merge-errors (when data {:data (fn [o] (hiccup-with-properties data o))})
(concat errors (:errors m)))))
(defn- resolve-capacities-value [key error-key capacities {:keys [name optional?]} hook]
(if-let [value (get hook name)]
(if-let [component ((get capacities key) value)]
{:data component}
{:errors [(errors/error error-key value)]})
(when-not optional?
{:errors [(errors/error ::errors/invalid-type-name name)]})))
(defmethod resolve-property :component [def hook {:keys [capacities]} _]
(resolve-capacities-value :components ::errors/unknown-component capacities def hook))
(defmethod resolve-property :event [def hook {:keys [capacities]} m]
(resolve-capacities-value :events ::errors/event-not-exposed capacities def hook))
(defmethod resolve-property :query [def hook {:keys [capacities]} _]
(resolve-capacities-value :queries ::errors/query-not-exposed capacities def hook))
(defn- resolve-property-value [f {:keys [name optional?]} hook]
(if-let [o (get hook name)]
(if (f o)
{:data o}
{:errors [(errors/error ::errors/invalid-type-value o)]})
(when-not optional?
{:errors [(errors/error ::errors/invalid-type-name name)]})))
(defmethod resolve-property :string [def hook _ _]
(resolve-property-value string? def hook))
(defmethod resolve-property :keyword [def hook _ _]
(resolve-property-value keyword? def hook))
(defmethod resolve-property :set [def hook _ _]
(resolve-property-value (-> def :type :one-of) def hook))
(defmethod resolve-property :subset [def hook _ _]
(resolve-property-value #(set/subset? % (:type def)) def hook))
(declare parse-properties)
(defn map->properties [m]
(reduce-kv #(conj %1 {:name %2 :type %3}) [] m))
(defmethod resolve-property :map [def hook opts m]
(parse-properties (map->properties (:type def)) ((:name def) hook) opts m))
(defmethod resolve-property :vector [def hook opts m]
(let [props (map->properties (first (:type def)))]
(apply errors/merge-results-with #(conj (vec %1) %2) (map #(parse-properties props % opts m) ((:name def) hook)))))
(defmethod resolve-property :default [{:keys [type]} _ _ _]
{:errors [(errors/error ::errors/invalid-type type)]})
(defn hook? [s]
(= "hooks" (namespace s)))
@ -111,35 +20,20 @@
(defn root-id [s]
(keyword (first (string/split (name s) #"\."))))
(defn properties [opts s]
(reduce-kv #(conj %1 {:name %2 :type %3}) []
(get-in opts [:capacities :hooks s :properties])))
(defn parse-hook [ctx ext hook v]
(types/resolve ctx ext (:properties hook) v))
(defn normalize-property [{:keys [name] :as prop}]
(let [normalized-name (keyword (string/replace (clojure.core/name name) "?" ""))]
(assoc prop
:name normalized-name
:optional? (not= name normalized-name))))
(defn parse-properties [props v opts m]
(reduce #(let [{:keys [data errors]} (resolve-property (normalize-property %2) v opts m)]
(errors/merge-errors (if data (assoc-in %1 [:data (:name (normalize-property %2))] data) %1) errors))
{} props))
(defn parse-hook [hook v opts m]
(parse-properties (map->properties (:properties hook)) v opts m))
(defn parse [opts m]
(defn parse [ctx ext]
(reduce-kv (fn [acc hook-key data]
(let [hook-id (local-id hook-key)
hook-root (root-id hook-key)
hook (get-in opts [:capacities :hooks hook-root])
{:keys [data errors]} (parse-hook hook data opts m)]
hook (get-in ctx [:capacities :hooks hook-root])
{:keys [data errors] :as m} (parse-hook ctx ext hook data)]
(errors/merge-errors
(-> acc
(assoc-in [:data :hooks hook-root hook-id :parsed] data)
(assoc-in [:data :hooks hook-root hook-id :hook-ref] hook))
errors)))
{}
(select-keys m (hooks m))))
(select-keys ext (hooks ext))))

View File

@ -5,44 +5,30 @@
(defn reference?
"Return true if argument is a reference"
[o]
(symbol? o))
(and (vector? o)
(let [c (count o)]
(or (= 1 c) (= 2 c)))
(symbol? (first o))))
(defn reference->symbol
"Return the symbol pointed by the reference
```clojure
(= 'some.ref (reference->name 'views/some.ref))
(= 'some.ref (reference->symbol ['views/some.ref]))
```"
[o]
(when (reference? o)
o))
(def ns->type {"views" :view "queries" :query "events" :event})
(defn reference->type
"Return the type of a reference
```clojure
(= :view (reference->type 'views/some.ref))
```"
[o]
(when (reference? o)
(when-let [ns (namespace (reference->symbol o))]
(get ns->type ns))))
(first o)))
(defn resolve
"Resolve a reference defined by a hook
```clojure
(= {:data \"view\"} (resolve {'views/id \"view\"} {:name :view :type :view} {:view 'views/id}))
(= {:data \"view\"} (resolve {'views/id \"view\"} ['views/id]))
```"
[m {:keys [name type optional?]} hook]
(let [ref (get hook name)]
(if ref
(if (= type (reference->type ref))
(if-let [o (get m (reference->symbol ref))]
{:data o}
{:errors [(errors/error ::errors/missing-property-value name)]})
{:errors [(errors/error ::errors/invalid-type type)]})
(when-not optional?
{:errors [(errors/error ::errors/missing-property-name type)]}))))
[ext value]
(if-let [s (reference->symbol value)]
(if-let [o (get ext s)]
{:data o}
{:errors [(errors/error ::errors/unknown-reference {:value s})]})
{:errors [(errors/error ::errors/invalid-reference {:value value})]}))

View File

@ -4,6 +4,7 @@
(:refer-clojure :exclude [resolve])
(:require [clojure.string :as string]
[clojure.set :as set]
[re-frame.core :as re-frame]
[pluto.reader.errors :as errors]))
(defmulti resolve
@ -11,7 +12,7 @@
Returns a map of either:
* data with the resolved data
* errors encapsulating all errors generated during resolution"
(fn [ctx type value]
(fn [ctx ext type value]
(cond
(keyword? type) type
(:one-of type) :one-of
@ -22,33 +23,29 @@
(defn invalid-type-value [type value]
(errors/error ::errors/invalid-type-value {:type type :value value}))
(defmethod resolve :string [_ _ value]
(defmethod resolve :string [_ _ _ value]
(if (string? value)
{:data value}
{:errors [(invalid-type-value :string value)]}))
(defmethod resolve :keyword [_ _ value]
(defmethod resolve :keyword [_ _ _ value]
(if (keyword? value)
{:data value}
{:errors [(invalid-type-value :keyword value)]}))
(defmethod resolve :subset [_ type value]
(println ">>" value type (not (nil? value)) (set/subset? value type))
(defmethod resolve :subset [_ _ type value]
(if (and (not (nil? value)) (set/subset? value type))
{:data value}
{:errors [(invalid-type-value :subset value)]}))
(defmethod resolve :one-of [_ {:keys [one-of]} value]
(defmethod resolve :one-of [_ _ {:keys [one-of]} value]
(if-let [o (one-of value)]
{:data o}
{:errors [(invalid-type-value :one-of value)]}))
(defmethod resolve :default [_ value _]
{:errors [(errors/error ::errors/invalid-type {:value value})]})
(defmethod resolve :sequence [ctx type value]
(defmethod resolve :sequence [ctx ext type value]
(if (and (vector? type) (= 1 (count type)) (map? (first type)))
(apply errors/merge-results-with #(conj (vec %1) %2) (map #(resolve ctx (first type) %) value))
(apply errors/merge-results-with #(conj (vec %1) %2) (map #(resolve ctx ext (first type) %) value))
{:errors [(errors/error ::errors/invalid-sequential-type {:type type :value value})]}))
(def ^:private sentinel ::sentinel)
@ -59,18 +56,28 @@
:name normalized-name
:optional? (not= name normalized-name)}))
(defn- resolve-property [ctx m {:keys [name optional? value]} type]
(defn- resolve-property [ctx ext m {:keys [name optional? value]} type]
(if (not= sentinel value)
(let [{:keys [data errors]} (resolve ctx type value)]
(let [{:keys [data errors]} (resolve ctx ext type value)]
(errors/merge-errors
(if data (assoc-in m [:data name] data) m)
errors))
(if-not optional?
{:errors [(errors/error ::errors/invalid-type-value {:type type :value value})]}
{:data {}})))
(if optional?
(update m :data #(if (empty? %) {} %))
(assoc m :errors [(errors/error ::errors/invalid-type-name name)]))))
(defmethod resolve :assoc [ctx type value]
(defmethod resolve :assoc [ctx ext type value]
(if (map? type)
(reduce-kv #(resolve-property ctx %1 (property %2 value) %3)
(reduce-kv #(resolve-property ctx ext %1 (property %2 value) %3)
{} type)
{:errors [(errors/error ::errors/invalid-assoc-type {:type type :value value})]}))
(defmethod resolve :event [_ _ _ value]
{:data #(re-frame/dispatch value)})
(defmethod resolve :query [ctx _ _ value]
{:data #(when-let [o (re-frame/subscribe value)]
@o)})
(defmethod resolve :default [_ _ type value]
{:errors [(errors/error ::errors/invalid-type (merge {:type type} (when value {:value value})))]})

View File

@ -1,10 +1,12 @@
(ns pluto.reader.views
(:require [clojure.spec.alpha :as spec]
[pluto.reader.blocks :as blocks]
[pluto.reader.errors :as errors]
[pluto.reader.permissions :as permissions]
[pluto.utils :as utils]
[re-frame.core :as re-frame]))
(:require [clojure.spec.alpha :as spec]
[pluto.reader.blocks :as blocks]
[pluto.reader.destructuring :as destructuring]
[pluto.reader.errors :as errors]
[pluto.reader.permissions :as permissions]
[pluto.reader.reference :as reference]
[pluto.reader.types :as types]
[pluto.utils :as utils]))
;; TODO Distinguish views (can contain blocks, symbols) validation
;; from hiccup validation (view after parsing) that are pure hiccup
@ -22,63 +24,56 @@
:attrs map?
:children (spec/* ::form)))
;; TODO add all possible event handlers
(def ^:private event-handler->selector
{:on-press (constantly true)
:on-change #(.-text (.-nativeEvent %))})
(declare parse)
(defn parse-hiccup-children [opts children]
(reduce #(let [{:keys [data errors]} (parse opts %2)]
(defn parse-hiccup-children [ctx ext children]
(reduce #(let [{:keys [data errors]} (parse ctx ext %2)]
(errors/merge-errors (update %1 :data conj data)
errors))
{:data []} children))
(defn resolve-component [{:keys [components]} o]
(defn component? [o]
(symbol? o))
(defn- resolve-component [ctx o]
(cond
(fn? o) o
(symbol? o) (:value (get components o))))
(symbol? o) (get-in ctx [:capacities :components o :value])))
(defn- event? [prop-value]
(and (list? prop-value)
(= 'event (first prop-value))))
(defmulti resolve-default-component-properties (fn [property value] property))
(defn- create-event-handler [re-frame-event selector]
(fn [event-value]
(re-frame/dispatch (conj re-frame-event (selector event-value)))))
(defmethod resolve-default-component-properties :style [_ value]
{:data value})
(defn- resolve-component-properties [component {:keys [permissions events] :as capacities} properties]
; (println ":" properties (get-in capacities [:components component :properties]))
; TODO validate component properties based on capacities
(reduce (fn [acc [k v]]
(if (contains? event-handler->selector k)
(if (not (event? v))
(update acc :errors conj (errors/error ::errors/invalid-event-handler v))
(let [[_ [event-name event-args] :as re-frame-event] v]
(cond
(defmethod resolve-default-component-properties :default [_ value]
nil)
(not (contains? events event-name))
(update acc :errors conj (errors/error ::errors/event-not-exposed event-name))
(defn- resolve-component-property [ctx ext component k v]
(or (resolve-default-component-properties k v)
(if-let [type (k (get-in ctx [:capacities :components component :properties]))]
(types/resolve ctx ext type v)
{:errors [(errors/error ::errors/unknown-component-property {:component component :property k})]})))
(not (permissions/allowed-path? event-args (:write permissions)))
(update acc :errors conj (errors/error ::errors/forbidden-write-path event-args))
(defn- resolve-property [ctx ext component k v]
(if (component? component)
(resolve-component-property ctx ext component k v)
{:data v}))
:else
(update acc :data assoc k (create-event-handler re-frame-event
(get event-handler->selector k))))))
(update acc :data assoc k v)))
{:data {}
:errors []}
properties))
(defn- resolve-component-properties [ctx ext component properties]
(reduce-kv (fn [acc k v]
(let [{:keys [data errors]} (resolve-property ctx ext component k v)]
(-> (update acc :data assoc k data))))
{:data {}
:errors []}
properties))
(defn resolve-properties-children [[properties? & children]]
(defn- resolve-properties-children [[properties? & children]]
[(and (map? properties?) properties?)
(if (map? properties?)
children
(cons properties? children))])
(defn parse-hiccup-element [{:keys [capacities] :as opts} o]
(defn- parse-hiccup-element [ctx ext o]
(let [explain (spec/explain-data ::form o)]
(cond
;; TODO Validate views, not hiccup
@ -90,10 +85,10 @@
(vector? o)
(let [[element & properties-children] o
[properties children] (resolve-properties-children properties-children)
component (resolve-component capacities element)
component (resolve-component ctx element)
{:keys [data errors]} (when properties
(resolve-component-properties element capacities properties))]
(cond-> (let [m (parse-hiccup-children opts children)]
(resolve-component-properties ctx ext element properties))]
(cond-> (let [m (parse-hiccup-children ctx ext children)]
;; 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 (if data [(or component element) data]
@ -102,12 +97,42 @@
(nil? component) (errors/accumulate-errors [(errors/error ::errors/unknown-component element)])
(seq errors) (errors/accumulate-errors errors))))))
(defn parse [opts o]
(defn parse [ctx ext o]
(cond
(list? o)
(let [{:keys [data errors]} (blocks/parse opts o)]
(let [{:keys [data errors]} (blocks/parse ctx o)]
(if errors
{:errors errors}
(parse opts data)))
(parse ctx ext data)))
:else
(parse-hiccup-element opts o)))
(parse-hiccup-element ctx ext o)))
(defn- inject-properties [m properties]
(if-let [ps (get-in m [:env 'properties])]
(let [{:keys [data errors]} (destructuring/destructure ps properties)]
(errors/merge-errors
{:data
(-> m
(update :env dissoc 'properties)
(update :env merge data))}
errors))
{:data m}))
(defn- hiccup-with-properties [h properties]
(if (vector? h)
(let [[tag & properties-children] h
[props children] (resolve-properties-children properties-children)
{:keys [data]} (when properties
(inject-properties props properties))]
(apply conj (if data [tag data] [tag])
(map #(hiccup-with-properties % properties) children)))
h))
(defmethod types/resolve :view [ctx ext type value]
(let [{:keys [data errors]} (reference/resolve ext value)]
(if data
(let [{:keys [data errors]} (parse ctx ext data)]
;; TODO Might fail at runtime if destructuring is incorrect
(errors/merge-errors (when data {:data (fn [o] (hiccup-with-properties data o))})
(concat errors (:errors ext))))
{:errors errors})))

View File

@ -1,6 +1,6 @@
(ns pluto.storage.ipfs
(:require [clojure.string :as string]
[pluto.storage :as storage]))
[pluto.storage :as storage]))
(defn result [xhr]
(let [status (.-status xhr)]

View File

@ -1,6 +1,6 @@
(ns pluto.storages
(:require [clojure.string :as string]
[pluto.storage :as storage]
(:require [clojure.string :as string]
[pluto.storage :as storage]
[pluto.storage.http :as http]
[pluto.storage.gist :as gist]
[pluto.storage.ipfs :as ipfs]))

View File

@ -11,19 +11,19 @@
(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]]))))
(is (= {:data '{x 1}} (blocks/bindings->env '[{x :x} {:x 1}]))))
(deftest let-block
(testing "parse"
(is (= {:data [blocks/let-block {:env {'s "Hello"}} 's]}
(blocks/parse {} '(let [s "Hello"] s))))
(is (= {:data [blocks/let-block {:env '{{a :a} (query [:aa])}} 'a]}
(blocks/parse {:capacities {:queries #{:aa}}} '(let [{a :a} (query [:aa])] a))))
(is (= {:data [blocks/let-block {:env '{{a :a} [:aa]}} 'a]}
(blocks/parse {:capacities {:queries #{:aa}}} '(let [{a :a} [:aa]] a))))
(is (= {:data [blocks/let-block {:env '{a {:b 1} b 1}} 'b]}
(blocks/parse {} '(let [{a :a} {:a {:b 1}} {b :b} a] b))))
(is (= {:data [blocks/let-block {:env '{x 1 {a :a} (query [:aa 1])}} 'a]}
(blocks/parse {:capacities {:queries #{:aa}}} '(let [x 1 {a :a} (query [:aa x])] a))))
(is (= {:data [blocks/let-block {:env '{x 1 {a :a} [:aa {:x x}]}} 'a]}
(blocks/parse {:capacities {:queries #{:aa}}} '(let [x 1 {a :a} [:aa {:x x}]] a))))
(is (= {:data [blocks/let-block {:env {'s "Hello"}} ['test {} 's]]}
(blocks/parse {} (list 'let ['s "Hello"] ['test {} 's]))))
@ -37,4 +37,4 @@
(deftest let-block-resolution
(is (= ['test {} 1] (blocks/let-block {:env {'a 1}} ['test {} 'a])))
#_
(is (= ['test {} 1] (blocks/let-block {:env '{{a :a} (query [:aa])}} '[test {} a]))))
(is (= ['test {} 1] (blocks/let-block {:env '{{a :a} [:aa]}} '[test {} a]))))

View File

@ -1,8 +1,8 @@
(ns pluto.reader.destructuring-test
(:refer-clojure :exclude [destructure])
(:require [clojure.test :refer [is deftest testing]]
[pluto.reader.errors :as errors]
[pluto.reader.destructuring :as destructuring]))
(:require [clojure.test :refer [is deftest]]
[pluto.reader.destructuring :as destructuring]
[pluto.reader.errors :as errors]))
(deftest destructure-seq
(is (= {:errors [{::errors/type ::errors/invalid-destructuring-format ::errors/value {:data [1] :type :sequential}}]}

View File

@ -2,83 +2,8 @@
(:require [clojure.test :refer [is deftest testing]]
[pluto.reader.blocks :as blocks]
[pluto.reader.errors :as errors]
[pluto.reader.hooks :as hooks]))
#_
(deftest resolve-property
(testing "View"
(is (= {:errors [{:pluto.reader.errors/type ::errors/missing-property-name
:pluto.reader.errors/value :view}]}
(hooks/resolve-property {:type :view :name :view}
{}
{:capacities {:components {'text :text}}}
{'views/id ['text {} ""]})))
(is (= [:text {} ""]
((:data (hooks/resolve-property {:type :view :name :view}
{:view 'views/id}
{:capacities {:components {'text :text}}}
{'views/id ['text {} ""]})) {})))
(is (= [blocks/let-block {:env {'value "test"}}
[:text {} 'value]]
((:data (hooks/resolve-property {:type :view :name :view}
{:view 'views/id}
{:capacities {:components {'text :text}}}
{'views/id '(let [{value :value} properties] [text {} value])})) {:value "test"}))))
(testing "Component"
(is (= {:errors [{:pluto.reader.errors/type ::errors/invalid-type-name
:pluto.reader.errors/value :component}]}
(hooks/resolve-property {:type :component :name :component}
{}
{:capacities {}}
{})))
(is (= {:errors [{:pluto.reader.errors/type ::errors/unknown-component
:pluto.reader.errors/value 'selector}]}
(hooks/resolve-property {:type :component :name :component}
{:component 'selector}
{:capacities {:components {}}}
{})))
(is (= {:data "selector"}
(hooks/resolve-property {:type :component :name :component}
{:component 'selector}
{:capacities {:components {'selector "selector"}}}
{}))))
(testing "Event"
(is (= {:errors [{:pluto.reader.errors/type ::errors/invalid-type-name
:pluto.reader.errors/value :event}]}
(hooks/resolve-property {:type :event :name :event}
{}
{:capacities {}}
{})))
(is (= {:errors [{:pluto.reader.errors/type ::errors/event-not-exposed
:pluto.reader.errors/value :set-in}]}
(hooks/resolve-property {:type :event :name :event}
{:event :set-in}
{:capacities {:events #{}}}
{})))
(is (= {:data :set-in}
(hooks/resolve-property {:type :event :name :event}
{:event :set-in}
{:capacities {:events #{:set-in}}}
{}))))
(testing "Query"
(is (= {:errors [{:pluto.reader.errors/type ::errors/invalid-type-name
:pluto.reader.errors/value :query}]}
(hooks/resolve-property {:type :query :name :query}
{}
{:capacities {}}
{})))
(is (= {:errors [{:pluto.reader.errors/type ::errors/query-not-exposed
:pluto.reader.errors/value :get-in}]}
(hooks/resolve-property {:type :query :name :query}
{:query :get-in}
{:capacities {:queries #{}}}
{})))
(is (= {:data :get-in}
(hooks/resolve-property {:type :query :name :query}
{:query :get-in}
{:capacities {:queries #{:get-in}}}
{})))))
[pluto.reader.hooks :as hooks]
[pluto.reader.views :as views]))
(defn- hooks [properties]
{:main {:properties properties}})
@ -86,9 +11,9 @@
(deftest parse
(is (= [:text {} ""]
((get-in (hooks/parse {:capacities {:hooks (hooks {:view :view})
:components {'text :text}}}
:components {'text {:value :text}}}}
{'views/main ['text {} ""]
'hooks/main.a {:view 'views/main}})
'hooks/main.a {:view ['views/main]}})
[:data :hooks :main :a :parsed :view]) {})))
(let [app-hooks (hooks {:name :string
:id :keyword})]
@ -100,16 +25,16 @@
:id :keyword}}))))
(testing "Optional property"
(let [app-hooks (hooks {:name :string :id :keyword})]
(is (= {:data {:hooks {:main {:a {:parsed {:name "name"}
(is (= {:data {:hooks {:main {:a {:parsed {:name "name"}
:hook-ref (:main app-hooks)}}}}
:errors [{::errors/type ::errors/invalid-type-name
::errors/value :id}]}
(hooks/parse {:capacities {:hooks app-hooks}}
{'hooks/main.a {:name "name"}}))))
{'hooks/main.a {:name "name"}}))))
(is (= {:name "name"}
(get-in (hooks/parse {:capacities {:hooks (hooks {:name :string :id? :keyword})}}
{'hooks/main.b {:name "name"}})
[:data :hooks :main :b :parsed])))
{'hooks/main.b {:name "name"}})
[:data :hooks :main :b :parsed])))
(is (= {:name "name"
:id :keyword}
(get-in (hooks/parse {:capacities {:hooks (hooks {:name :string :id? :keyword})}}

View File

@ -1,36 +1,28 @@
(ns pluto.reader.reference-test
(:refer-clojure :exclude [resolve])
(:require [clojure.test :refer [is deftest testing]]
(:require [clojure.test :refer [is deftest]]
[pluto.reader.errors :as errors]
[pluto.reader.reference :as reference]))
(deftest reference?
(is (false? (reference/reference? "")))
(is (true? (reference/reference? 'test)))
(is (true? (reference/reference? 'views/id))))
(is (false? (reference/reference? 'test)))
(is (true? (reference/reference? ['views/id])))
(is (true? (reference/reference? ['views/id {}])))
(is (false? (reference/reference? ['views/id {} {}])))
(is (false? (reference/reference? ["views/id" {}]))))
(deftest reference->symbol
(is (= nil (reference/reference->symbol "")))
(is (= 'test (reference/reference->symbol 'test)))
(is (= 'views/id (reference/reference->symbol 'views/id))))
(deftest reference->type
(is (= nil (reference/reference->type "")))
(is (= nil (reference/reference->type 'test)))
(is (= :view (reference/reference->type 'views/id))))
(is (= 'test (reference/reference->symbol ['test])))
(is (= 'views/id (reference/reference->symbol ['views/id {}]))))
(deftest resolve
(is (= {:errors [{::errors/type ::errors/missing-property-value
::errors/value :view}]}
(reference/resolve {} {:name :view :type :view} {:view 'views/id})))
(is (= {:errors [{::errors/type ::errors/invalid-type
::errors/value :view}]}
(reference/resolve {'events/id "events"} {:name :view :type :view} {:view 'events/id})))
(is (= {:errors [{::errors/type ::errors/missing-property-name
::errors/value nil}]}
(reference/resolve {'events/id "events"} {} {:event 'events/id})))
(is (= {:errors [{::errors/type ::errors/missing-property-name
::errors/value :view}]}
(reference/resolve {'views/id "view"} {:name :view :type :view} {})))
(is (= {:errors [{::errors/type ::errors/unknown-reference
::errors/value {:value 'views/id}}]}
(reference/resolve {} ['views/id])))
(is (= {:errors [{::errors/type ::errors/invalid-reference
::errors/value {:value ""}}]}
(reference/resolve {'views/id "view"} "")))
(is (= {:data "view"}
(reference/resolve {'views/id "view"} {:name :view :type :view} {:view 'views/id}))))
(reference/resolve {'views/id "view"} ['views/id]))))

View File

@ -6,87 +6,87 @@
(deftest resolve-primitive
(is (= {:errors [{::errors/type ::errors/invalid-type
::errors/value {:value :unknown}}]}
(types/resolve {} :unknown nil)))
::errors/value {:type :unknown}}]}
(types/resolve {} {} :unknown nil)))
(testing "String"
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :string :value nil}}]}
(types/resolve {} :string nil)))
(types/resolve {} {} :string nil)))
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :string :value :value}}]}
(types/resolve {} :string :value)))
(types/resolve {} {} :string :value)))
(is (= {:data "value"}
(types/resolve {} :string "value"))))
(types/resolve {} {} :string "value"))))
(testing "Keyword"
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :keyword :value nil}}]}
(types/resolve {} :keyword nil)))
(types/resolve {} {} :keyword nil)))
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :keyword :value "value"}}]}
(types/resolve {} :keyword "value")))
(types/resolve {} {} :keyword "value")))
(is (= {:data :value}
(types/resolve {} :keyword :value))))
(types/resolve {} {} :keyword :value))))
(testing "Subset"
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :subset :value nil}}]}
(types/resolve {} #{"a" "b" "c"} nil)))
(types/resolve {} {} #{"a" "b" "c"} nil)))
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :subset :value "value"}}]}
(types/resolve {} #{"a" "b" "c"} "value")))
(types/resolve {} {} #{"a" "b" "c"} "value")))
(is (= {:data #{"a"}}
(types/resolve {} #{"a" "b" "c"} #{"a"}))))
(types/resolve {} {} #{"a" "b" "c"} #{"a"}))))
(testing "One of"
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :one-of :value nil}}]}
(types/resolve {} {:one-of #{:one :two :three}} nil)))
(types/resolve {} {} {:one-of #{:one :two :three}} nil)))
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :one-of :value :for}}]}
(types/resolve {} {:one-of #{:one :two :three}} :for)))
(types/resolve {} {} {:one-of #{:one :two :three}} :for)))
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :one-of :value "one"}}]}
(types/resolve {} {:one-of #{:one :two :three}} "one")))
(types/resolve {} {} {:one-of #{:one :two :three}} "one")))
(is (= {:data :one}
(types/resolve {} {:one-of #{:one :two :three}} :one)))))
(types/resolve {} {} {:one-of #{:one :two :three}} :one)))))
(deftest resolve-sequential
(is (= {:errors [{::errors/type ::errors/invalid-sequential-type
::errors/value {:type [:string] :value ["value"]}}]}
(types/resolve {} [:string] ["value"])))
(types/resolve {} {} [:string] ["value"])))
(is (= {:errors [{::errors/type ::errors/invalid-sequential-type
::errors/value {:type [{:name :string} {:name :string}] :value ["value"]}}]}
(types/resolve {} [{:name :string} {:name :string}] ["value"])))
(types/resolve {} {} [{:name :string} {:name :string}] ["value"])))
(is (= {:data [{:name "name"}
{:name "name"}]}
(types/resolve {} [{:name :string}]
[{:name "name"}
{:name "name"}])))
(types/resolve {} {} [{:name :string}]
[{:name "name"}
{:name "name"}])))
(is (= {:data [{:name "name" :scopes [{:scope :one}]}
{:name "name" :scopes [{:scope :two}]}]}
(types/resolve {} [{:name :string :scopes [{:scope {:one-of #{:one :two :three}}}]}]
[{:name "name" :scopes [{:scope :one}]}
{:name "name" :scopes [{:scope :two}]}]))))
(types/resolve {} {} [{:name :string :scopes [{:scope {:one-of #{:one :two :three}}}]}]
[{:name "name" :scopes [{:scope :one}]}
{:name "name" :scopes [{:scope :two}]}]))))
(deftest resolve-assoc
(is (= {:data {:name "value"}}
(types/resolve {} {:name :string} {:name "value"})))
(types/resolve {} {} {:name :string} {:name "value"})))
(is (= {:data {:name "value"}}
(types/resolve {} {:name? :string} {:name "value"})))
(types/resolve {} {} {:name? :string} {:name "value"})))
(is (= {:data {}}
(types/resolve {} {:name? :string} {})))
(types/resolve {} {} {:name? :string} {})))
(is (= {:errors [{::errors/type ::errors/invalid-type-value
::errors/value {:type :string :value nil}}]}
(types/resolve {} {:name? :string} {:name nil})))
(types/resolve {} {} {:name? :string} {:name nil})))
(is (= {:data {}}
(types/resolve {} {:name? :string} {:extra "value"})))
(types/resolve {} {} {:name? :string} {:extra "value"})))
(is (= {:data {:scopes [{:scope :one}]}}
(types/resolve {} {:scopes [{:scope {:one-of #{:one :two :three}}}]}
{:scopes [{:scope :one}]})))
(types/resolve {} {} {:scopes [{:scope {:one-of #{:one :two :three}}}]}
{:scopes [{:scope :one}]})))
(is (= {:data {:name "hello"
:children [{:name "name" :scopes [{:scope :one}]}
{:name "name" :scopes [{:scope :two}]}]}}
(types/resolve {} {:name? :string
:children [{:name :string :scopes [{:scope {:one-of #{:one :two :three}}}]}]}
{:extra "value"
:name "hello"
:children [{:name "name" :scopes [{:scope :one}]}
{:name "name" :scopes [{:scope :two}]}]}))))
(types/resolve {} {} {:name? :string
:children [{:name :string :scopes [{:scope {:one-of #{:one :two :three}}}]}]}
{:extra "value"
:name "hello"
:children [{:name "name" :scopes [{:scope :one}]}
{:name "name" :scopes [{:scope :two}]}]}))))

View File

@ -1,12 +1,16 @@
(ns pluto.reader.views-test
(:refer-clojure :exclude [resolve])
(:require [clojure.test :refer [is deftest testing]]
[pluto.reader.blocks :as blocks]
[pluto.reader.errors :as errors]
[pluto.reader.views :as views]))
[pluto.reader.types :as types]
[pluto.reader.views :as views]))
(deftest parse-hiccup-children
(is (= {:data (list [:text {} ""])} (views/parse-hiccup-children {:capacities {:components {'text :text}}}
(list ['text {} ""])))))
(is (= {:data (list [:text {} ""])}
(views/parse-hiccup-children {:capacities {:components {'text {:value :text}}}}
{}
(list ['text {} ""])))))
(defn- first-error-type [m]
(::errors/type (first (:errors m))))
@ -16,17 +20,18 @@
(is (= ::errors/invalid-view (first-error-type (views/parse {} {}))))
#_
(is (= ::errors/invalid-view
(first-error-type (views/parse {:capacities {:components {'text :text}}} ['text "Hello"]))))
(first-error-type (views/parse {:capacities {:components {'text {:value :text}}}} ['text "Hello"]))))
#_
(is (= ::errors/invalid-view
(first-error-type (views/parse {:capacities {:components {'text :text}}} ['text {} []]))))
(first-error-type (views/parse {:capacities {:components {'text {:value :text}}}} ['text {} []]))))
#_
(is (= {:data ['text {} "Hello"]
:errors (list {::errors/type ::errors/unknown-component ::errors/value 'text})}
(views/parse {} ['text {} "Hello"])))
(is (= {:data [:text {} "Hello"]}
(views/parse {:capacities {:components {'text :text}}} ['text {} "Hello"])))
(views/parse {:capacities {:components {'text {:value :text}}}} {} ['text {} "Hello"])))
(is (= {:data [:text {} "Hello"]}
(views/parse {:capacities {:components {'text :text}}} ['text {} "Hello"])))
(views/parse {:capacities {:components {'text {:value :text}}}} {} ['text {} "Hello"])))
(is (= {:data [:view
[:text {} "Hello"]
[blocks/let-block
@ -36,8 +41,9 @@
[:text {:style {:color "green"}} "World?"]
[:text {:style {:color "red"}} "World?"]]]]}
(views/parse {:capacities {:queries #{:random-boolean}
:components {'text :text
'view :view}}}
:components {'text {:value :text}
'view {:value :view}}}}
{}
'[view
[text {} "Hello"]
(let [cond? (query [:random-boolean])]
@ -48,5 +54,10 @@
"World?"]))])))
(testing "Properties"
(is (= {:data [:text {} "Hello"]}
(views/parse {:capacities {:components {'text :text}}} ['text {} "Hello"])))))
(views/parse {:capacities {:components {'text {:value :text}}}} {} ['text {} "Hello"])))))
(deftest resolve
(is (= [:text "Hello"] ((:data (types/resolve {:capacities {:components {'text {:value :text}}}} {'views/main ['text "Hello"]} :view ['views/main])) {})))
(is (= {:errors [{::errors/type ::errors/unknown-reference,
::errors/value {:value 'views/unknown}}]}
(types/resolve {:capacities {:components {'text {:value :text}}}} {'views/main ['text "Hello"]} :view ['views/unknown]))))

View File

@ -2,8 +2,8 @@
(:refer-clojure :exclude [read])
(:require [clojure.test :refer [is deftest]]
[pluto.reader :as reader]
[pluto.reader.errors :as errors]
[pluto.reader.blocks :as blocks]))
[pluto.reader.blocks :as blocks]
[pluto.reader.errors :as errors]))
(deftest read
(is (= {:data nil} (reader/read "")))
@ -69,27 +69,27 @@
((get-in m [:data :hooks :main :a :parsed :view]) {}))
(deftest parse-blocks
(is (= [blocks/let-block {:env {'s "Hello"}} [:text {} 's]]
(is (= [blocks/let-block {:env {'s "Hello"}} '[text {} s]]
(view (reader/parse default-capacities
(extension {'views/main (list 'let ['s "Hello"] ['text {} 's])
'hooks/main.a {:view 'views/main}})))))
(is (= [blocks/when-block {:test 'cond} [:text {} ""]]
'hooks/main.a {:view ['views/main]}})))))
(is (= [blocks/when-block {:test 'cond} '[text {} ""]]
(view (reader/parse default-capacities
(extension {'views/main (list 'when 'cond ['text {} ""])
'hooks/main.a {:view 'views/main}})))))
'hooks/main.a {:view ['views/main]}})))))
(is (= {:data {'meta default-meta
:hooks {:main {:a {:parsed nil
:hook-ref (:main default-hooks)}}}}
:errors (list {::errors/type ::errors/unsupported-test-type ::errors/value "string"})}
(reader/parse default-capacities (extension {'views/main (list 'when "string" ['text {} ""])
'hooks/main.a {:view 'views/main}})))))
'hooks/main.a {:view ['views/main]}})))))
(deftest parse
(is (= (list {::errors/type ::errors/unknown-component ::errors/value 'text})
(:errors (reader/parse {:capacities {:hooks default-hooks}}
(extension {'views/main ['text {} "Hello"]
'hooks/main.a {:view 'views/main}})))))
(is (= [:text {} "Hello"]
'hooks/main.a {:view ['views/main]}})))))
(is (= '[text {} "Hello"]
(view (reader/parse default-capacities
(extension {'views/main ['text {} "Hello"]
'hooks/main.a {:view 'views/main}}))))))
'hooks/main.a {:view ['views/main]}}))))))