ソースを参照

clarity: Storing structured data in datomic.

Lucas Stadler 12 年 前
コミット
801767ba48
共有5 個のファイルを変更した294 個の追加0 個の削除を含む
  1. 1 0
      clj/clarity/.gitignore
  2. 151 0
      clj/clarity/clarity.clj
  3. 35 0
      clj/clarity/clarity/server.clj
  4. 94 0
      clj/clarity/map-sample.html
  5. 13 0
      clj/clarity/project.clj

+ 1 - 0
clj/clarity/.gitignore

@ -0,0 +1 @@
1
/target

+ 151 - 0
clj/clarity/clarity.clj

@ -0,0 +1,151 @@
1
(ns clarity
2
  (:use clojure.core.typed))
3
4
; (HMap :mandatory {:a ... :b ...}) -> { :a input :b input }
5
;   needs nesting, dynamic rules?
6
7
; - required/optional? sometimes comes from above, an environment?
8
; - optional fields: adding dynamically would be better?
9
;     * but how to realize without creating an abstract ui model?
10
; - additional attributes (req/opt, hidden, defaults) are a problem
11
;   in general as they are quite tightly coupled to the implementations,
12
;   is there a name for this? is this a leaking abstraction?
13
14
; what do i want? from a description of the data i want to create an
15
; input method that only allows inserting valid values.
16
; sometimes we have existing data present or required so we must
17
; render that as part of the input tree. we also sometimes want to add
18
; information dynamically, for example adding key-value-pairs to maps
19
; and new elements to collections. to be able to do that we need to
20
; dynamically add new typed input methods on request. we also want to
21
; delete information sometimes, but only when it is not required.
22
23
(defmulti make-input (fn [type & _]
24
                       (if (seq? type)
25
                         (first type)
26
                         type)))
27
28
(defmethod make-input 'Value [type]
29
  (let [x (second type)
30
        attrs {:value (str x)
31
               :readonly ""}]
32
    [:input (into attrs
33
              (cond
34
               (instance? Boolean x) {:type "checkbox"}
35
               (number? x) {:type "number"}
36
               (or (instance? java.net.URL x) (instance? java.net.URI x)) {:type "url"}
37
               (keyword? x) {:pattern "^:(\\w+|\\w+(\\.\\w+)*\\/\\w+)$"
38
                             :type "text"}
39
               :else {:type "text"}))]))
40
41
(defmethod make-input 'Option [[_ type]]
42
  (make-input type))
43
44
(defmethod make-input 'U [[_ & alts]]
45
  [:select
46
   (map (fn [[_ x]]
47
          [:option (str x)])
48
        alts)])
49
50
(defmethod make-input 'Keyword [_]
51
  [:input {:type "text"
52
           :placeholder ":my.ns/identifier"
53
           :pattern "^:(\\w+|\\w+(\\.\\w+)*\\/\\w+)$"}])
54
55
(defmethod make-input 'String [_]
56
  [:input {:type "text"}])
57
58
(defmethod make-input 'Boolean [_]
59
  [:input {:type "checkbox"}])
60
61
(defmethod make-input 'Number [_]
62
  [:input {:type "number"}])
63
64
(defmethod make-input 'HVec [[_ & types]]
65
  ; display existing values as editable *and* allow adding new elements
66
  ; those elements can be of multiple types -> dynamism required?
67
  )
