diff --git a/contacts/.gitignore b/contacts/.gitignore index e04714b..4188ff6 100644 --- a/contacts/.gitignore +++ b/contacts/.gitignore @@ -7,3 +7,4 @@ pom.xml.asc *.class /.lein-* /.nrepl-port +.repl* diff --git a/contacts/contacts.iml b/contacts/contacts.iml index 1b6b335..b678c63 100644 --- a/contacts/contacts.iml +++ b/contacts/contacts.iml @@ -5,41 +5,61 @@ + + + + + + + - + + + - - - - - + + + + + - + - + + + + + + + + + + + + - + + - - - - + + + + + - - - + @@ -48,19 +68,26 @@ - + - - - - - - - + + + + + + - - - + + + + + + + + + + + @@ -69,7 +96,6 @@ - @@ -81,6 +107,6 @@ + - - + \ No newline at end of file diff --git a/contacts/project.clj b/contacts/project.clj index 284de73..761a896 100644 --- a/contacts/project.clj +++ b/contacts/project.clj @@ -6,20 +6,24 @@ :jvm-opts ^:replace ["-Xms512m" "-Xmx512m" "-server"] - :dependencies [[org.clojure/clojure "1.5.1"] - [org.clojure/clojurescript "0.0-2173"] - [com.datomic/datomic-free "0.9.4699"] + :dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/clojurescript "0.0-3308"] + [com.datomic/datomic-free "0.9.5153"] [bidi "1.10.2"] - [om "0.5.3"] - [secretary "1.1.0"] + [org.omcljs/om "0.9.0"] [ring/ring "1.2.2"] - [fogus/ring-edn "0.2.0"] + [com.cognitect/transit-clj "0.8.271"] + [com.cognitect/transit-cljs "0.8.220"] + [cljs-http "0.1.30" :exclusions + [org.clojure/clojure org.clojure/clojurescript + com.cognitect/transit-cljs]] + [cljsjs/codemirror "5.1.0-2"] [com.stuartsierra/component "0.2.1"] - [org.clojure/core.async "0.1.278.0-76b25b-alpha"]] + [org.clojure/core.async "0.1.346.0-17112a-alpha"]] :source-paths ["src/clj"] - :plugins [[lein-cljsbuild "1.0.2"] + :plugins [[lein-cljsbuild "1.0.5"] [lein-ring "0.8.10"] [lein-beanstalk "0.2.7"]] diff --git a/contacts/resources/data/initial.edn b/contacts/resources/data/initial.edn index 139597f..7c2303e 100644 --- a/contacts/resources/data/initial.edn +++ b/contacts/resources/data/initial.edn @@ -1,2 +1,18 @@ - - +[{:db/id #db/id[:db.part/user] + :person/first-name "Bob" + :person/last-name "Smith" + :person/email [{:email/address "bob.smith@foo.com"}] + :person/telephone [{:telephone/number "111-111-1111"}] + :person/address [{:address/street "Maple Street" + :address/city "Boston" + :address/state "Massachusetts" + :address/zipcode "11111"}]} + {:db/id #db/id[:db.part/user] + :person/first-name "Martha" + :person/last-name "Smith" + :person/email [{:email/address "martha.smith@bar.com"}] + :person/telephone [{:telephone/number "111-111-1112"}] + :person/address [{:address/street "Maple Street" + :address/city "Boston" + :address/state "Massachusetts" + :address/zipcode "11111"}]}] \ No newline at end of file diff --git a/contacts/resources/public/codemirror/closebrackets.js b/contacts/resources/public/codemirror/closebrackets.js new file mode 100644 index 0000000..7030268 --- /dev/null +++ b/contacts/resources/public/codemirror/closebrackets.js @@ -0,0 +1,185 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var defaults = { + pairs: "()[]{}''\"\"", + triples: "", + explode: "[]{}" + }; + + var Pos = CodeMirror.Pos; + + CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.removeKeyMap(keyMap); + cm.state.closeBrackets = null; + } + if (val) { + cm.state.closeBrackets = val; + cm.addKeyMap(keyMap); + } + }); + + function getOption(conf, name) { + if (name == "pairs" && typeof conf == "string") return conf; + if (typeof conf == "object" && conf[name] != null) return conf[name]; + return defaults[name]; + } + + var bind = defaults.pairs + "`"; + var keyMap = {Backspace: handleBackspace, Enter: handleEnter}; + for (var i = 0; i < bind.length; i++) + keyMap["'" + bind.charAt(i) + "'"] = handler(bind.charAt(i)); + + function handler(ch) { + return function(cm) { return handleChar(cm, ch); }; + } + + function getConfig(cm) { + var deflt = cm.state.closeBrackets; + if (!deflt) return null; + var mode = cm.getModeAt(cm.getCursor()); + return mode.closeBrackets || deflt; + } + + function handleBackspace(cm) { + var conf = getConfig(cm); + if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; + + var pairs = getOption(conf, "pairs"); + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + for (var i = ranges.length - 1; i >= 0; i--) { + var cur = ranges[i].head; + cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1)); + } + } + + function handleEnter(cm) { + var conf = getConfig(cm); + var explode = conf && getOption(conf, "explode"); + if (!explode || cm.getOption("disableInput")) return CodeMirror.Pass; + + var ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) return CodeMirror.Pass; + var around = charsAround(cm, ranges[i].head); + if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass; + } + cm.operation(function() { + cm.replaceSelection("\n\n", null); + cm.execCommand("goCharLeft"); + ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var line = ranges[i].head.line; + cm.indentLine(line, null, true); + cm.indentLine(line + 1, null, true); + } + }); + } + + function handleChar(cm, ch) { + var conf = getConfig(cm); + if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass; + + var pairs = getOption(conf, "pairs"); + var pos = pairs.indexOf(ch); + if (pos == -1) return CodeMirror.Pass; + var triples = getOption(conf, "triples"); + + var identical = pairs.charAt(pos + 1) == ch; + var ranges = cm.listSelections(); + var opening = pos % 2 == 0; + + var type, next; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i], cur = range.head, curType; + var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1)); + if (opening && !range.empty()) { + curType = "surround"; + } else if ((identical || !opening) && next == ch) { + if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch) + curType = "skipThree"; + else + curType = "skip"; + } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 && + cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch && + (cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != ch)) { + curType = "addFour"; + } else if (identical) { + if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, ch)) curType = "both"; + else return CodeMirror.Pass; + } else if (opening && (cm.getLine(cur.line).length == cur.ch || + isClosingBracket(next, pairs) || + /\s/.test(next))) { + curType = "both"; + } else { + return CodeMirror.Pass; + } + if (!type) type = curType; + else if (type != curType) return CodeMirror.Pass; + } + + var left = pos % 2 ? pairs.charAt(pos - 1) : ch; + var right = pos % 2 ? ch : pairs.charAt(pos + 1); + cm.operation(function() { + if (type == "skip") { + cm.execCommand("goCharRight"); + } else if (type == "skipThree") { + for (var i = 0; i < 3; i++) + cm.execCommand("goCharRight"); + } else if (type == "surround") { + var sels = cm.getSelections(); + for (var i = 0; i < sels.length; i++) + sels[i] = left + sels[i] + right; + cm.replaceSelections(sels, "around"); + } else if (type == "both") { + cm.replaceSelection(left + right, null); + cm.triggerElectric(left + right); + cm.execCommand("goCharLeft"); + } else if (type == "addFour") { + cm.replaceSelection(left + left + left + left, "before"); + cm.execCommand("goCharRight"); + } + }); + } + + function isClosingBracket(ch, pairs) { + var pos = pairs.lastIndexOf(ch); + return pos > -1 && pos % 2 == 1; + } + + function charsAround(cm, pos) { + var str = cm.getRange(Pos(pos.line, pos.ch - 1), + Pos(pos.line, pos.ch + 1)); + return str.length == 2 ? str : null; + } + + // Project the token type that will exists after the given char is + // typed, and use it to determine whether it would cause the start + // of a string token. + function enteringString(cm, pos, ch) { + var line = cm.getLine(pos.line); + var token = cm.getTokenAt(pos); + if (/\bstring2?\b/.test(token.type)) return false; + var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4); + stream.pos = stream.start = token.start; + for (;;) { + var type1 = cm.getMode().token(stream, token.state); + if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1); + stream.start = stream.pos; + } + } +}); diff --git a/contacts/resources/public/codemirror/matchbrackets.js b/contacts/resources/public/codemirror/matchbrackets.js new file mode 100644 index 0000000..70e1ae1 --- /dev/null +++ b/contacts/resources/public/codemirror/matchbrackets.js @@ -0,0 +1,120 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: http://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { + var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && + (document.documentMode == null || document.documentMode < 8); + + var Pos = CodeMirror.Pos; + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; + + function findMatchingBracket(cm, where, strict, config) { + var line = cm.getLineHandle(where.line), pos = where.ch - 1; + var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; + if (!match) return null; + var dir = match.charAt(1) == ">" ? 1 : -1; + if (strict && (dir > 0) != (pos == where.ch)) return null; + var style = cm.getTokenTypeAt(Pos(where.line, pos + 1)); + + var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config); + if (found == null) return null; + return {from: Pos(where.line, pos), to: found && found.pos, + match: found && found.ch == match.charAt(0), forward: dir > 0}; + } + + // bracketRegex is used to specify which type of bracket to scan + // should be a regexp, e.g. /[[\]]/ + // + // Note: If "where" is on an open bracket, then this bracket is ignored. + // + // Returns false when no bracket was found, null when it reached + // maxScanLines and gave up + function scanForBracket(cm, where, dir, style, config) { + var maxScanLen = (config && config.maxScanLineLength) || 10000; + var maxScanLines = (config && config.maxScanLines) || 1000; + + var stack = []; + var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/; + var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1) + : Math.max(cm.firstLine() - 1, where.line - maxScanLines); + for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) { + var line = cm.getLine(lineNo); + if (!line) continue; + var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1; + if (line.length > maxScanLen) continue; + if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0); + for (; pos != end; pos += dir) { + var ch = line.charAt(pos); + if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) { + var match = matching[ch]; + if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch); + else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch}; + else stack.pop(); + } + } + } + return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null; + } + + function matchBrackets(cm, autoclear, config) { + // Disable brace matching in long lines, since it'll cause hugely slow updates + var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; + var marks = [], ranges = cm.listSelections(); + for (var i = 0; i < ranges.length; i++) { + var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config); + if (match && cm.getLine(match.from.line).length <= maxHighlightLen) { + var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style})); + if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen) + marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style})); + } + } + + if (marks.length) { + // Kludge to work around the IE bug from issue #1193, where text + // input stops going to the textare whever this fires. + if (ie_lt8 && cm.state.focused) cm.focus(); + + var clear = function() { + cm.operation(function() { + for (var i = 0; i < marks.length; i++) marks[i].clear(); + }); + }; + if (autoclear) setTimeout(clear, 800); + else return clear; + } + } + + var currentlyHighlighted = null; + function doMatchBrackets(cm) { + cm.operation(function() { + if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} + currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); + }); + } + + CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) + cm.off("cursorActivity", doMatchBrackets); + if (val) { + cm.state.matchBrackets = typeof val == "object" ? val : {}; + cm.on("cursorActivity", doMatchBrackets); + } + }); + + CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); + CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){ + return findMatchingBracket(this, pos, strict, config); + }); + CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){ + return scanForBracket(this, pos, dir, style, config); + }); +}); diff --git a/contacts/resources/public/css/main.css b/contacts/resources/public/css/main.css index c0fab42..fdb550e 100644 --- a/contacts/resources/public/css/main.css +++ b/contacts/resources/public/css/main.css @@ -1,63 +1,70 @@ body { - margin: 0px; - padding: 0px; - width: 100%; + font-size: 24px; } -ul { - margin: 0; - padding: 0 0 0 10px; +#demo1, #demo2 { + border: 1px solid #ccc; + display: -ms-flex; + display: -webkit-flex; + display: flex; + height: 600px; } -li { - list-style: none; - margin: 0; - padding: 10px; +#demo2 { + padding: 2em; + font-family: helvetica, arial, sans-serif; } -label { - font-size: 11px; - font-weight: bold; - text-transform: uppercase; +#demo2 h3 { + margin: 0; } -.button { - font-family: sans-serif; - padding: 5px 10px; - border: 1px solid #666; - border-radius: 4px; - background-color: white; - text-transform: uppercase; +#demo2 li { + padding: 0 0 0.5em 0; } -#contacts { - font-family: sans-serif; - position: absolute; - width: 400px; - left: 50%; - margin-left: -200px; - margin-top: 10px; +#demo2 li label { + font-weight: bold; + padding-right: 1em; } -#contacts-list, #contact-info { - border: 1px solid #ccc; - min-height: 50px; - padding: 10px; - margin-top: 10px; +textarea { + width: 50%; } -#contacts-list li { - cursor: pointer; +.CodeMirror { + height: 510px; + border-bottom: 1px solid #ccc; } -.section { - margin-top: 20px; +#demo1 > div { + width: 50%; } -.list { - font-size: 13px; +button { + font-size: 24px; + border: 2px solid black; + background-color: white; + border-radius: 4px; + margin-top: 1em; + margin-left: 1em; } -.editable button { - margin-left: 10px; +#input { } + +#output { + background-color: #efefef; + border-left: 1px solid #ccc; + height: 100%; +} + +pre { + white-space: pre-wrap; /* css-3 */ + white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/contacts/resources/public/html/demo1.html b/contacts/resources/public/html/demo1.html new file mode 100644 index 0000000..e3cc183 --- /dev/null +++ b/contacts/resources/public/html/demo1.html @@ -0,0 +1,20 @@ + + + + + Demo 1 + + +
+
+ + +
+
+
+            
+
+ + + \ No newline at end of file diff --git a/contacts/resources/public/html/demo2.html b/contacts/resources/public/html/demo2.html new file mode 100644 index 0000000..1285b74 --- /dev/null +++ b/contacts/resources/public/html/demo2.html @@ -0,0 +1,10 @@ + + + + Demo 2 + + +
+ + + diff --git a/contacts/resources/public/html/index.html b/contacts/resources/public/html/index.html deleted file mode 100644 index e4a46c2..0000000 --- a/contacts/resources/public/html/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - -
- - - - - - diff --git a/contacts/script/brepl.clj b/contacts/script/brepl.clj new file mode 100644 index 0000000..d70da75 --- /dev/null +++ b/contacts/script/brepl.clj @@ -0,0 +1,23 @@ +(require '[cljs.build.api :as b]) +(require '[cljs.repl :as repl]) +(require '[cljs.repl.browser :as browser]) + +(def shared-opts + {:asset-path "/js" + :output-dir "resources/public/js" + :foreign-libs + [{:provides ["cljsjs.codemirror.addons.matchbrackets"] + :requires ["cljsjs.codemirror"] + :file "public/codemirror/matchbrackets.js"} + {:provides ["cljsjs.codemirror.addons.closebrackets"] + :requires ["cljsjs.codemirror"] + :file "public/codemirror/closebrackets.js"}] + :verbose true}) + +(b/build (b/inputs "src/dev") + (merge + {:main 'contacts.dev + :output-to "resources/public/js/demo.js"} + shared-opts)) + +(repl/repl* (browser/repl-env :host-port 8081) shared-opts) \ No newline at end of file diff --git a/contacts/src/clj/contacts/core.clj b/contacts/src/clj/contacts/core.clj index 656a87d..f198b61 100644 --- a/contacts/src/clj/contacts/core.clj +++ b/contacts/src/clj/contacts/core.clj @@ -10,7 +10,7 @@ (defn dev-start [] (let [sys (system/dev-system {:db-uri "datomic:mem://localhost:4334/contacts" - :web-port 8080}) + :web-port 8081}) sys' (component/start sys)] (reset! servlet-system sys') sys')) diff --git a/contacts/src/clj/contacts/datomic.clj b/contacts/src/clj/contacts/datomic.clj index 4eb6bd7..8b1971a 100644 --- a/contacts/src/clj/contacts/datomic.clj +++ b/contacts/src/clj/contacts/datomic.clj @@ -2,51 +2,32 @@ (:require [datomic.api :as d] [com.stuartsierra.component :as component] [clojure.java.io :as io] - [clojure.edn :as edn]) + [clojure.edn :as edn] + [cognitect.transit :as t]) (:import datomic.Util)) - -;; ============================================================================= -;; Helpers - -(defn convert-db-id [x] - (cond - (instance? datomic.query.EntityMap x) - (assoc (into {} (map convert-db-id x)) - :db/id (str (:db/id x))) - - (instance? clojure.lang.MapEntry x) - [(first x) (convert-db-id (second x))] - - (coll? x) - (into (empty x) (map convert-db-id x)) - - :else x)) - - ;; ============================================================================= ;; Queries -(defn list-contacts [db] - (map - ;; won't roundtrip to conn bc segment already probably cached - #(d/entity db (first %)) - (d/q '[:find ?eid - :where - ;; talk about how we can make it do first OR last name - [?eid :person/first-name]] - db))) - - (defn display-contacts [db] - (let [contacts (list-contacts db)] - (map - #(select-keys % [:db/id :person/last-name :person/first-name]) - (sort-by :person/last-name (map convert-db-id contacts))))) +(defn contacts + ([db] (contacts db '[*])) + ([db selector] + (mapv first + (d/q '[:find (pull ?eid selector) + :in $ selector + :where + [?eid :person/first-name]] ;; talk about how we can make it do first OR last name + db selector)))) + +(defn get-contact + ([db id] (get-contact db id '[*])) + ([db id selector] + (d/pull db selector id))) +;; ============================================================================= +;; CRUD -(defn get-contact [db id-string] - (convert-db-id (d/touch (d/entity db (edn/read-string id-string))))) - +;; TODO: rewrite this into something generic, client will send transaction (defn create-contact [conn data] (let [tempid (d/tempid :db.part/user) @@ -86,37 +67,6 @@ @(d/transact conn [[:db.fn/retractEntity (edn/read-string id)]]) true) -(defn create-phone [conn data]) - -(defn update-phone [conn data]) - -(defn delete-phone [conn data]) - - -;; return datoms to add - -(def initial-data - (let [person-id (d/tempid :db.part/user) - address-id (d/tempid :db.part/user) - phone-id (d/tempid :db.part/user) - email-id (d/tempid :db.part/user)] - [{:db/id person-id - :person/first-name "Bob" - :person/last-name "Smith" - :person/email email-id - :person/telephone phone-id - :person/address address-id} - {:db/id email-id - :email/address "bob.smith@gmail.com"} - {:db/id phone-id - :telephone/number "123-456-7890"} - {:db/id address-id - :address/street "111 Main St" - :address/city "Brooklyn" - :address/state "NY" - :address/zipcode "11234"}])) - - (defrecord DatomicDatabase [uri schema initial-data connection] component/Lifecycle (start [component] @@ -130,7 +80,7 @@ (defn new-database [db-uri] (DatomicDatabase. db-uri (first (Util/readAll (io/reader (io/resource "data/schema.edn")))) - initial-data + (first (Util/readAll (io/reader (io/resource "data/initial.edn")))) nil)) ;; ============================================================================= diff --git a/contacts/src/clj/contacts/middleware.clj b/contacts/src/clj/contacts/middleware.clj new file mode 100644 index 0000000..7ca7ec9 --- /dev/null +++ b/contacts/src/clj/contacts/middleware.clj @@ -0,0 +1,133 @@ +(ns contacts.middleware + (:require [ring.util.response :refer :all] + [cognitect.transit :as transit]) + (:import [java.io ByteArrayOutputStream])) + +(defn- write [x t opts] + (let [baos (ByteArrayOutputStream.) + w (transit/writer baos t opts) + _ (transit/write w x) + ret (.toString baos)] + (.reset baos) + ret)) + +(defn- transit-request? [request] + (if-let [type (:content-type request)] + (let [mtch (re-find #"^application/transit\+(json|msgpack)" type)] + [(not (empty? mtch)) (keyword (second mtch))]))) + +(defn- read-transit [request {:keys [opts]}] + (let [[res t] (transit-request? request)] + (if res + (if-let [body (:body request)] + (let [rdr (transit/reader body t opts)] + (try + [true (transit/read rdr)] + (catch Exception ex + [false nil]))))))) + +(def ^{:doc "The default response to return when a Transit request is malformed."} +default-malformed-response + {:status 400 + :headers {"Content-Type" "text/plain"} + :body "Malformed Transit in request body."}) + +(defn wrap-transit-body + "Middleware that parses the body of Transit request maps, and replaces the :body + key with the parsed data structure. Requests without a Transit content type are + unaffected. + Accepts the following options: + :keywords? - true if the keys of maps should be turned into keywords + :opts - a map of options to be passed to the transit reader + :malformed-response - a response map to return when the JSON is malformed" + {:arglists '([handler] [handler options])} + [handler & [{:keys [malformed-response] + :or {malformed-response default-malformed-response} + :as options}]] + (fn [request] + (if-let [[valid? transit] (read-transit request options)] + (if valid? + (handler (assoc request :body transit)) + malformed-response) + (handler request)))) + +(defn- assoc-transit-params [request transit] + (let [request (assoc request :transit-params transit)] + (if (map? transit) + (update-in request [:params] merge transit) + request))) + +(defn wrap-transit-params + "Middleware that parses the body of Transit requests into a map of parameters, + which are added to the request map on the :transit-params and :params keys. + Accepts the following options: + :malformed-response - a response map to return when the JSON is malformed + :opts - a map of options to be passed to the transit reader + Use the standard Ring middleware, ring.middleware.keyword-params, to + convert the parameters into keywords." + {:arglists '([handler] [handler options])} + [handler & [{:keys [malformed-response] + :or {malformed-response default-malformed-response} + :as options}]] + (fn [request] + (if-let [[valid? transit] (read-transit request options)] + (if valid? + (handler (assoc-transit-params request transit)) + malformed-response) + (handler request)))) + +(defn wrap-transit-response + "Middleware that converts responses with a map or a vector for a body into a + Transit response. + Accepts the following options: + :encoding - one of #{:json :json-verbose :msgpack} + :opts - a map of options to be passed to the transit writer" + {:arglists '([handler] [handler options])} + [handler & [{:as options}]] + (let [{:keys [encoding opts] :or {encoding :json}} options] + (assert (#{:json :json-verbose :msgpack} encoding) "The encoding must be one of #{:json :json-verbose :msgpack}.") + (fn [request] + (let [response (handler request)] + (if (coll? (:body response)) + (let [transit-response (update-in response [:body] write encoding opts)] + (if (contains? (:headers response) "Content-Type") + transit-response + (content-type transit-response (format "application/transit+%s; charset=utf-8" (name encoding))))) + response))))) + +;(ns contacts.middleware +; (:require [cognitect.transit :as transit]) +; (:import [java.io ByteArrayInputStream ByteArrayOutputStream] +; [java.nio.charset StandardCharsets])) +; +;(defn str->is [str] +; (ByteArrayInputStream. (.getBytes str StandardCharsets/UTF_8))) +; +;(defn transit-request? +; [req] +; (if-let [^String type (:content-type req)] +; (not (empty? (re-find #"^application/transit+json" type))))) +; +;(defprotocol TransitRead +; (transit-read [this])) +; +;(extend-type String +; TransitRead +; (transit-edn [s] +; (transit/read (transit/reader (str->is s) :json)))) +; +;(extend-type java.io.InputStream +; TransitRead +; (transit-read [is] +; (transit/read (transit/reader is :json)))) +; +;(defn wrap-transit-params +; [handler] +; (fn [req] +; (if-let [body (and (transit-request? req) (:body req))] +; (let [transit-params (transit-read body) +; req* (assoc req +; :transit-params transit-params +; :params (merge (:params req) transit-params))] +; (handler req*)) +; (handler req)))) diff --git a/contacts/src/clj/contacts/server.clj b/contacts/src/clj/contacts/server.clj index 9d27dbf..5b2573d 100644 --- a/contacts/src/clj/contacts/server.clj +++ b/contacts/src/clj/contacts/server.clj @@ -1,142 +1,113 @@ (ns contacts.server - (:require [contacts.util :as util] - [ring.util.response :refer [file-response resource-response]] + (:require [clojure.java.io :as io] + [contacts.util :as util] + [ring.util.response :refer [response file-response resource-response]] [ring.adapter.jetty :refer [run-jetty]] - [ring.middleware.edn :refer [wrap-edn-params]] + [contacts.middleware + :refer [wrap-transit-body wrap-transit-response + wrap-transit-params]] [ring.middleware.resource :refer [wrap-resource]] [bidi.bidi :refer [make-handler] :as bidi] [com.stuartsierra.component :as component] [datomic.api :as d] - [contacts.datomic] - )) + [contacts.datomic])) ;; ============================================================================= -;; Routing +;; Routes (def routes - ["" {"/" :index - "/index.html" :index - "/contacts" - {:get - {[""] :contacts - ["/" :id] :contact-get} - :post {[""] :contact-create} - :put {["/" :id] :contact-update} - :delete {["/" :id] :contact-delete}} - "/phone" - {:post {[""] :phone-create} - :put {["/" :id] :phone-update} - :delete {["/" :id] :phone-delete}}}]) + ["" {"/" :demo1 + "/demo/1" :demo1 + "/demo/2" :demo2 + "/css/codemirror.css" :css.codemirror + "/query" + {:post {[""] :query}}}]) ;; ============================================================================= ;; Handlers -(defn index [req] - (assoc (resource-response "html/index.html" {:root "public"}) +(defn demo [n req] + (assoc (resource-response (str "html/demo" n ".html") {:root "public"}) :headers {"Content-Type" "text/html"})) +(defn codemirror-css [req] + (assoc + (resource-response "cljsjs/codemirror/production/codemirror.min.css") + :headers {"Content-Type" "text/css"})) (defn generate-response [data & [status]] - {:status (or status 200) - :headers {"Content-Type" "application/edn"} - :body (pr-str data)}) - + {:status (or status 200) + :headers {"Content-Type" "application/transit+json"} + :body data}) ;; CONTACT HANDLERS -(defn contacts [req] - (generate-response - (vec - (contacts.datomic/display-contacts - (d/db (:datomic-connection req)))))) - - -(defn contact-get [req id] - (generate-response - (contacts.datomic/get-contact - (d/db (:datomic-connection req)) id))) - - -(defn contact-create [req] - (generate-response - (contacts.datomic/create-contact - (:datomic-connection req) - ;; must have form {:person/first-name "x" :person/last-name "y} - (:edn-params req)))) +(defn contacts [conn selector] + (contacts.datomic/contacts (d/db conn) selector)) +(defn contact-get [conn id] + (contacts.datomic/get-contact (d/db conn) id)) -(defn contact-update [req id] - (generate-response - (contacts.datomic/update-contact - (:datomic-connection req) - (assoc (:edn-params req) :db/id id)))) +(defmulti -fetch (fn [_ k _] k)) +(defmethod -fetch :app/contacts + [conn _ selector] + (contacts conn selector)) -(defn contact-delete [req id] - (generate-response - (contacts.datomic/delete-contact - (:datomic-connection req) - id))) +(defn fetch + ([conn k] (fetch conn k '[*])) + ([conn k selector] + (-fetch conn k selector))) +(defn populate [conn query] + (letfn [(step [ret k] + (cond + (map? k) + (let [[k v] (first k)] + (assoc ret k (fetch conn k v))) -;;;; PHONE HANLDERS + (keyword? k) + (assoc ret k (fetch conn k)) -(defn phone-create [req] - (generate-response - (contacts.datomic/create-phone - (:datomic-connection req) - ;; must have form {:person/_telephone person-id} - (:edn-params req)))) + :else + (throw + (ex-info (str "Invalid query key " k) + {:type :error/invalid-query-value}))))] + (reduce step {} query))) -(defn phone-update [req id] +(defn query [req] (generate-response - (contacts.datomic/update-phone - (:datomic-connection req) - (assoc (:edn-params req) :db/id id)))) - -(defn phone-delete [req id] - (generate-response - (contacts.datomic/delete-phone - (:datomic-connection req) - id))) + (populate (:datomic-connection req) (:transit-params req)))) ;;;; PRIMARY HANDLER (defn handler [req] - (let [match (bidi/match-route - routes - (:uri req) + (let [match (bidi/match-route routes (:uri req) :request-method (:request-method req))] ;(println match) (case (:handler match) - :index (index req) - :contacts (contacts req) + :css.codemirror (codemirror-css req) + :demo1 (demo 1 req) + :demo2 (demo 2 req) + :query (query req) :contact-get (contact-get req (:id (:params match))) - :contact-create (contact-create req) - :contact-update (contact-update req (:id (:params match))) - :contact-delete (contact-delete req (:id (:params match))) - - :phone-create (phone-create req) - :phone-update (phone-update req (:id (:params match))) - :phone-delete (phone-delete req (:id (:params match))) - req))) - (defn wrap-connection [handler conn] (fn [req] (handler (assoc req :datomic-connection conn)))) - (defn contacts-handler [conn] (wrap-resource - (wrap-edn-params (wrap-connection handler conn)) + (wrap-transit-response + (wrap-transit-params (wrap-connection handler conn))) "public")) - (defn contacts-handler-dev [conn] (fn [req] ((contacts-handler conn) req))) +;; ============================================================================= +;; WebServer (defrecord WebServer [port handler container datomic-connection] component/Lifecycle @@ -151,56 +122,31 @@ (stop [component] (.stop container))) - (defn dev-server [web-port] (WebServer. web-port contacts-handler-dev true nil)) + (defn prod-server [] (WebServer. nil contacts-handler false nil)) ;; ============================================================================= ;; Route Testing (comment + (require '[contacts.core :as cc]) + (cc/dev-start) ;; get contact - (handler {:uri "/contacts/17592186045438" - :request-method :get - :datomic-connection (:connection (:db @contacts.core/servlet-system))}) + (handler {:uri "/query" + :request-method :post + :transit-params [{:app/contacts [:person/first-name :person/last-name + {:person/telephone '[*]}]}] + :datomic-connection (:connection (:db @cc/servlet-system))}) + + (.basisT (d/db (:connection (:db @cc/servlet-system)))) ;; create contact (handler {:uri "/contacts" :request-method :post - :edn-params {:person/first-name "Bib" :person/last-name "Bibooo"} - :datomic-connection (:connection (:db @contacts.core/servlet-system))}) - - ;; update contact - (handler {:uri "/contacts/17592186045434" - :request-method :put - :edn-params {:person/first-name "k" :person/last-name "b"} - :datomic-connection (:connection (:db @contacts.core/servlet-system))}) - - ;; delete contact - (handler {:uri "/contacts/17592186045434" - :request-method :delete - :datomic-connection (:connection (:db @contacts.core/servlet-system))}) - - ;; create phone - (handler {:uri "/phone" - :request-method :post - :edn-params {:telephone/number "000-111-2222" - :person/_telephone "17592186045438"} - :datomic-connection (:connection (:db @contacts.core/servlet-system))}) - - ;; update phone - (handler {:uri "/phone/17592186045444" - :request-method :put - :edn-params {:telephone/number "999-888-7777"} - :datomic-connection (:connection (:db @contacts.core/servlet-system))}) - - - ;; delete phone - (handler {:uri "/phone/17592186045444" - :request-method :delete - :datomic-connection (:connection (:db @contacts.core/servlet-system))}) - + :transit-params {:person/first-name "Bib" :person/last-name "Bibooo"} + :datomic-connection (:connection (:db @cc/servlet-system))}) ) diff --git a/contacts/src/clj/contacts/system.clj b/contacts/src/clj/contacts/system.clj index 3c12347..ef0a722 100644 --- a/contacts/src/clj/contacts/system.clj +++ b/contacts/src/clj/contacts/system.clj @@ -1,7 +1,7 @@ (ns contacts.system (:require [com.stuartsierra.component :as component] contacts.server - contacts.datomic)) + [contacts.datomic :as contacts])) (defn dev-system [config-options] (let [{:keys [db-uri web-port]} config-options] @@ -23,6 +23,24 @@ (comment (def s (dev-system {:db-uri "datomic:mem://localhost:4334/contacts" - :web-port 8081})) + :web-port 8081})) + (def s1 (component/start s)) + + (require '[datomic.api :as d]) + + (def conn (-> s1 :db :connection)) + (def db (d/db conn)) + + (contacts/contacts db + [:db/id :person/first-name :person/last-name + {:person/telephone [:telephone/number]}]) + + (d/q '[:find (pull ?p sel) + :in $ ?street sel + :where + [?a :address/street ?street] + [?p :person/address ?a]] + db "Maple Street" + [:person/first-name :person/last-name]) ) diff --git a/contacts/src/cljs/contacts/components.cljs b/contacts/src/cljs/contacts/components.cljs deleted file mode 100644 index 88c205f..0000000 --- a/contacts/src/cljs/contacts/components.cljs +++ /dev/null @@ -1,48 +0,0 @@ -(ns contacts.components - (:require [om.core :as om :include-macros true] - [om.dom :as dom :include-macros true])) - - -(defn display [show] - (if show - #js {} - #js {:display "none"})) - - -(defn handle-change [e owner] - (om/set-state! owner :edit-text (.. e -target -value))) - - -(defn end-edit [data edit-key owner cb] - (om/set-state! owner :editing false) - (om/update! data edit-key (om/get-state owner :edit-text)) - (cb {:value @data :edit-key edit-key})) - - -(defn editable [data owner {:keys [edit-key on-edit] :as opts}] - (reify - om/IInitState - (init-state [_] - {:editing false - :edit-text ""}) - om/IRenderState - (render-state [_ {:keys [edit-text editing]}] - (let [text (get data edit-key)] - (dom/li nil - (dom/span #js {:style (display (not editing))} text) - (dom/input - #js {:style (display editing) - :value edit-text - :onChange #(handle-change % owner) - :onKeyPress #(when (and (om/get-state owner :editing) - (== (.-keyCode %) 13)) - (end-edit data edit-key owner on-edit)) - :onBlur (fn [e] - (when (om/get-state owner :editing) - (end-edit data edit-key owner on-edit)))}) - (dom/button - #js {:style (display (not editing)) - :onClick (fn [e] - (om/set-state! owner :edit-text text) - (om/set-state! owner :editing true))} - "Edit")))))) diff --git a/contacts/src/cljs/contacts/core.cljs b/contacts/src/cljs/contacts/core.cljs deleted file mode 100644 index df78391..0000000 --- a/contacts/src/cljs/contacts/core.cljs +++ /dev/null @@ -1,193 +0,0 @@ -(ns contacts.core - (:require-macros [cljs.core.async.macros :refer [go]]) - (:require [goog.dom :as gdom] - [goog.events :as events] - [secretary.core :as secretary :include-macros true :refer [defroute]] - [om.core :as om :include-macros true] - [om.dom :as dom :include-macros true] - [cljs.core.async :refer [chan put! >! ! chan]] + [cljs.reader :as reader] + [cljsjs.codemirror.mode.clojure] + [cljsjs.codemirror.addons.matchbrackets]) + (:import [goog.events EventType])) + +(defn log [x] + (println) ;; flush past prompt + (pprint x)) + +(defn fetch [q] + (http/post "http://localhost:8081/query" {:transit-params q})) + +(defn main [] + (let [ed (js/CodeMirror.fromTextArea (gdom/getElement "input") + #js {:lineNumbers true + :matchBrackets true + :mode #js {:name "clojure"}})] + (events/listen (gdom/getElement "submit") EventType.CLICK + (fn [e] + (go + (set! (.-innerHTML (gdom/getElement "output")) + (with-out-str + (binding [pprint/*print-right-margin* 40] + (pprint + (:body + (! chan]] + [clojure.browser.repl :as repl])) + +;; ============================================================================= +;; Utilities + +(defn log [x] + (println) ;; flush past prompt + (pprint x)) + +(defn fetch [q] + (http/post "http://localhost:8081/query" {:transit-params q})) + +(defn label+span + ([label-content span-content] + (label+span nil label-content span-content)) + ([props label-content span-content] + (let [label-content (if-not (sequential? label-content) + [label-content] + label-content) + span-content (if-not (sequential? span-content) + [span-content] + span-content)] + (dom/div props + (apply dom/label nil label-content) + (apply dom/span nil span-content))))) + +;; ============================================================================= +;; AddressInfo Component + +(defui AddressInfo + static om/IQuery + (query [this] + '[:address/street :address/city :address/zipcode]) + Object + (render [this] + (let [{:keys [:address/street :address/city + :address/state :address/zipcode]} + (om/props this)] + (label+span #js {:className "address"} + "Address:" + (dom/span nil + (str street " " city ", " state " " zipcode)))))) + +(def address-info (om/create-factory AddressInfo)) + +;; ============================================================================= +;; Contact Component + +(defui Contact + ;static om/IQueryParams + ;(params [this] + ; {:address (om/get-query AddressInfo)}) + static om/IQuery + (query [this] + '[:person/first-name :person/last-name + {:person/telephone [:telephone/number]} + #_{:person/address ?address}]) + Object + (render [this] + (let [{:keys [:person/first-name :person/last-name :person/address] + :as props} + (om/props this)] + (dom/div nil + (label+span "Full Name:" + (str last-name ", " first-name)) + (label+span "Number:" + (:telephone/number (first (:person/telephone props)))) + #_(address-info (first address)))))) + +(def contact (om/create-factory Contact)) + +;; ============================================================================= +;; ContactList Component + +(defui ContactList + static om/IQueryParams + (params [this] + {:contact (om/get-query Contact)}) + static om/IQuery + (query [this] + '[{:app/contacts ?contact}]) + Object + (render [this] + (let [{:keys [:app/contacts]} (om/props this)] + (dom/div nil + (dom/h3 nil "Contacts") + (apply dom/ul nil + (map #(dom/li nil (contact %)) contacts)))))) + +(def contact-list (om/create-factory ContactList)) + +;; ============================================================================= +;; main + +(defn main [] + (defonce conn (repl/connect "http://localhost:9000/repl")) + (let [c (fetch (om/get-query ContactList))] + (go + (let [contacts (:body (