├── .gitignore ├── .whitesource ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── project.clj ├── src ├── fetch │ ├── core.cljs │ ├── lazy_store.cljs │ ├── macros.clj │ └── remotes.cljs └── logic │ └── fetch │ ├── macros.clj │ └── remotes.clj └── test └── fetch └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | .lein-deps-sum -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | ########################################################## 2 | #### WhiteSource "Bolt for Github" configuration file #### 3 | ########################################################## 4 | 5 | # Configuration # 6 | #---------------# 7 | ws.repo.scan=true 8 | vulnerable.check.run.conclusion.level=failure 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 2 | 3 | * Remove goog.net.XhrIo, goog.net.EventType, and goog.events 4 | 5 | ## 0.2.2 6 | 7 | * Add goog.net.EventType 8 | * Bump Clojure version to 1.6.0 9 | 10 | ## 0.2.0 11 | 12 | * Remove fetch.util since it's unused and causes compiler warnings 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See [Light Table's CONTRIBUTING.md](https://github.com/LightTable/LightTable/blob/master/CONTRIBUTING.md). 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2014 Kodowa, Inc. & Light Table contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fetch 2 | 3 | A ClojureScript library that makes client/server interaction painless. 4 | 5 | ## Usage 6 | 7 | ### Remotes 8 | 9 | Remotes let you make calls to a noir server without having to think about XHR. On the client-side you simply have code that looks like this: 10 | 11 | ```clojure 12 | (ns playground.client.test 13 | (:require [fetch.remotes :as remotes]) 14 | (:require-macros [fetch.macros :as fm])) 15 | 16 | (fm/remote (adder 2 5 6) [result] 17 | (js/alert result)) 18 | 19 | (fm/remote (get-user 2) [{:keys [username age]}] 20 | (js/alert (str "Name: " username ", Age: " age))) 21 | 22 | ;; for a much nicer experience, use letrem 23 | (fm/letrem [a (adder 3 4) 24 | b (adder 5 6)] 25 | (js/alert (str "a: " a " b: " b))) 26 | ``` 27 | 28 | Note that the results we get are real Clojure datastructures and so we use them just as we would in normal Clojure code. No JSON here. 29 | 30 | The noir side of things is just as simple. All you do is declare a remote using defremote. 31 | 32 | ```clojure 33 | (use 'noir.fetch.remotes) 34 | 35 | (defremote adder [& nums] 36 | (apply + nums)) 37 | 38 | (defremote get-user [id] 39 | {:username "Chris" 40 | :age 24}) 41 | 42 | (server/start 8080) 43 | ``` 44 | 45 | ## License 46 | 47 | Copyright (c) 2014 Kodowa, Inc. & Light Table contributors 48 | 49 | Distributed under the MIT License. See LICENSE.md 50 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject fetch "0.4.0" 2 | :description "A ClojureScript and Noir library to make client-server interaction painless." 3 | :url "https://github.com/LightTable/fetch" 4 | :license {:name "MIT License"} 5 | :dependencies [[org.clojure/clojure "1.10.1"] 6 | [compojure "1.6.2"] 7 | [ring "1.8.2"] 8 | [hiccup "2.0.0-alpha2"] 9 | [org.clojure/tools.macro "0.1.2"]]) 10 | 11 | -------------------------------------------------------------------------------- /src/fetch/core.cljs: -------------------------------------------------------------------------------- 1 | (ns fetch.core 2 | (:require [clojure.string :as string] 3 | [clojure.browser.net :as net] 4 | [clojure.browser.event :as event] 5 | [cljs.reader :as reader] 6 | [goog.Uri.QueryData :as query-data] 7 | [goog.structs :as structs])) 8 | 9 | (defn ->method [m] 10 | (string/upper-case (name m))) 11 | 12 | (defn parse-route [route] 13 | (cond 14 | (string? route) ["GET" route] 15 | (vector? route) (let [[m u] route] 16 | [(->method m) u]) 17 | :else ["GET" route])) 18 | 19 | (defn ->data [d] 20 | (let [cur (clj->js d) 21 | query (query-data/createFromMap (structs/Map. cur))] 22 | (str query))) 23 | 24 | (defn ->callback [callback] 25 | (when callback 26 | (fn [req] 27 | (let [data (. req (getResponseText))] 28 | (callback data))))) 29 | 30 | (defn xhr [route content callback & [opts]] 31 | (let [req (net/xhr-connection) 32 | [method uri] (parse-route route) 33 | data (->data content) 34 | callback (->callback callback)] 35 | (when callback 36 | (event/listen req :success #(callback req))) 37 | (net/transmit req uri method data (when opts (clj->js opts))))) 38 | -------------------------------------------------------------------------------- /src/fetch/lazy_store.cljs: -------------------------------------------------------------------------------- 1 | (ns fetch.lazy-store 2 | (:refer-clojure :exclude [get set]) 3 | (:require [fetch.core :as core]) 4 | (:use [cljs.reader :only [read-string]])) 5 | 6 | (def cache (atom {})) 7 | 8 | (defn ->vector [ks] 9 | (if-not (vector? ks) 10 | [ks] 11 | ks)) 12 | 13 | (defn set [ks v] 14 | (let [ks (->vector ks)] 15 | (swap! cache assoc-in ks v))) 16 | 17 | (defn latest [ks callback] 18 | (let [ks (->vector ks)] 19 | (core/xhr [:post "/lazy-store"] {:ks (pr-str ks)} 20 | (fn [data] 21 | (let [data (if (= data "") "nil" data) 22 | d (read-string data)] 23 | (set ks d) 24 | (callback d)))))) 25 | 26 | (defn get [ks callback] 27 | (let [ks (->vector ks)] 28 | (if-let [v (get-in @cache ks)] 29 | (callback v) 30 | (latest ks callback)))) 31 | 32 | -------------------------------------------------------------------------------- /src/fetch/macros.clj: -------------------------------------------------------------------------------- 1 | (ns fetch.macros) 2 | 3 | (defmacro remote 4 | [[sym & params] & [destruct & body]] 5 | (let [func (if destruct 6 | `(fn ~destruct ~@body) 7 | nil)] 8 | `(fetch.remotes/remote-callback ~(name sym) 9 | ~(vec params) 10 | ~func))) 11 | 12 | (defmacro letrem 13 | [bindings & body] 14 | (let [bindings (partition 2 bindings)] 15 | (reduce 16 | (fn [prev [destruct func]] 17 | `(remote ~func [~destruct] ~prev)) 18 | `(do ~@body) 19 | (reverse bindings)))) 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/fetch/remotes.cljs: -------------------------------------------------------------------------------- 1 | (ns fetch.remotes 2 | (:require [fetch.core :as core] 3 | [cljs.reader :as reader])) 4 | 5 | (def remote-uri "/_fetch") 6 | 7 | (defn remote-callback [remote params callback] 8 | (core/xhr [:post remote-uri] 9 | {:remote remote 10 | :params (pr-str params)} 11 | (when callback 12 | (fn [data] 13 | (let [data (if (= data "") "nil" data)] 14 | (callback (reader/read-string data))))))) 15 | -------------------------------------------------------------------------------- /src/logic/fetch/macros.clj: -------------------------------------------------------------------------------- 1 | (ns logic.fetch.macros 2 | "Functions/Macros to work with partials and pages. 3 | Extracted from noir source code." 4 | (:require [clojure.string :as string] 5 | [clojure.tools.macro :as macro] 6 | [compojure.core])) 7 | 8 | (defonce noir-routes (atom {})) 9 | (defonce route-funcs (atom {})) 10 | (defonce pre-routes (atom (sorted-map))) 11 | (defonce post-routes (atom [])) 12 | (defonce compojure-routes (atom [])) 13 | 14 | (defn- keyword->symbol [namesp kw] 15 | (symbol namesp (string/upper-case (name kw)))) 16 | 17 | (defn- route->key [action rte] 18 | (let [action (string/replace (str action) #".*/" "")] 19 | (str action (-> rte 20 | (string/replace #"\." "!dot!") 21 | (string/replace #"/" "--") 22 | (string/replace #":" ">") 23 | (string/replace #"\*" "<"))))) 24 | 25 | (defn- throwf [msg & args] 26 | (throw (Exception. (apply format msg args)))) 27 | 28 | (defn- parse-fn-name [[cur :as all]] 29 | (let [[fn-name remaining] (if (and (symbol? cur) 30 | (or (@route-funcs (keyword (name cur))) 31 | (not (resolve cur)))) 32 | [cur (rest all)] 33 | [nil all])] 34 | [{:fn-name fn-name} remaining])) 35 | 36 | (defn- parse-route [[{:keys [fn-name] :as result} [cur :as all]] default-action] 37 | (let [cur (if (symbol? cur) 38 | (try 39 | (deref (resolve cur)) 40 | (catch Exception e 41 | (throwf "Symbol given for route has no value"))) 42 | cur)] 43 | (when-not (or (vector? cur) (string? cur)) 44 | (throwf "Routes must either be a string or vector, not a %s" (type cur))) 45 | (let [[action url] (if (vector? cur) 46 | [(keyword->symbol "compojure.core" (first cur)) (second cur)] 47 | [default-action cur]) 48 | final (-> result 49 | (assoc :fn-name (if fn-name 50 | fn-name 51 | (symbol (route->key action url)))) 52 | (assoc :url url) 53 | (assoc :action action))] 54 | [final (rest all)]))) 55 | 56 | (defn- parse-destruct-body [[result [cur :as all]]] 57 | (when-not (some true? (map #(% cur) [vector? map? symbol?])) 58 | (throwf "Invalid destructuring param: %s" cur)) 59 | (-> result 60 | (assoc :destruct cur) 61 | (assoc :body (rest all)))) 62 | 63 | (defn ^{:skip-wiki true} parse-args 64 | "parses the arguments to defpage. Returns a map containing the keys :fn-name :action :url :destruct :body" 65 | [args & [default-action]] 66 | (-> args 67 | (parse-fn-name) 68 | (parse-route (or default-action 'compojure.core/GET)) 69 | (parse-destruct-body))) 70 | 71 | (defmacro defpage 72 | "Adds a route to the server whose content is the the result of evaluating the body. 73 | The function created is passed the params of the request and the destruct param allows 74 | you to destructure that meaningfully for use in the body. 75 | 76 | There are several supported forms: 77 | 78 | (defpage \"/foo/:id\" {id :id}) an unnamed route 79 | (defpage [:post \"/foo/:id\"] {id :id}) a route that responds to POST 80 | (defpage foo \"/foo:id\" {id :id}) a named route 81 | (defpage foo [:post \"/foo/:id\"] {id :id}) 82 | 83 | The default method is GET." 84 | [& args] 85 | (let [{:keys [fn-name action url destruct body]} (parse-args args)] 86 | `(do 87 | (defn ~fn-name {::url ~url 88 | ::action (quote ~action) 89 | ::args (quote ~destruct)} [~destruct] 90 | ~@body) 91 | (swap! route-funcs assoc ~(keyword fn-name) ~fn-name) 92 | (swap! noir-routes assoc ~(keyword fn-name) (~action ~url {params# :params} (~fn-name params#)))))) 93 | 94 | (defmacro defpartial 95 | "Create a function that returns html using hiccup. The function is callable with the given name. Can optionally include a docstring or metadata map, like a normal function declaration." 96 | [fname & args] 97 | (let [[fname args] (macro/name-with-attributes fname args) 98 | [params & body] args] 99 | `(defn ~fname ~params 100 | (html 101 | ~@body)))) 102 | -------------------------------------------------------------------------------- /src/logic/fetch/remotes.clj: -------------------------------------------------------------------------------- 1 | (ns logic.fetch.remotes 2 | (:use [logic.fetch.macros :only [defpage]])) 3 | 4 | (def remotes (atom {})) 5 | 6 | (defn get-remote [remote] 7 | (get @remotes remote)) 8 | 9 | (defn add-remote [remote func] 10 | (swap! remotes assoc remote func)) 11 | 12 | ;; Unsafe way to do this, Fix later: 1st priority. 13 | (defn safe-read [s] 14 | (binding [*read-eval* false] 15 | (read-string s))) 16 | 17 | (defmacro defremote [remote params & body] 18 | `(do 19 | (defn ~remote ~params ~@body) 20 | (add-remote ~(keyword (name remote)) ~remote))) 21 | 22 | (defn call-remote [remote params] 23 | (if-let [func (get-remote remote)] 24 | (let [result (apply func params)] 25 | {:status 202 26 | :headers {"Content-Type" "application/clojure; charset=utf-8"} 27 | :body (pr-str result)}) 28 | {:status 404})) 29 | 30 | (defn wrap-remotes [handler] 31 | (println "*** fetch/wrap-remotes is no longer needed. Please remove it ***") 32 | handler) 33 | 34 | (defpage [:any "/_fetch"] {:keys [remote params]} 35 | (let [params (safe-read params) 36 | remote (keyword remote)] 37 | (call-remote remote params))) 38 | -------------------------------------------------------------------------------- /test/fetch/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns fetch.core-test 2 | (:use clojure.test 3 | fetch.core)) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) --------------------------------------------------------------------------------