68
69
(defmethod make-input 'HMap [[ _ & {:keys [mandatory optional]}]]
70
  (concat '("{")
71
          (map (fn [[ key val]]
72
                 [:div.field
73
                  [:label (str key)]
74
                  (make-input val)])
75
               mandatory)
76
          (map (fn [[key val]]
77
                 [:div.field.optional
78
                  [:label (str key)]
79
                  (make-input val)])
80
               optional)
81
          '("}")))
82
83
(def datomic-attr-type
84
  '(HMap :mandatory {:db/id (Value "#db/id[db.part/db]")
85
                     :db/ident Keyword
86
                     :db/valueType (U (Value :db.type/keyword)
87
                                      (Value :db.type/string)
88
                                      (Value :db.type/boolean)
89
                                      (Value :db.type/long)
90
                                      (Value :db.type/bigint)
91
                                      (Value :db.type/float)
92
                                      (Value :db.type/double)
93
                                      (Value :db.type/bigdec)
94
                                      (Value :db.type/ref)
95
                                      (Value :db.type/instant)
96
                                      (Value :db.type/uuid)
97
                                      (Value :db.type/uri)
98
                                      (Value :db.type/bytes))
99
                     :db/cardinality (U (Value :db.cardinality/one)
100
                                        (Value :db.cardinality/many))
101
                     :db.install/_attribute (Value :db.part/db)}
102
         :optional {:db/doc String
103
                    :db/unique (Option (U (Value :db.unique/value)
104
                                          (Value :db.unique/identity)))
105
                    :db/index Boolean ; what about defaults?
106
                    :db/fulltext Boolean
107
                    :db/isComponent (Value :db.type/ref) ; FIXME: needs a custom type (dynamic even, because only valid refs should be allowed)
108
                    :db/noHistory Boolean}))
109
110
(defmulti friendly-check (fn [form type]
111
                           (if (seq? type)
112
                             (first type)
113
                             type)))
114
115
(defmethod friendly-check 'Value [val [_ expected]]
116
  (if (= val expected)
117
    true
118
    {:error (str "Expected value " expected ", but found " val ".")}))
119
120
(defmacro def-friendly-check [sym pred]
121
  `(defmethod friendly-check ~sym [val# _#]
122
     (if (~pred val#)
123
       true
124
       {:error (str "Expected value of type " (name ~sym) ", but got value " val# ".")})))
125
126
(def-friendly-check 'String string?)
127
(def-friendly-check 'Keyword keyword?)
128
(def-friendly-check 'Boolean #(instance? Boolean %))
129
130
(defmethod friendly-check 'U [val [_ & types]]
131
  (if (some true? (map (partial friendly-check val) types))
132
    true
133
    {:error (str "Expected one of " types ", but found " val ".")}))
134
135
(defn friendly-check-keys [m mandatory _]
136
  (map (fn [[key type]]
137
         (if-let [val (get m key)]
138
           (let [check (friendly-check val type)]
139
             (if (true? check)
140
               true
141
               {key (:error check)}))
142
           {key (str "No value found, but expected one of type " type ".")}))
143
       mandatory))
144
145
(defmethod friendly-check 'HMap [val [_ & {:keys [mandatory optional]}]]
146
  (if (map? val)
147
    (let [key-checks (friendly-check-keys val mandatory optional)]
148
      (if (every? true? key-checks)
149
        true
150
        {:error (filter (comp not true?) key-checks)}))
151
    {:error (str "Expected value of type Map, but got value " val ".")}))

+ 35 - 0
clj/clarity/clarity/server.clj

@ -0,0 +1,35 @@
1
(ns clarity.server
2
  (:use compojure.core)
3
  (:use ring.middleware.params
4
        ring.middleware.keyword-params)
5
  (:use [clojure.java.io :only (reader)])
6
  (:require [clojure.tools.reader.edn :as edn])
7
  (:use [hiccup.core :only (html)])
8
9
  (:require clarity)
10
  (:require [datomic.api :as d]))
11
12
(defonce conn
13
  (do
14
    (d/create-database "datomic:mem://self.data")
15
    (d/connect "datomic:mem://self.data")))
16
17
(defn read-tx-data [str]
18
  (edn/read-string {:readers {'db/id (partial apply d/tempid)}} str))
19
20
(defroutes app-routes
21
  (GET "/" []
22
       {:status 200
23
        :headers {"Content-Type" "text/html"}
24
        :body (html [:form#query {:action "/facts" :accept-charset "utf-8"}
25
                     [:textarea {:name "q" :cols 80 :rows 20}]
26
                     [:input {:type "submit"}]])})
27
  (GET "/facts" {{query :q} :params}
28
       (pr-str (d/q (edn/read-string query) (d/db conn))))
29
  (POST "/facts" [facts]
30
        (pr-str (d/transact conn (read-tx-data facts)))))
31
32
(def app
33
  (-> app-routes
34
      wrap-keyword-params
35
      wrap-params))

+ 94 - 0
clj/clarity/map-sample.html

@ -0,0 +1,94 @@
1
<!doctype html>
2
<html>
3
<head>
4
	<title>editing a map (clarity)</title>
5
	<meta charset="utf-8" />
6
	<style type="text/css">
7
	.field {
8
		display: block;
9
		margin: 0.5em 1em;
10
	}
11
	</style>
12
</head>
13
14
<body>
15
	<form id="sample">
16
		{
17
		<span class="field">
18
			<label>:db/id</label>
19
			<input type="text" readonly value="#db/id[db.part/db]" />
20
		</span>
21
22
		<span class="field">
23
			<label>:db/ident</label>
24
			<input type="text" required pattern="^:\w+$|^:\w+(\.\w+)?\/\w+$" placeholder=":my.ns/identifier" />
25
		</span>
26
27
		<span class="field">
28
			<label>:db/valueType</label>
29
			<!--<input type="text" list="db-valueType" required>-->
30
			<!--<datalist id="db-valueType">-->
31
			<select required>
32
				<option>:db.type/keyword</option>
33
				<option>:db.type/string</option>
34
				<option>:db.type/boolean</option>
35
				<option>:db.type/long</option>
36
				<option>:db.type/bigint</option>
37
				<option>:db.type/float</option>
38
				<option>:db.type/double</option>
39
				<option>:db.type/bigdec</option>
40
				<option>:db.type/ref</option>
41
				<option>:db.type/instant</option>
42
				<option>:db.type/uuid</option>
43
				<option>:db.type/uri</option>
44
				<option>:db.type/bytes</option>
45
			<!--</datalist>-->
46
			</select>
47
		</span>
48
49
		<span class="field">
50
			<label>:db/cardinality</label>
51
			<select required>
52
				<option>:db.cardinality/one</option>
53
				<option>:db.cardinality/many</option>
54
			</select>
55
		</span>
56
57
		<span class="field">
58
			<label>:ext/multiField</label>
59
			[
60
			<span class="field">
61
				<input type="text" placeholder=":my.ns/identifier" />
62
				<input type="button" value="Delete" />
63
			</span>
64
65
			<span class="field">
66
				<input type="text" placeholder=":my.ns/identifier" />
67
				<input type="button" value="Delete" />
68
			</span>
69
			]
70
			<input type="button" value="Delete" />
71
		</span>
72
73
		<span class="field">
74
			<label>:db.install/_attribute</label>
75
			<input type="text" readonly value=":db.part/db" />
76
		</span>
77
78
		<span class="field">
79
			<select>
80
				<option>:db/doc</option>
81
				<option>:db/unique</option>
82
				<option>:db/index</option>
83
				<option>:db/fulltext</option>
84
				<option>:db/isComponent</option>
85
				<option>:db/noHistory</option>
86
			</select>
87
			<input type="text" disabled />
88
89
			<input type="button" value="Add" />
90
		</span>
91
		}
92
	</form>
93
</body>
94
</html>

+ 13 - 0
clj/clarity/project.clj

@ -0,0 +1,13 @@
1
(defproject clarity "0.0-SNAPSHOT"
2
  :dependencies [[org.clojure/clojure "1.5.1"]
3
                 [org.clojure/core.typed "0.2.19"]
4
5
                 [com.datomic/datomic-free "0.9.4331"]
6
7
                 [ring "1.2.1"]
8
                 [compojure "1.1.6"]
9
10
                 [hiccup "1.0.4"]]
11
  :source-paths ["."]
12
  :plugins [[lein-ring "0.8.8"]]
13
  :ring {:handler clarity.server/app})