├── .gitignore ├── LICENSE ├── README.md ├── project.clj ├── resources └── public │ ├── bench.html │ └── index.html ├── src ├── cambo │ ├── component.cljc │ ├── component │ │ └── macros.clj │ ├── core.cljc │ ├── graph.cljc │ ├── http.cljs │ ├── model.cljc │ ├── profile.cljc │ ├── ring.clj │ ├── router.cljc │ └── utils.cljc ├── data_readers.clj └── examples │ ├── benchmarks.clj │ ├── benchmarks.cljs │ ├── github.clj │ ├── server.clj │ ├── ssr.clj │ └── todo.cljs └── test └── cambo ├── core_test.clj ├── graph_test.clj └── router_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea/ 13 | cambo.iml 14 | /out 15 | /resources/public/cljs 16 | figwheel_server.log 17 | /src/examples/queries.cljc 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cambo 2 | 3 | A combo of Falcor, Relay, and Om Next written in Clojure(Script). A cambo if you will. 4 | 5 | ## Should I use this? 6 | 7 | Only for non-critical applications. It is ready for trying out and we are using it for internal tools, but the API is 8 | in flux and there will be areas of functionality we haven't hit yet which are probably broken. 9 | 10 | Any issues are super welcome. We want to expand our use of this and so bug reports are A+. 11 | 12 | ## What 13 | 14 | Cambo is three things: 15 | 16 | - A router which runs on the server and is able to expose a single schema to clients which is composed of multiple backend 17 | services. When a client request is received the router dispatches to each backend service, collects the results, and 18 | returns it back to the client. 19 | 20 | - A model which runs on the client or server and is able to cache results and diff queries against this cache. When a 21 | client requests data it does so through the model which can either answer immediately if the data is cached, send a 22 | partial request if the cache contains some of the data, or send a full request if the cache contains none of the data. 23 | 24 | - A react component system which allows components to declare their data requirements, compose those data requirements 25 | via the component hierarchy, and interface with the model to satisfy those data requirements before being mounted. 26 | 27 | Each piece can be used independently -- which is a goal -- but they are all contained in a single repository because 28 | I am lazy. 29 | 30 | The router and model are inspired (aka stolen) from Falcor while the component model is inspired (again, stolen) from 31 | Relay. The use of datomic pull-like queries was inspired by Om Next as well as general 'how does clojurescript work' 32 | ideas. 33 | 34 | ## Why 35 | 36 | There is a general movement for having part of your client -- be it web or native -- running on the server (see: 37 | [Backend for Frontends](http://samnewman.io/patterns/architectural/bff/)). This might initially be a performance 38 | optimization -- reducing network requests -- but the real benefit is in developer productivity. 39 | 40 | Relay was introduced via a talk highlighting the productivity benefits. When client data requirements change any 41 | individual component need only change its declared data dependencies. These data requirements flow up the component 42 | hierarchy, and then data flows back down when rendered. If the server exposes it any component can require the data. 43 | The fact that these data requirements can be collected and sent as a single request are just an optimization. 44 | 45 | ## Concepts 46 | 47 | ### Paths 48 | 49 | The key concept in all of this is the idea of a path. A path is just a vector of keys describing the location of a 50 | piece of data within a map. Think `get-in`. 51 | 52 | ```clj 53 | (def graph {:user/by-id {1 {:user/name "Huey"}}}) 54 | 55 | (get-in graph [:user/by-id 1 :user/name]) 56 | ;; => "Huey" 57 | ``` 58 | 59 | In the above example the path is `[:user/by-id 1 :user/name]` and the data at the location described by the path is 60 | `"Huey"`. 61 | 62 | Most applications aren't pulling data from a map but from a database, web service, etc. That said, you can still 63 | describe each piece of data with a unique path location. Lets take the github api as an example. I want to know 64 | the description of the Netflix organization [api](https://api.github.com/orgs/netflix). A path for this datum could be 65 | `[:org/by-name "Netflix" :org/description]` (I used a Rich Hickey word in this sentence -- achievement unlocked). 66 | 67 | If we modeled the github api as a map I could execute the following line to get the description: 68 | 69 | ```clj 70 | (get-in github-api [:org/by-name "Netflix" :org/description]) 71 | ;; => "Netflix Open Source Platform" 72 | ``` 73 | 74 | Being able to treat a backend as a map is exactly what the Cambo Router allows. 75 | How to use the router to do this will be described later. 76 | 77 | ### Queries 78 | 79 | A query is simply a concise way to express multiple paths. Building on the above example we can also add the data 80 | requirement of the organizations location. 81 | 82 | ```clj 83 | (get-in github-api [:org/by-name "Netflix" :org/description]) 84 | ;; => "Netflix Open Source Platform" 85 | (get-in github-api [:org/by-name "Netflix" :org/location]) 86 | ;; => "Los Gatos, California" 87 | ``` 88 | 89 | Instead of getting multiple paths individually we can use the pull syntax to do it as a single operation. 90 | 91 | ```clj 92 | (pull github-api [{:org/by-name [{"Netflix" [:org/description 93 | :org/location]}]}]) 94 | ;; => {:org/by-name {"Netflix" {:org/description "Netflix Open Source Platform" 95 | ;; :org/location "Los Gatos, California"}}} 96 | ``` 97 | 98 | ### Fragments 99 | 100 | Sometimes a component doesn't know the identity of its data requirements. For instance a react component might know 101 | it needs `:org/description` and `:org/location` but doesn't know (or care) which organization -- some parent will 102 | figure that stuff out. It can still express its data requirements using the query syntax, but this query is not 103 | rooted at an identity. 104 | 105 | ```clj 106 | ;; no identity, what org? ... no idea how to satisify this! 107 | (pull github-api [:org/description :org/location]) 108 | ;; => nil 109 | ``` 110 | 111 | Luckily we can easily compose a fragment with a path to get a rooted query which has identity. 112 | 113 | ```clj 114 | (prepend-query [:org/by-name "Netflix"] [:org/description 115 | :org/location]) 116 | ;; => [{:org/by-name [{"Netflix" [:org/description 117 | ;; :org/location]}]}] 118 | ``` 119 | 120 | This composition of fragments is key to building up full queries via small component-local data requirements. 121 | 122 | ### Graph 123 | 124 | TODO: ref / atom 125 | 126 | ### Router 127 | 128 | TODO 129 | 130 | ```clj 131 | (def org-route 132 | {:route [:org/by-id INTEGERS [:org/description 133 | :org/email 134 | :org/name 135 | :org/login 136 | :org/id 137 | :org/url]] 138 | :get (fn [[_ ids keys] _] 139 | (for [id ids 140 | :let [org (api-get (str "/organizations/" id))] 141 | :when org 142 | key keys 143 | :let [github-key (keyword (name key))]] 144 | (path-value [:org/by-id id key] 145 | (get org github-key))))}) 146 | ``` 147 | 148 | 149 | 150 | ### Containers 151 | 152 | TODO 153 | 154 | ```clj 155 | (defcontainer OrganizationLink 156 | :fragments {:org [:org/url]} 157 | (render [this] 158 | (let [{:keys [url]} (props this :org)] 159 | (link {:href url} (children this))))) 160 | 161 | (def org-link (factory OrganizationLink)) 162 | 163 | (defcontainer OrganizationHeader 164 | :fragments {:org [:org/name 165 | :org/description 166 | (get-fragment OrganizationLink :org)]} 167 | (render [this] 168 | (let [{:keys [name description] :as org} (props this :org)] 169 | (div 170 | (h1 nil name) 171 | (p nil description) 172 | (org-link {:org org} "details"))))) 173 | 174 | (def org-header (factory OrganizationHeader)) 175 | ``` 176 | 177 | ### Renderers 178 | 179 | TODO 180 | 181 | ```clj 182 | (def model (model/model {:datasource (http-datasource "http://localhost:4000/cambo" 183 | {"X-CSRF-TOKEN" "abc123"})})) 184 | 185 | (js/ReactDOM.render 186 | (renderer {:queries {:org [:org/by-name "Netflix"]} 187 | :container OrganizationHeader 188 | :model model}) 189 | (.getElementById js/document "app")) 190 | ``` 191 | 192 | ### Model 193 | 194 | TODO 195 | 196 | 197 | ## License 198 | 199 | Copyright © 2016 Erik Petersen 200 | 201 | Distributed under the Eclipse Public License either version 1.0 or (at 202 | your option) any later version. 203 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.ladderlife/cambo "0.1.0-SNAPSHOT" 2 | :description "A Clojure take on Netflix's Falcor" 3 | :url "https://github.com/eyston/cambo" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0" :scope "provided"] 7 | [org.clojure/clojurescript "1.8.51" :scope "provided"] 8 | [cljsjs/react "15.0.1-1"] 9 | [cljsjs/react-dom "15.0.1-1"] 10 | [com.cognitect/transit-clj "0.8.285"] 11 | [com.cognitect/transit-cljs "0.8.239"]] 12 | :plugins [[lein-figwheel "0.5.4-3"] 13 | [lein-cljsbuild "1.1.3"]] 14 | :jar-exclusions [#"examples/" #"public/"] 15 | :profiles {:dev {:dependencies 16 | [[org.clojure/clojure "1.8.0"] 17 | [org.clojure/clojurescript "1.8.51"]]} 18 | :examples {:dependencies 19 | [[org.clojure/clojure "1.8.0"] 20 | [org.clojure/clojurescript "1.8.51"] 21 | [clj-http "2.2.0"] 22 | [cheshire "5.6.2"] 23 | [environ "1.0.3"] 24 | [ring "1.5.0"] 25 | [criterium "0.4.4"] 26 | [cljsjs/benchmark "2.1.0-1"]]}} 27 | :cljsbuild {:builds [{:id "dev" 28 | :source-paths ["src"] 29 | :figwheel true 30 | :compiler {:main "examples.todo" 31 | :asset-path "cljs/main-out" 32 | :output-to "resources/public/cljs/main.js" 33 | :output-dir "resources/public/cljs/main-out"}} 34 | {:id "bench" 35 | :source-paths ["src"] 36 | :figwheel true 37 | :compiler {:main "examples.benchmarks" 38 | :asset-path "cljs/bench-out" 39 | :output-to "resources/public/cljs/bench.js" 40 | :output-dir "resources/public/cljs/bench-out" 41 | :optimizations :advanced}} 42 | {:id "prod" 43 | :source-paths ["src"] 44 | :figwheel true 45 | :compiler {:main "examples.todo" 46 | :asset-path "cljs/out" 47 | :output-to "resources/public/cljs/prod.js" 48 | :output-dir "resources/public/cljs/prod-out" 49 | :optimizations :advanced}}]}) 50 | -------------------------------------------------------------------------------- /resources/public/bench.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/cambo/component.cljc: -------------------------------------------------------------------------------- 1 | (ns cambo.component 2 | #?(:cljs (:require-macros [cambo.component.macros])) 3 | (:require [cambo.core :as core] 4 | [cambo.model :as model] 5 | [cambo.utils :as utils] 6 | [cambo.profile #?(:clj :refer :cljs :refer-macros) [p pa]] 7 | #?(:clj [cambo.component.macros]) 8 | #?(:cljs [cljsjs.react]))) 9 | 10 | ;;; REACT 11 | 12 | (def props-key "cambo$props") 13 | 14 | (defn get-props 15 | [props] 16 | (aget props props-key)) 17 | 18 | (defn props 19 | ([comp] 20 | (get-props (.-props comp))) 21 | ([comp key] 22 | (get (get-props (.-props comp)) key))) 23 | 24 | #?(:cljs (declare container?)) 25 | 26 | #?(:cljs (defn ->props 27 | [{:keys [key ref] :as props}] 28 | (let [js-props (js-obj props-key props)] 29 | ;; TODO: nicer way to do this? 30 | ;; pull key from props into component react base props 31 | (when key (aset js-props "key" key)) 32 | ;; we pass the component as the ref -- not the container itself 33 | ;; if we ever need the container itself, can revisit this 34 | ;; TODO: assert ref is a fn, not string 35 | (when ref (aset js-props "ref" #(ref (if (container? %) 36 | (some-> % .-component) 37 | %)))) 38 | js-props))) 39 | 40 | (defn get-children 41 | [props] 42 | (.-children props)) 43 | 44 | (defn children 45 | [comp] 46 | (get-children (.-props comp))) 47 | 48 | (def state-key "cambo$state") 49 | 50 | (defn get-state 51 | [state] 52 | (when state 53 | (aget state state-key))) 54 | 55 | (defn state 56 | [comp] 57 | (get-state (.-state comp))) 58 | 59 | #?(:cljs (defn ->state 60 | [state] 61 | (js-obj state-key state))) 62 | 63 | #?(:cljs (defn set-state 64 | [comp fn-or-map] 65 | (let [state-fn (if (fn? fn-or-map) 66 | (fn [state props] 67 | (let [cambo-state (get-state state) 68 | cambo-props (get-props props) 69 | next-state (fn-or-map cambo-state cambo-props)] 70 | (->state (merge cambo-state next-state)))) 71 | (fn [state props] 72 | (let [cambo-state (get-state state)] 73 | (->state (merge cambo-state fn-or-map)))))] 74 | (.setState comp state-fn)))) 75 | 76 | (def context-key "cambo$context") 77 | 78 | (defn get-context 79 | [context] 80 | (aget context context-key)) 81 | 82 | #?(:cljs (defn ->context 83 | [context] 84 | (js-obj context-key context))) 85 | 86 | (defn context 87 | [comp] 88 | (get-context (.-context comp))) 89 | 90 | ;;; HELPERS 91 | 92 | (defn range-seq 93 | [elements] 94 | (->> elements 95 | (filter (comp integer? first)) 96 | (sort-by first))) 97 | 98 | ;;; FRAGMENTS 99 | 100 | (defprotocol IFragments 101 | (fragment-names [this]) 102 | (fragment [this name])) 103 | 104 | (defn fragments? [x] 105 | (satisfies? IFragments x)) 106 | 107 | (defprotocol IFragment 108 | (query [this])) 109 | 110 | (defn fragment? [x] (satisfies? IFragment x)) 111 | 112 | (deftype LazyFragment [delay] 113 | IFragment 114 | (query [_] @delay)) 115 | 116 | (defprotocol IRecursive 117 | (depth [this])) 118 | 119 | (defn recursive? [x] (satisfies? IRecursive x)) 120 | 121 | (deftype RecursiveFragment [fragment depth] 122 | IFragment 123 | (query [_] 124 | (query fragment)) 125 | IRecursive 126 | (depth [_] depth)) 127 | 128 | (defn get-fragment 129 | ([fragments name] 130 | (fragment fragments name)) 131 | ([fragments name depth] 132 | (RecursiveFragment. (get-fragment fragments name) depth))) 133 | 134 | ;; NOTE: 135 | ;; - this shouldn't be shared -- should be per fragment instance 136 | ;; - doesn't need to be dynamic anymore ... but whatever! 137 | (def ^:dynamic *depth* nil) 138 | 139 | (defn full-fragment 140 | [fragment] 141 | (letfn [(walk-query [q] 142 | (into [] 143 | (mapcat (fn [entry] 144 | (cond 145 | (core/key? entry) [entry] 146 | (map? entry) (let [[k q] (first entry) 147 | q (walk-query q)] 148 | (when (seq q) 149 | [{k q}])) 150 | (fragment? entry) (if (recursive? entry) 151 | (binding [*depth* (dec (or *depth* (depth entry)))] 152 | (when (>= *depth* 0) 153 | (walk-query (query entry)))) 154 | (walk-query (query entry)))))) 155 | q))] 156 | (walk-query (query fragment)))) 157 | 158 | (defn full-query 159 | [path fragment] 160 | (core/prepend-query path (full-fragment fragment))) 161 | 162 | (defn local-fragment 163 | [fragment] 164 | (letfn [(walk-query [q] 165 | (into [] 166 | (comp (map (fn [entry] 167 | (cond 168 | (core/key? entry) entry 169 | (map? entry) (let [[k q] (first entry) 170 | q (walk-query q)] 171 | (if (seq q) 172 | {k q} 173 | k)) 174 | (fragment? entry) nil))) 175 | (filter some?)) 176 | q))] 177 | (walk-query (query fragment)))) 178 | 179 | (defn local-query 180 | [path fragment] 181 | (core/prepend-query path (local-fragment fragment))) 182 | 183 | ;;; CONTAINER 184 | 185 | (defprotocol IContainer 186 | (get-container-fragment [this name vars])) 187 | 188 | (defn container-fragment [f vars] 189 | (LazyFragment. (delay (core/eval-query (f vars))))) 190 | 191 | (defn mock? 192 | [_] 193 | false) 194 | 195 | (defn get-path 196 | [props name] 197 | (get-in props [name :cambo/path])) 198 | 199 | (defprotocol IDisposable 200 | (dispose [this])) 201 | 202 | ;; TODO: get rid of this -- can be a function? 203 | #?(:cljs (deftype ContainerSubscription 204 | [^:mutable model ^:mutable query ^:mutable sub cb] 205 | IDisposable 206 | (dispose [this] 207 | (.unsubscribe this) 208 | nil) 209 | 210 | Object 211 | (update [this new-model new-query] 212 | (when (or (not= model new-model) 213 | (not= query new-query)) 214 | (.unsubscribe this) 215 | (set! model new-model) 216 | (set! query new-query) 217 | (.subscribe this)) 218 | this) 219 | (subscribe [this] 220 | (set! sub (model/subscribe model query cb)) 221 | this) 222 | (unsubscribe [this] 223 | (when sub 224 | (sub)) 225 | this))) 226 | 227 | #?(:cljs (defn container-subscription [model query cb] 228 | (.subscribe (ContainerSubscription. model query nil cb)))) 229 | 230 | (defn create-fragment-pointers 231 | [container props vars] 232 | (into {} (for [name (fragment-names container) 233 | :let [root (get-path props name)] 234 | :when root 235 | :let [fragment (get-container-fragment container name vars) 236 | query (local-query root fragment)]] 237 | [name {:root root 238 | :query query}]))) 239 | 240 | (def variables-key "cambo$variables") 241 | 242 | #?(:cljs (defn get-variables 243 | [props] 244 | (aget props variables-key))) 245 | 246 | #?(:cljs (defn variables* 247 | [this] 248 | (get-variables (.-props this)))) 249 | 250 | #?(:cljs (defn variables 251 | [this] 252 | (get (variables* this) :variables))) 253 | 254 | #?(:cljs (defn set-variables 255 | ([this variables] 256 | (set-variables this variables nil)) 257 | ([this variables cb] 258 | (let [{:keys [run]} (variables* this)] 259 | (run variables cb false))))) 260 | 261 | #?(:cljs (defn force-fetch 262 | ([this] 263 | (force-fetch this nil nil)) 264 | ([this variables] 265 | (force-fetch this variables nil)) 266 | ([this variables cb] 267 | (let [{:keys [run]} (variables* this)] 268 | (run variables cb true))))) 269 | 270 | #?(:cljs (defn ^:boolean container? 271 | [x] 272 | (if-not (nil? x) 273 | (true? (. x -cambo$isContainer)) 274 | false))) 275 | 276 | #?(:cljs (defn create-container* 277 | [{:keys [initial-variables prepare-variables fragments]} component] 278 | (let [fragment-names (into #{} (keys fragments)) 279 | fragment-cache (volatile! {}) 280 | container (fn container [] 281 | (this-as this 282 | (.apply js/React.Component this (js-arguments)) 283 | (set! (.-pending this) nil) 284 | (set! (.-mounted this) true) 285 | (set! (.-state this) 286 | (->state {:query-data nil 287 | :variables nil 288 | :cambo/variables {:run (fn [variables cb force?] 289 | (.run-variables this variables cb force?)) 290 | :set (fn [pathmaps cb] 291 | (let [{:keys [model]} (context this)] 292 | (model/set model pathmaps cb))) 293 | :call (fn [path args queries cb] 294 | (let [{:keys [model]} (context this)] 295 | (model/call model path args queries cb)))}})) 296 | this))] 297 | (set! (.-prototype container) 298 | (goog.object/clone js/React.Component.prototype)) 299 | 300 | (set! (.-contextTypes container) (->context React.PropTypes.object)) 301 | 302 | (set! (.. container -prototype -cambo$isContainer) true) 303 | 304 | (specify! container 305 | IFragments 306 | (fragment-names [_] fragment-names) 307 | (fragment [this name] 308 | (p :container/fragment 309 | (get-container-fragment this name (prepare-variables initial-variables nil)))) 310 | IContainer 311 | (get-container-fragment [_ name vars] 312 | (if-let [v (get @fragment-cache [name vars])] 313 | v 314 | (let [fragment-fn (get fragments name) 315 | fragment (container-fragment fragment-fn vars)] 316 | (vswap! fragment-cache assoc [name vars] fragment) 317 | fragment)))) 318 | 319 | (specify! (.-prototype container) 320 | Object 321 | 322 | (run-variables [this partial-vars cb force?] 323 | (let [{:keys [route model]} (context this) 324 | {:keys [variables]} (state this) 325 | variables (get (.-pending this) :variables variables) 326 | variables (merge variables partial-vars) 327 | next-variables (prepare-variables variables route) 328 | pointers (create-fragment-pointers container (props this) next-variables) 329 | query (into [] (mapcat :query (vals pointers))) 330 | on-readystate (fn [ready] 331 | (when ready 332 | (set! (.-pending this) nil) 333 | (set! (.-fragment-pointers this) pointers) 334 | (.update-subscription this (context this)) 335 | (when (.-mounted this) 336 | (set-state this (fn [{:keys [cambo/variables]}] 337 | {:query-data (.get-query-data this (props this) (context this)) 338 | :variables variables 339 | :cambo/variables (assoc variables :variables next-variables)})))) 340 | (when cb 341 | (cb ready)))] 342 | (if force? 343 | (model/force model query on-readystate) 344 | (model/prime model query on-readystate)) 345 | (set! (.-pending this) {:variables variables}))) 346 | 347 | (initialize [this {:keys [cambo/variables]} props {:keys [route] :as context} vars] 348 | (p :container/initialize 349 | (let [next-vars (prepare-variables vars route)] 350 | (.update-fragment-pointers this props next-vars) 351 | (.update-subscription this context) 352 | {:query-data (.get-query-data this props context) 353 | :variables vars 354 | :cambo/variables (assoc variables :variables next-vars)}))) 355 | 356 | (update-fragment-pointers [this props vars] 357 | (p :container/update-fragment-pointers 358 | (set! (.-fragment-pointers this) (create-fragment-pointers container props vars)))) 359 | 360 | (update-subscription [this {:keys [model]}] 361 | (p :container/update-subscription 362 | (let [pointers (.-fragment-pointers this) 363 | query (into [] (mapcat :query (vals pointers))) 364 | subscription (if-let [subscription (.-subscription this)] 365 | (.update subscription model query) 366 | (container-subscription model query (fn [] 367 | (.handle-fragment-data-update this))))] 368 | (set! (.-subscription this) subscription)))) 369 | 370 | (get-query-data [this props {:keys [model]}] 371 | (p :container/get-query-data 372 | (let [pointers (.-fragment-pointers this) 373 | query (into [] (mapcat :query (vals pointers))) 374 | graph (-> model model/with-path-info (model/pull-cache query)) 375 | query-data (into {} (map (fn [[name {:keys [root]}]] 376 | [name (get-in graph root)]) 377 | pointers))] 378 | query-data))) 379 | 380 | (handle-fragment-data-update [this] 381 | (p :container/handle-fragment-data-update 382 | (when (.-mounted this) 383 | (set-state this {:query-data (.get-query-data this (props this) (context this))})))) 384 | 385 | (componentWillMount [this] 386 | (p :container/componentWillMount 387 | (when-not (mock? this) 388 | (set-state this (fn [state] 389 | (.initialize this state (props this) (context this) initial-variables)))))) 390 | 391 | (componentWillReceiveProps [this next-props next-context] 392 | (p :container/componentWillReceiveProps 393 | (when-not (mock? this) 394 | (set-state this (fn [{:keys [variables] :as state}] 395 | (.initialize this state (get-props next-props) (get-context next-context) variables)))))) 396 | 397 | (componentWillUnmount [this] 398 | (p :container/componentWillUnmount 399 | (set! (.-mounted this) false) 400 | (when-let [sub (.-subscription this)] 401 | (dispose sub)))) 402 | 403 | (shouldComponentUpdate [this next-props next-state next-context] 404 | (p :container/shouldComponentUpdate 405 | (if (not= (get-children next-props) 406 | (children this)) 407 | true 408 | (let [update-props (fn [props] 409 | (reduce (fn [props key] 410 | (let [path (get-path props key)] 411 | (assoc props key path))) 412 | props 413 | (keys fragments))) 414 | current-props (update-props (props this)) 415 | current-state (state this) 416 | current-context (context this) 417 | next-props (update-props (get-props next-props)) 418 | next-state (get-state next-state) 419 | next-context (get-context next-context)] 420 | (or (not= current-props next-props) 421 | (not= current-state next-state) 422 | (not= current-context next-context)))))) 423 | 424 | (render [this] 425 | (p :container/render 426 | (let [{:keys [query-data cambo/variables]} (state this) 427 | component-props (merge (props this) 428 | query-data) 429 | children (.. this -props -children)] 430 | (js/React.createElement component 431 | (js-obj props-key component-props 432 | variables-key variables 433 | "ref" #(set! (.-component this) %)) 434 | children))))) 435 | 436 | container))) 437 | 438 | #?(:cljs (do 439 | (defn ^{:jsdoc ["@constructor"]} StaticContainer [] 440 | (this-as this 441 | (.apply js/React.Component this (js-arguments)) 442 | this)) 443 | 444 | (set! (.-prototype StaticContainer) 445 | (goog.object/clone js/React.Component.prototype)) 446 | 447 | (specify! (.-prototype StaticContainer) 448 | Object 449 | (shouldComponentUpdate [this next-props next-state] 450 | (not (not (aget next-props "shouldUpdate")))) 451 | (render [this] 452 | (when-let [child (.. this -props -children)] 453 | (React.Children.only child)))))) 454 | 455 | (defn build-query 456 | [queries container] 457 | (into [] 458 | (mapcat (fn [[name path]] 459 | (let [fragment (get-fragment container name) 460 | query (full-query path fragment)] 461 | query))) 462 | queries)) 463 | 464 | #?(:cljs (do 465 | (defn ^{:jsdoc ["@constructor"]} Renderer [] 466 | (this-as this 467 | ;; TODO: state! 468 | (set! (.-mounted this) true) 469 | (set! (.-state this) (->state {:readystate nil})) 470 | (.apply js/React.Component this (js-arguments)) 471 | this)) 472 | 473 | (set! (.-prototype Renderer) 474 | (goog.object/clone js/React.Component.prototype)) 475 | 476 | ;; this is not in extern ... 477 | ;(set! (.-childContextTypes Renderer) (->context React.PropTypes.object)) 478 | 479 | (aset Renderer "childContextTypes" (->context React.PropTypes.object)) 480 | 481 | (specify! (.-prototype Renderer) 482 | Object 483 | (run-queries [this {:keys [container model queries force]}] 484 | (p :renderer/run-queries 485 | (let [query (build-query queries container) 486 | on-readystate (fn [readystate] 487 | (when (.-mounted this) 488 | (p :renderer/set-state (set-state this {:readystate readystate})) 489 | #_(set-state this {:readystate readystate}) 490 | (.handle-readystate-change this readystate)))] 491 | (if force 492 | (model/force model query on-readystate) 493 | (model/prime model query on-readystate))))) 494 | (handle-readystate-change [this readystate] 495 | (p :renderer/handle-readystate-change 496 | (when-let [on-readystate-change (:on-readystate-change (props this))] 497 | (on-readystate-change readystate)))) 498 | (getChildContext [this] 499 | (let [{:keys [model]} (props this)] 500 | (->context {:model model}))) 501 | (componentDidMount [this] 502 | (p :renderer/componentDidMount 503 | (.run-queries this (props this)))) 504 | (componentWillReceiveProps [this next-props] 505 | (p :renderer/componentWillReceiveProps 506 | (let [renderer-props [:model :queries :container :force]] 507 | (when-not (= (select-keys (props this) renderer-props) 508 | (select-keys (get-props next-props) renderer-props)) 509 | (set-state this {:readystate nil}) 510 | (.run-queries this (get-props next-props)))))) 511 | (componentDidUpdate [this prev-props prev-state] 512 | (p :renderer/componentDidUpdate 513 | (let [readystate (:readystate (state this)) 514 | prev-readystate (:readystate (get-state prev-state))] 515 | (when-not (= readystate prev-readystate) 516 | (.handle-readystate-change this readystate))))) 517 | (componentWillUnmount [this] 518 | (p :renderer/componentWillUnmount 519 | (set! (.-mounted this) false))) 520 | (render [this] 521 | (p :renderer/render 522 | (let [{:keys [container queries]} (props this) 523 | {:keys [readystate]} (state this) 524 | container-props (dissoc (props this) :container :model :queries :force) 525 | container-props (into container-props 526 | (for [[name path] queries] 527 | [name {:cambo/path path}])) 528 | ;; TODO: support render prop -- handle js/undefined response 529 | container-element (when readystate 530 | (js/React.createElement container 531 | (->props container-props) 532 | (children this)))] 533 | (js/React.createElement StaticContainer 534 | #js {"shouldUpdate" (some? container-element)} 535 | container-element))))))) 536 | 537 | #?(:cljs (defn renderer [props & children] 538 | (apply js/React.createElement Renderer (->props props) children))) 539 | 540 | (defn factory [cls] 541 | #?(:cljs (fn [props & children] 542 | (apply React.createElement cls (->props props) children)) 543 | :clj (fn [& args]))) 544 | 545 | #?(:cljs (defn set-model 546 | [this prop-key pathmap] 547 | (let [{:keys [set]} (variables* this) 548 | path (get-path (props this) prop-key)] 549 | (set [(assoc-in {} path pathmap)] (fn [_]))))) 550 | 551 | #?(:cljs (defn call-model 552 | ([this path] 553 | (call-model this path nil)) 554 | ([this path args] 555 | (call-model this path args {})) 556 | ([this path args queries] 557 | (call-model this path args queries (fn [_]))) 558 | ([this path args queries cb] 559 | (let [{:keys [call]} (variables* this)] 560 | (call path args queries cb))))) 561 | 562 | ;;; MACROS 563 | 564 | #?(:clj 565 | (defmacro defcomponent [name & forms] 566 | `(utils/if-cljs 567 | (cambo.component.macros/defcomponent-cljs ~name ~@forms) 568 | (cambo.component.macros/defcomponent-clj ~name ~@forms)))) 569 | 570 | #?(:clj 571 | (defmacro defcontainer [name & forms] 572 | `(utils/if-cljs 573 | (cambo.component.macros/defcontainer-cljs ~name ~@forms) 574 | (cambo.component.macros/defcontainer-clj ~name ~@forms)))) 575 | -------------------------------------------------------------------------------- /src/cambo/component/macros.clj: -------------------------------------------------------------------------------- 1 | (ns cambo.component.macros 2 | (:require [cljs.core :refer [js-obj specify! this-as js-arguments]] 3 | [cljs.analyzer :as ana])) 4 | 5 | (def lifecycle-sigs 6 | '{constructor [this props] 7 | initLocalState [this] 8 | componentWillReceiveProps [this next-props] 9 | componentWillUpdate [this next-props next-state] 10 | componentDidUpdate [this prev-props prev-state] 11 | componentWillMount [this] 12 | componentDidMount [this] 13 | componentWillUnmount [this] 14 | render [this]}) 15 | 16 | (defn validate-sig [[name sig :as method]] 17 | (let [sig' (get lifecycle-sigs name)] 18 | (assert (= (count sig') (count sig)) 19 | (str "Invalid signature for " name " got " sig ", need " sig')))) 20 | 21 | (def reshape-map 22 | {'componentWillReceiveProps 23 | (fn [[name [this next-props] & body]] 24 | `(~name [this# next-props#] 25 | (let [~this this# 26 | ~next-props (cambo.component/get-props next-props#)] 27 | ~@body))) 28 | 'componentWillUpdate 29 | (fn [[name [this next-props next-state] & body]] 30 | `(~name [this# next-props# next-state#] 31 | (let [~this this# 32 | ~next-props (cambo.component/get-props next-props#) 33 | ~next-state (cambo.component/get-state next-state#)] 34 | ~@body))) 35 | 'componentDidUpdate 36 | (fn [[name [this prev-props prev-state] & body]] 37 | `(~name [this# prev-props# prev-state#] 38 | (let [~this this# 39 | ~prev-props (cambo.component/get-props prev-props#) 40 | ~prev-state (cambo.component/get-state prev-state#)] 41 | ~@body)))}) 42 | 43 | (defn reshape [dt reshape] 44 | (letfn [(reshape* [x] 45 | (if (and (sequential? x) 46 | (contains? reshape (first x))) 47 | (let [reshapef (get reshape (first x))] 48 | (validate-sig x) 49 | (reshapef x)) 50 | x))] 51 | (into ['Object] 52 | (map reshape* dt)))) 53 | 54 | (defmacro defcomponent-cljs [name & forms] 55 | (let [rname (if &env 56 | ;; copied from om.next -- no idea 57 | (:name (ana/resolve-var (dissoc &env :locals) name)) 58 | name) 59 | ctor `(defn ~(with-meta name {:jsdoc ["@constructor"]}) [] 60 | (this-as this# 61 | (.apply js/React.Component this# (js-arguments)) 62 | (if-not (nil? (.-initLocalState this#)) 63 | (set! (.-state this#) (cambo.component.->state (.initLocalState this#))) 64 | (set! (.-state this#) (cambo.component.->state {}))) 65 | this#)) 66 | set-react-proto! `(set! (.-prototype ~name) 67 | (goog.object/clone js/React.Component.prototype)) 68 | {:keys [display-name]} (meta name) 69 | display-name (or display-name 70 | (if &env 71 | ;; copied from om.next -- no idea 72 | (str (-> &env :ns :name) "/" name) 73 | 'js/undefined))] 74 | `(do 75 | ~ctor 76 | ~set-react-proto! 77 | 78 | (specify! (.-prototype ~name) ~@(reshape forms reshape-map)) 79 | 80 | (set! (.. ~name -prototype -constructor) ~name) 81 | (set! (.. ~name -prototype -constructor -displayName) ~display-name) 82 | 83 | (set! (.-cljs$lang$type ~rname) true) 84 | (set! (.-cljs$lang$ctorStr ~rname) ~(str rname)) 85 | (set! (.-cljs$lang$ctorPrWriter ~rname) 86 | (fn [this# writer# opt#] 87 | (cljs.core/-write writer# ~(str rname))))))) 88 | 89 | (defn collect-container [forms] 90 | (letfn [(split-on-spec [forms] 91 | (split-with (complement keyword?) forms)) 92 | (split-on-component [forms] 93 | (split-with (complement seq?) forms))] 94 | (loop [forms forms specs {} component []] 95 | (if-let [form (first forms)] 96 | (cond 97 | (keyword? form) 98 | (let [[specs' remaining] (split-on-component forms) 99 | specs' (into {} (map vec (partition 2 specs')))] 100 | (recur remaining (merge specs specs') component)) 101 | 102 | (seq? form) 103 | (let [[component' remaining] (split-on-spec forms)] 104 | (recur remaining specs (into component component')))) 105 | {:component component :specs specs})))) 106 | 107 | (defn normalize-fragments [fragments] 108 | (into {} (for [[name fragment-fn] fragments] 109 | [name (if (seq? fragment-fn) 110 | fragment-fn 111 | `(fn [~'_] ~fragment-fn))]))) 112 | 113 | (defn normalize-specs [specs] 114 | (let [specs (merge {:initial-variables {} 115 | :prepare-variables `(fn [vars# _#] vars#)} 116 | specs) 117 | specs (update specs :fragments normalize-fragments)] 118 | specs)) 119 | 120 | (defmacro defcontainer-cljs [name & forms] 121 | (let [{:keys [specs component]} (collect-container forms) 122 | specs (normalize-specs specs) 123 | component-name (with-meta (gensym (str name "_")) {:anonymous true 124 | :display-name (str name "*")}) 125 | display-name (if &env 126 | (str (-> &env :ns :name) "/" name) 127 | 'js/undefined)] 128 | `(do 129 | (defcomponent-cljs ~component-name ~@component) 130 | 131 | (def ~name (let [container# (cambo.component/create-container* ~specs ~component-name)] 132 | (set! (.-displayName container#) ~display-name) 133 | container#))))) 134 | 135 | (defmacro defcomponent-clj [name & forms] 136 | `(defrecord ~name [])) 137 | 138 | (defmacro defcontainer-clj [name & forms] 139 | (let [{:keys [specs component]} (collect-container forms) 140 | {:keys [initial-variables prepare-variables fragments]} (normalize-specs specs) 141 | fragment-names (into #{} (keys fragments))] 142 | `(def ~name 143 | (reify 144 | cambo.component/IFragments 145 | (~'fragment-names [_#] 146 | ~fragment-names) 147 | (~'fragment [this# name#] 148 | (cambo.component/get-container-fragment 149 | this# name# (~prepare-variables ~initial-variables nil))) 150 | cambo.component/IContainer 151 | (~'get-container-fragment [_# name# vars#] 152 | (let [fragment-fn# (get ~fragments name#) 153 | fragment# (cambo.component/container-fragment fragment-fn# vars#)] 154 | fragment#)))))) 155 | -------------------------------------------------------------------------------- /src/cambo/core.cljc: -------------------------------------------------------------------------------- 1 | (ns cambo.core 2 | (:refer-clojure :exclude [get set atom ref keys range ->Atom Atom ->Ref Ref ->Range Range])) 3 | 4 | ;;; DATASOURCE 5 | 6 | (defprotocol IDataSource 7 | ;; TODO: do we want this to be a single call cb, or observable? 8 | (pull [this query cb]) 9 | (set [this pathmaps cb]) 10 | (call [this path args queries cb])) 11 | 12 | ;;; GRAPH 13 | 14 | (defrecord Atom []) 15 | 16 | (defn atom 17 | ([] 18 | (Atom.)) 19 | ([value] 20 | (map->Atom {:value value}))) 21 | 22 | (defn atom? [x] 23 | (instance? Atom x)) 24 | 25 | (defn non-value? [x] 26 | (and (atom? x) 27 | (not (contains? x :value)))) 28 | 29 | (defrecord Ref [path]) 30 | 31 | (defn ref [path] 32 | (Ref. path)) 33 | 34 | (defn ref? [x] 35 | (instance? Ref x)) 36 | 37 | (defn boxed? 38 | [value] 39 | (or (atom? value) 40 | (ref? value))) 41 | 42 | (defrecord PathValue [path value]) 43 | 44 | (defn pv [path value] 45 | (PathValue. path value)) 46 | 47 | (defn path-value [path value] 48 | (PathValue. path value)) 49 | 50 | (defn path-value? [x] 51 | (instance? PathValue x)) 52 | 53 | (defn pathmap [path value] 54 | (assoc-in {} path value)) 55 | 56 | (defn pathmap? [x] 57 | (and (map? x) 58 | (not (record? x)))) 59 | 60 | ;;; KEYS 61 | 62 | (defn uuid? [x] 63 | #?(:clj (instance? java.util.UUID x) 64 | :cljs (or (instance? cljs.core/UUID x) 65 | (instance? com.cognitect.transit.types.UUID x)))) 66 | 67 | (defn key? [x] 68 | (or (string? x) 69 | (keyword? x) 70 | (integer? x) 71 | (symbol? x) 72 | (uuid? x))) 73 | 74 | (defrecord Range [start end]) 75 | 76 | (defn range [start end] 77 | (Range. start end)) 78 | 79 | (defn range-keys [{:keys [start end]}] 80 | (clojure.core/range start end)) 81 | 82 | (defn range? [x] 83 | (instance? Range x)) 84 | 85 | (defn ranges [ns] 86 | (->> ns 87 | distinct 88 | sort 89 | (reduce (fn [acc n] 90 | (let [a (-> acc last last)] 91 | (if (or (nil? a) 92 | (not= n (inc a))) 93 | (conj acc [n]) 94 | (update acc (dec (count acc)) conj n)))) 95 | []) 96 | (map (fn [ns] 97 | (let [from (first ns) 98 | to (last ns)] 99 | (range from (inc to))))) 100 | (into []))) 101 | 102 | (defn keyset [keys] 103 | (if (= 1 (count keys)) 104 | (first keys) 105 | (let [{ints true keys false} (group-by integer? keys) 106 | keys (cond-> (into #{} keys) 107 | (seq ints) (into (ranges ints)))] 108 | (into [] keys)))) 109 | 110 | (defn keyset? [x] 111 | (or (key? x) 112 | (range? x) 113 | (and (vector? x) 114 | (every? #(or (key? %) (range? %)) x)))) 115 | 116 | (defn keys [x] 117 | (cond 118 | (key? x) [x] 119 | (range? x) (range-keys x) 120 | (vector? x) (mapcat keys x))) 121 | 122 | (defn keyset-seq [x] 123 | (cond 124 | (key? x) [x] 125 | (range? x) [x] 126 | (vector? x) x)) 127 | 128 | ;;; PATHS 129 | 130 | (defn path? [x] 131 | (and (vector? x) 132 | (every? key? x))) 133 | 134 | (defn pathset? [x] 135 | (and (vector? x) 136 | (every? keyset? x))) 137 | 138 | (defn expand-pathset 139 | [[keyset & pathset]] 140 | (if (seq pathset) 141 | (for [key (keys keyset) 142 | path (expand-pathset pathset)] 143 | (into [key] path)) 144 | (map vector (keys keyset)))) 145 | 146 | (defn expand-pathset' 147 | [pathset] 148 | (letfn [(inner-expand [path [keyset & pathset]] 149 | (let [paths (map #(conj path %) (keys keyset))] 150 | (if (seq pathset) 151 | (mapcat #(inner-expand % pathset) paths) 152 | paths)))] 153 | (inner-expand [] pathset))) 154 | 155 | (defn expand-pathset'' 156 | [pathset] 157 | (letfn [(walk-pathset [expansions path [keyset & pathset]] 158 | (let [paths (into [] (map #(conj path %)) (keys keyset))] 159 | (if (seq pathset) 160 | (reduce (fn [expansions path] 161 | (walk-pathset expansions path pathset)) 162 | expansions 163 | paths) 164 | (reduce (fn [expansions path] 165 | (conj! expansions path)) 166 | expansions 167 | paths))))] 168 | (persistent! (walk-pathset (transient []) [] pathset)))) 169 | 170 | (defn expand-pathset''' 171 | [pathset] 172 | (letfn [(pathset-seq [path [keyset & pathset]] 173 | (let [paths (into [] (map #(conj path %)) (keys keyset))] 174 | (if (seq pathset) 175 | (mapcat #(pathset-seq % pathset) paths) 176 | paths)))] 177 | (into [] (pathset-seq [] pathset)))) 178 | 179 | (defn expand-pathsets [pathsets] 180 | (into [] (mapcat expand-pathset''' pathsets))) 181 | 182 | (def leaf ::leaf) 183 | (defn leaf? [v] (= leaf v)) 184 | 185 | (defn pathsets [tree] 186 | (letfn [(inner-pathsets [path tree] 187 | (mapcat (fn [[sub-tree kvs]] 188 | (let [path (conj path (keyset (map first kvs)))] 189 | (if (leaf? sub-tree) 190 | [path] 191 | (inner-pathsets path sub-tree)))) 192 | (group-by second tree)))] 193 | (inner-pathsets [] tree))) 194 | 195 | (defn length-tree' 196 | [length pathsets] 197 | (letfn [(keys [keyset] 198 | (if (vector? keyset) 199 | keyset 200 | [keyset])) 201 | (pathset-tree [tree [ks & pathset]] 202 | (if (seq pathset) 203 | (reduce (fn [tree k] 204 | (update tree k pathset-tree pathset)) 205 | tree 206 | (keys ks)) 207 | (reduce (fn [tree k] 208 | (assoc tree k leaf)) 209 | tree 210 | (keys ks))))] 211 | [length (reduce pathset-tree {} pathsets)])) 212 | 213 | (defn length-tree 214 | [pathsets] 215 | (into {} 216 | (map (fn [[length tree]] (length-tree' length tree))) 217 | (group-by count pathsets))) 218 | 219 | (defn length-tree-pathsets 220 | [length-tree] 221 | (mapcat (comp pathsets second) length-tree)) 222 | 223 | (defn tree [pathsets] 224 | (loop [[p & ps] (expand-pathsets pathsets) tree {}] 225 | (if p 226 | (recur ps (assoc-in tree p leaf)) 227 | tree))) 228 | 229 | (defn collapse [pathsets] 230 | (length-tree-pathsets (length-tree pathsets))) 231 | 232 | (defn assert-inner-reference 233 | [cache ref] 234 | (loop [cache cache [key & rest] ref] 235 | (if (ref? cache) 236 | (throw (ex-info "inner reference" {:path ref})) 237 | (when (seq rest) 238 | (recur (clojure.core/get cache key) rest))))) 239 | 240 | ;; TODO: redo this ... 241 | (defn optimize* [root cache [key & rest :as path] optimized] 242 | (cond 243 | (ref? cache) (let [ref-path (:path cache)] 244 | (assert-inner-reference root ref-path) 245 | (if (seq? path) 246 | (optimize* root root (concat ref-path path) []) 247 | [ref-path])) 248 | 249 | (atom? cache) [] 250 | :else (mapcat (fn [key] 251 | (if (map? cache) 252 | (if (contains? cache key) 253 | (optimize* root (clojure.core/get cache key) rest (conj optimized key)) 254 | [(into [] (concat optimized [key] rest))]) 255 | [])) 256 | (keys key)))) 257 | 258 | (defn optimize 259 | [cache paths] 260 | (mapcat (fn [path] 261 | (optimize* cache cache path [])) 262 | paths)) 263 | 264 | (defn pathmap-paths 265 | [pathmap] 266 | (letfn [(paths [path pathmap] 267 | (if (and (map? pathmap) 268 | (not (boxed? pathmap))) 269 | (mapcat (fn [[key pathmap]] 270 | (paths (conj path key) 271 | pathmap)) 272 | pathmap) 273 | [path]))] 274 | (paths [] pathmap))) 275 | 276 | (defn branch? [x] 277 | (and (map? x) 278 | (not (boxed? x)))) 279 | 280 | (defn pathmap-values 281 | [pathmap] 282 | (letfn [(path-values [path pathmap] 283 | (if (branch? pathmap) 284 | (mapcat (fn [[key pathmap]] 285 | (path-values (conj path key) 286 | pathmap)) 287 | pathmap) 288 | [(path-value path pathmap)]))] 289 | (path-values [] pathmap))) 290 | 291 | (defn join? 292 | [x] 293 | (and (map? x) 294 | (= 1 (count x)) 295 | (vector? (second (first x))))) 296 | 297 | (defn query? 298 | [x] 299 | (vector? x)) 300 | 301 | (defn prepend-query 302 | ([path] 303 | (prepend-query path nil)) 304 | ([[k & path] query] 305 | (cond 306 | (seq path) [{k (prepend-query path query)}] 307 | (seq query) [{k query}] 308 | :else [k]))) 309 | 310 | ;; TODO: this has proved useful -- make it robust! 311 | (defn query-pathsets [query] 312 | (letfn [(create-range [min max] 313 | (cond 314 | ;; TODO: replace with a range/max when we got that 315 | (and (nil? min) (nil? max)) (range 0 100) 316 | (and (nil? max)) (range 0 min) 317 | :else (range min max))) 318 | (expand-ranges [key] 319 | (cond 320 | (list? key) 321 | (let [[name key min max] key] 322 | (assert (= name 'range)) 323 | {key [(create-range min max)]}) 324 | 325 | (and (map? key) 326 | (list? (ffirst key))) 327 | (let [[[name key min max] query] (first key)] 328 | (assert (= name 'range)) 329 | {key [{(create-range min max) query}]}) 330 | 331 | :else key))] 332 | (let [query (map expand-ranges query) 333 | leafs (into [] (remove map? query)) 334 | paths (for [join (filter map? query) 335 | :let [[key query] (first join)] 336 | paths (query-pathsets query)] 337 | (into [key] paths))] 338 | (cond-> (into [] paths) 339 | (seq leafs) (conj [leafs]))))) 340 | 341 | (defn eval-query 342 | [query] 343 | (letfn [(expand-range [[name key min max] query] 344 | (assert (= name 'range)) 345 | (let [range (cond 346 | (and (nil? min) (nil? max)) (range 0 100) 347 | (and (nil? max)) (range 0 min) 348 | :else (range min max))] 349 | (if (seq query) 350 | {key [{range query}]} 351 | {key [range]})))] 352 | (into [] 353 | (map (fn [key] 354 | (cond 355 | (list? key) (expand-range key nil) 356 | (join? key) (let [[key query] (first key)] 357 | (if (list? key) 358 | (expand-range key (eval-query query)) 359 | {key (eval-query query)})) 360 | :else key))) 361 | query))) 362 | 363 | (defn tree-query 364 | [tree] 365 | (reduce (fn [query [key tree]] 366 | (if (leaf? tree) 367 | (conj query key) 368 | (conj query {key (tree-query tree)}))) 369 | [] 370 | tree)) 371 | 372 | (defn pathsets-query [pathsets] 373 | (letfn [(keys [keyset] 374 | (if (vector? keyset) 375 | keyset 376 | [keyset])) 377 | (merge-query-tree [tree [ks & pathset]] 378 | (if (seq pathset) 379 | (reduce (fn [tree k] 380 | (update tree k merge-query-tree pathset)) 381 | tree 382 | (keys ks)) 383 | (reduce (fn [tree k] 384 | (assoc tree k leaf)) 385 | tree 386 | (keys ks))))] 387 | (tree-query (reduce merge-query-tree 388 | {} 389 | pathsets)))) 390 | -------------------------------------------------------------------------------- /src/cambo/graph.cljc: -------------------------------------------------------------------------------- 1 | (ns cambo.graph 2 | (:refer-clojure :exclude [get set atom ref keys]) 3 | (:require [cambo.core :refer [branch? boxed? atom atom? ref? keys] :as core])) 4 | 5 | (defn get 6 | ([graph pathsets] 7 | (get graph pathsets {:normalize false 8 | :path-info false 9 | :boxed false})) 10 | ([graph pathsets {:keys [normalize boxed path-info]}] 11 | (letfn [(get-pathset [node path opt result [ks & pathset :as ps]] 12 | ;; TODO: can simplify -- have tests / look at pull 13 | (if-not (branch? node) 14 | (update result :missing conj (into opt ps)) 15 | (reduce (fn [result k] 16 | (if (and (branch? node) 17 | (not (contains? node k))) 18 | (update result :missing conj (into (conj opt k) pathset)) 19 | (let [node (clojure.core/get node k) 20 | path (conj path k) 21 | opt (conj opt k) 22 | result (if (and path-info 23 | (branch? node)) 24 | (assoc-in result 25 | (into [:graph] (conj path :cambo/path)) 26 | opt) 27 | result)] 28 | (if (seq pathset) 29 | (cond 30 | (ref? node) 31 | (let [result (if normalize 32 | (assoc-in result (into [:graph] opt) node) 33 | result) 34 | opt (:path node) 35 | result (if path-info 36 | (assoc-in result 37 | (into [:graph] (conj path :cambo/path)) 38 | opt) 39 | result) 40 | node (get-in graph opt)] 41 | (get-pathset node path opt result pathset)) 42 | 43 | (branch? node) 44 | (get-pathset node path opt result pathset) 45 | 46 | (nil? node) 47 | (assoc-in result (into [:graph] (if normalize opt path)) 48 | (if boxed 49 | (atom node) 50 | node)) 51 | 52 | (core/non-value? node) 53 | result 54 | 55 | (atom? node) 56 | (assoc-in result (into [:graph] (if normalize opt path)) 57 | (if boxed 58 | node 59 | (:value node))) 60 | 61 | :else 62 | (assoc-in result (into [:graph] (if normalize opt path)) 63 | (if boxed 64 | (atom node) 65 | node))) 66 | (cond 67 | (branch? node) result 68 | (atom? node) (assoc-in result (into [:graph] (if normalize opt path)) 69 | (if boxed 70 | node 71 | (:value node))) 72 | (ref? node) (if path-info 73 | (let [opt (:path node)] 74 | (assoc-in result 75 | (into [:graph] (conj path :cambo/path)) 76 | opt)) 77 | result) 78 | :else (assoc-in result (into [:graph] (if normalize opt path)) 79 | (if boxed 80 | (atom node) 81 | node))))))) 82 | result 83 | (keys ks))))] 84 | (reduce (partial get-pathset graph [] []) 85 | {:graph {} 86 | :missing []} 87 | pathsets)))) 88 | 89 | (defn pull 90 | ([cache query] 91 | (pull cache query {:normalize false 92 | :path-info false 93 | :boxed false})) 94 | ([cache query {:keys [normalize boxed path-info]}] 95 | ;; overhead of atoms? 96 | (let [refs (clojure.core/atom []) 97 | missing (clojure.core/atom [])] 98 | (letfn [(add-ref! [path query] 99 | (swap! refs conj (core/prepend-query path query))) 100 | (add-missing! [path query] 101 | (swap! missing into (core/prepend-query path query))) 102 | (set-value [result k node query] 103 | (cond 104 | (branch? node) result 105 | (core/non-value? node) result 106 | (ref? node) (do 107 | (add-ref! (:path node) query) 108 | (assoc result k node)) 109 | :else (assoc result k (cond 110 | (atom? node) (if boxed node (:value node)) 111 | :else (if boxed (atom node) node))))) 112 | (set-path [result k path] 113 | (if path-info 114 | (assoc-in result [k :cambo/path] path) 115 | result)) 116 | (inner-query [node result path query] 117 | (if-not (branch? node) 118 | (do (add-missing! path query) 119 | result) 120 | (reduce (fn [result k] 121 | (let [[k query] (if (core/join? k) 122 | (first k) 123 | [k []])] 124 | (reduce (fn [result k] 125 | (if-not (contains? node k) 126 | (do (add-missing! (conj path k) query) 127 | result) 128 | (let [node (clojure.core/get node k) 129 | path (conj path k) 130 | result (if (branch? node) 131 | (set-path result k path) 132 | result)] 133 | (cond 134 | (and (branch? node) 135 | (seq query)) 136 | (let [inner-result (inner-query node (clojure.core/get result k) path query)] 137 | (if (or (seq inner-result) 138 | (contains? result k)) 139 | (assoc result k inner-result) 140 | result)) 141 | 142 | ;; TODO: revisit refs ;p 143 | (and (ref? node) 144 | (not normalize)) 145 | (let [ref-path (:path node) 146 | ;; TODO: we need to assert this ref path too 147 | ref-result (inner-query (get-in cache ref-path) (clojure.core/get result k) ref-path query)] 148 | (-> result 149 | (assoc k ref-result) 150 | (set-path k ref-path))) 151 | 152 | :else (set-value result k node query))))) 153 | result 154 | (keys k)))) 155 | result 156 | query)))] 157 | (loop [result (inner-query cache {} [] query)] 158 | (let [refs' @refs] 159 | (reset! refs []) 160 | (if (seq refs') 161 | (recur (reduce #(inner-query cache %1 [] %2) 162 | result 163 | refs')) 164 | {:graph result 165 | :missing @missing}))))))) 166 | 167 | (defn missing-transient 168 | [graph pathsets] 169 | (letfn [(missing-pathset [node path missing [ks & pathset :as ps]] 170 | (if (or (nil? node) 171 | (empty? node)) 172 | (conj! missing ps) 173 | (let [] 174 | (reduce (fn [missing k] 175 | (if (and (branch? node) 176 | (not (contains? node k))) 177 | (conj! missing (into (conj path k) pathset)) 178 | (let [node (clojure.core/get node k) 179 | path (conj path k)] 180 | (cond 181 | (ref? node) 182 | (let [path (:path node) 183 | node (get-in graph path)] 184 | (missing-pathset node path missing pathset)) 185 | 186 | (and (branch? node) 187 | (seq pathsets)) 188 | (missing-pathset node path missing pathset) 189 | 190 | :else missing)))) 191 | missing 192 | (keys ks)))))] 193 | (persistent! (reduce (partial missing-pathset graph []) 194 | (transient []) 195 | pathsets)))) 196 | 197 | (defn missing 198 | [graph pathsets] 199 | (letfn [(missing-pathset [node path [ks & pathset :as ps]] 200 | (if (or (nil? node) 201 | (empty? node)) 202 | [ps] 203 | (mapcat (fn [k] 204 | (if (and (branch? node) 205 | (not (contains? node k))) 206 | [(into (conj path k) pathset)] 207 | (let [node (clojure.core/get node k) 208 | path (conj path k)] 209 | (cond 210 | (ref? node) 211 | (let [path (:path node) 212 | node (get-in graph path)] 213 | (missing-pathset node path pathset)) 214 | 215 | (and (branch? node) 216 | (seq pathsets)) 217 | (missing-pathset node path pathset) 218 | 219 | :else [])))) 220 | ;; TODO: potential area of optimization around ranges (diff'ing pre-expansion) 221 | (keys ks))))] 222 | (vec (mapcat (partial missing-pathset graph []) 223 | pathsets)))) 224 | 225 | (defn get-value 226 | [graph path] 227 | (let [{:keys [graph]} (get graph [path])] 228 | (get-in graph path))) 229 | 230 | (defn set 231 | ([graph pathmaps] 232 | (set graph pathmaps {})) 233 | ;; TODO: maybe boxed vs unboxed option? 234 | ([graph pathmaps _] 235 | (letfn [(set-pathmap [graph path opt pathmap] 236 | (let [node (get-in graph (into [:graph] opt)) 237 | graph (cond-> graph 238 | (or (atom? node) 239 | (not (map? node))) 240 | (assoc-in (into [:graph] path) {})) 241 | opt (if (ref? node) 242 | (:value node) 243 | opt)] 244 | (reduce (fn [graph k] 245 | (let [value (clojure.core/get pathmap k) 246 | path' (conj path k) 247 | opt' (conj opt k)] 248 | (cond 249 | (ref? value) 250 | (assoc-in graph (into [:graph] opt') value) 251 | 252 | (atom? value) 253 | (-> graph 254 | (update :paths conj path') 255 | (assoc-in (into [:graph] opt') value)) 256 | 257 | (map? value) 258 | (set-pathmap graph path' opt' value) 259 | 260 | :else 261 | (-> graph 262 | (update :paths conj path') 263 | (assoc-in (into [:graph] opt') (atom value)))))) 264 | graph 265 | (clojure.core/keys pathmap))))] 266 | (reduce #(set-pathmap %1 [] [] %2) 267 | {:graph graph :paths []} 268 | pathmaps)))) 269 | 270 | (defn set-path-value 271 | ([graph {:keys [path value]}] 272 | (set-path-value graph path value)) 273 | ([graph [k & path] value] 274 | (let [graph (if (or (boxed? graph) 275 | (not (map? graph))) 276 | {} 277 | graph)] 278 | (if (seq path) 279 | (assoc graph k (set-path-value (clojure.core/get graph k) path value)) 280 | (assoc graph k (if (boxed? value) 281 | value 282 | (atom value))))))) 283 | 284 | (defn invalidate 285 | [graph paths] 286 | ;; TODO: could remove empty branches post-walk style 287 | (letfn [(invalidate-path [graph path] 288 | (let [key (last path) 289 | path (butlast path)] 290 | (if (seq path) 291 | (update-in graph path dissoc key) 292 | (dissoc graph key))))] 293 | {:graph (reduce invalidate-path graph paths) 294 | :paths paths})) 295 | 296 | (defrecord GraphDataSource [graph]) 297 | 298 | ;; have to extend type due to get / set names clashing with core ... oops! 299 | (extend-type GraphDataSource 300 | core/IDataSource 301 | (pull [{:keys [graph]} query cb] 302 | (cb (pull @graph query {:normalize true 303 | :boxed true})) 304 | nil) 305 | (set [{:keys [graph]} pathmaps cb] 306 | #?(:cljs (let [ps (clojure.core/atom nil)] 307 | (let [g (swap! graph (fn [graph] 308 | (let [{:keys [graph paths] :as r} (set graph pathmaps)] 309 | (reset! ps paths) 310 | graph))) 311 | {:keys [graph]} (get g @ps {:normalize true 312 | :boxed true})] 313 | (cb {:graph graph 314 | :paths @ps}))) 315 | :clj (with-local-vars [ps nil] 316 | (let [g (swap! graph (fn [graph] 317 | (let [{:keys [graph paths] :as r} (set graph pathmaps)] 318 | (var-set ps paths) 319 | graph))) 320 | {:keys [graph]} (get g @ps {:normalize true 321 | :boxed true})] 322 | (cb {:graph graph 323 | :paths @ps})))) 324 | nil) 325 | (call [_ _ _ _ _] 326 | (throw (ex-info "not implemented" {:method :call})))) 327 | 328 | (defn as-datasource [graph] 329 | (GraphDataSource. (clojure.core/atom (:graph (set {} [graph]))))) 330 | -------------------------------------------------------------------------------- /src/cambo/http.cljs: -------------------------------------------------------------------------------- 1 | (ns cambo.http 2 | (:require [cognitect.transit :as transit] 3 | [cambo.core :as core]) 4 | (:import [goog.net XhrIo])) 5 | 6 | (deftype RecordHandler [t] 7 | Object 8 | (tag [this v] t) 9 | (rep [this v] (into {} v))) 10 | 11 | (def write-handlers 12 | {core/Range (RecordHandler. "cambo.core.Range") 13 | core/Atom (RecordHandler. "cambo.core.Atom") 14 | core/Ref (RecordHandler. "cambo.core.Ref")}) 15 | 16 | 17 | (def read-handlers 18 | {"cambo.core.Range" (fn [{:keys [start end]}] 19 | (core/range start end)) 20 | "cambo.core.Ref" (fn [{:keys [path]}] 21 | (core/ref path)) 22 | "cambo.core.Atom" (fn [v] 23 | (if (contains? v :value) 24 | (core/atom (:value v)) 25 | (core/atom)))}) 26 | 27 | (defn ->transit 28 | [edn] 29 | (let [writer (transit/writer :json {:handlers write-handlers})] 30 | (transit/write writer edn))) 31 | 32 | (defn ->edn 33 | [transit] 34 | (let [reader (transit/reader :json {:handlers read-handlers})] 35 | (transit/read reader transit))) 36 | 37 | (defn http-send 38 | [url request headers cb] 39 | (let [default-headers {"Content-Type" "application/transit+json"} 40 | headers (cond-> default-headers 41 | (fn? headers) (merge (headers request)) 42 | (map? headers) (merge headers)) 43 | headers (clj->js headers)] 44 | (.send XhrIo url 45 | (fn [_] 46 | (this-as this 47 | ;; TODO: handle errors 48 | (when (= (.getStatus this) 200) 49 | (let [response (->edn (.getResponseText this))] 50 | (cb response))))) 51 | "POST" 52 | (->transit request) 53 | headers))) 54 | 55 | (defrecord HttpDataSource 56 | [url opts] 57 | core/IDataSource 58 | (pull [_ query cb] 59 | (http-send url {:method :pull :query query} opts cb)) 60 | (set [_ pathmaps cb] 61 | (http-send url {:method :set :pathmaps pathmaps} opts cb)) 62 | (call [_ path args queries cb] 63 | (http-send url {:method :call :path path :args args :queries queries} opts cb))) 64 | 65 | (defn http-datasource 66 | ([url] 67 | (HttpDataSource. url nil)) 68 | ([url opts] 69 | (HttpDataSource. url opts))) 70 | -------------------------------------------------------------------------------- /src/cambo/model.cljc: -------------------------------------------------------------------------------- 1 | (ns cambo.model 2 | (:refer-clojure :exclude [get set force]) 3 | (:require [cambo.core :as core] 4 | [cambo.graph :as graph])) 5 | 6 | (defrecord Model 7 | [cache datasource subscribers]) 8 | 9 | (defn model [{:keys [cache] :as opts}] 10 | (map->Model (assoc opts :cache (atom (->> [(or cache {})] 11 | (graph/set {}) 12 | :graph)) 13 | :subscribers (atom {})))) 14 | 15 | (defn with-path-info [model] 16 | (assoc model :path-info true)) 17 | 18 | (defn get-options 19 | ([model] 20 | (get-options model nil)) 21 | ([model opts] 22 | (merge (select-keys model [:path-info :boxed]) 23 | {:normalize false} 24 | opts))) 25 | 26 | (defn get-cache 27 | ([{:keys [cache]}] 28 | @cache) 29 | ([{:keys [cache] :as m} pathsets] 30 | (let [c @cache] 31 | (:graph (graph/get c pathsets (get-options m)))))) 32 | 33 | (defn pull-cache 34 | [{:keys [cache] :as m} query] 35 | (let [c @cache] 36 | (:graph (graph/pull c query (get-options m))))) 37 | 38 | (defn publish [{:keys [subscribers]}] 39 | (let [subs @subscribers] 40 | (doseq [[_ cb] subs] 41 | (cb)))) 42 | 43 | (defn unsubscribe [{:keys [subscribers]} id] 44 | (swap! subscribers dissoc id) 45 | nil) 46 | 47 | (def subscriber-id (atom 0)) 48 | 49 | (defn subscribe [{:keys [subscribers] :as model} pathsets cb] 50 | (let [id (swap! subscriber-id inc)] 51 | (swap! subscribers assoc id cb) 52 | (fn [] 53 | (unsubscribe model id)))) 54 | 55 | (defn set-cache* 56 | [{:keys [cache] :as m} pathmaps] 57 | (swap! cache (fn [cache] 58 | (let [{:keys [graph]} (graph/set cache pathmaps)] 59 | graph))) 60 | (publish m) 61 | nil) 62 | 63 | (defn set-cache 64 | [{:keys [cache] :as m} pathmaps] 65 | #?(:cljs (let [ps (atom nil) 66 | c (swap! cache (fn [cache] 67 | (let [{:keys [graph paths]} (graph/set cache pathmaps)] 68 | (reset! ps paths) 69 | graph)))] 70 | (publish m) 71 | (:graph (graph/get c @ps {:normalize false}))) 72 | :clj (with-local-vars [ps nil] 73 | (let [c (swap! cache (fn [cache] 74 | (let [{:keys [graph paths]} (graph/set cache pathmaps)] 75 | (var-set ps paths) 76 | graph)))] 77 | (publish m) 78 | (:graph (graph/get c @ps {:normalize false})))))) 79 | 80 | (defn invalidate-cache 81 | [{:keys [cache]} paths] 82 | (swap! cache (fn [cache] 83 | (:graph (graph/invalidate cache paths)))) 84 | paths) 85 | 86 | (defn pull 87 | #?(:clj ([model query] 88 | (pull model query (fn [_])))) 89 | ([{:keys [cache datasource] :as m} query cb] 90 | (let [{:keys [graph missing]} (graph/pull @cache query (get-options m)) 91 | #?@(:clj [response (promise) 92 | cb (fn [graph] 93 | (cb graph) 94 | (deliver response graph))])] 95 | (if (empty? missing) 96 | (cb graph) 97 | (core/pull datasource missing (fn [{:keys [graph]}] 98 | (set-cache m [graph]) 99 | (cb (:graph (graph/pull @cache query (get-options m))))))) 100 | #?(:clj response 101 | :cljs nil)))) 102 | 103 | (defn set 104 | [{:keys [datasource] :as m} pathmaps cb] 105 | (set-cache m pathmaps) 106 | (core/set datasource pathmaps (fn [{:keys [graph]}] 107 | ;; TODO: this could be partial -- but can't trust datasource paths yet (router) 108 | (set-cache m [graph]) 109 | (cb (get-cache m (core/pathmap-paths pathmaps)))))) 110 | 111 | (defn call 112 | [{:keys [datasource] :as m} path args queries cb] 113 | (core/call datasource path args queries (fn [{:keys [graph invalidate]}] 114 | (invalidate-cache m invalidate) 115 | (set-cache m [graph]) 116 | (cb graph)))) 117 | 118 | (defn prime 119 | [{:keys [cache datasource] :as m} query cb] 120 | (let [{:keys [missing]} (graph/pull @cache query (get-options m))] 121 | (if (seq missing) 122 | (core/pull datasource missing (fn [{:keys [graph]}] 123 | (set-cache* m [graph]) 124 | (cb true))) 125 | (cb {:ready true})))) 126 | 127 | (defn force 128 | [{:keys [cache datasource] :as m} query cb] 129 | (core/pull datasource query (fn [{:keys [graph]}] 130 | (set-cache* m [graph]) 131 | (cb true)))) 132 | 133 | ;; TODO: rest of falcor model interface as necessary 134 | -------------------------------------------------------------------------------- /src/cambo/profile.cljc: -------------------------------------------------------------------------------- 1 | (ns cambo.profile 2 | (:require [cambo.utils :as utils])) 3 | 4 | (def ^:dynamic *profile* true) 5 | 6 | (defn p-cljs [tag body] 7 | (if *profile* 8 | (let [tag (str tag)] 9 | `(let [measure# ~tag 10 | mark# (str (gensym ~tag)) 11 | _# (js/performance.mark mark#) 12 | start# (js/performance.now) 13 | result# (do ~@body) 14 | duration# (- (js/performance.now) start#)] 15 | (when (>= duration# 0.1) 16 | (js/performance.measure measure# mark#)) 17 | (js/performance.clearMeasures measure#) 18 | (js/performance.clearMarks mark#) 19 | result#)) 20 | `(do ~@body))) 21 | 22 | #?(:clj 23 | (defmacro p [tag & body] 24 | `(utils/if-cljs 25 | ~(p-cljs tag body) 26 | (do ~@body)))) 27 | 28 | (defn pa-cljs [tag done body] 29 | (if *profile* 30 | (let [tag (str tag)] 31 | `(let [measure# ~tag 32 | mark# (str (gensym ~tag)) 33 | _# (js/performance.mark mark#) 34 | start# (js/performance.now) 35 | done# (fn [] 36 | (let [duration# (- (js/performance.now) start#)] 37 | (when (>= duration# 0.1) 38 | (js/performance.measure measure# mark#)) 39 | (js/performance.clearMeasures measure#) 40 | (js/performance.clearMarks mark#)))] 41 | (let [~done done#] 42 | (do ~@body)))) 43 | `(do ~@body))) 44 | 45 | #?(:clj 46 | (defmacro pa [tag [done] & body] 47 | (utils/if-cljs 48 | (pa-cljs tag done body) 49 | `(do ~@body)))) 50 | 51 | (comment 52 | 53 | 54 | (binding [*profile* true] 55 | (macroexpand-1 '(p :foo 56 | (println "foo") 57 | (model/pull [:query])))) 58 | 59 | (binding [*profile* true] 60 | (macroexpand-1 '(pa :foo [xxx] 61 | (println "foo") 62 | (model/pull [:query] (fn [_] 63 | (xxx)))))) 64 | 65 | ) 66 | -------------------------------------------------------------------------------- /src/cambo/ring.clj: -------------------------------------------------------------------------------- 1 | (ns cambo.ring 2 | (:require [cognitect.transit :as transit] 3 | [cambo.core]) 4 | (:import [java.io ByteArrayOutputStream] 5 | [cambo.core Range Atom Ref])) 6 | 7 | (def read-handlers 8 | (transit/record-read-handlers Range Atom Ref)) 9 | 10 | (def write-handlers 11 | (transit/record-write-handlers Range Atom Ref)) 12 | 13 | (defn wrap-cambo 14 | [handler] 15 | ;; TODO: not sure how much we want to throw vs not throw? 16 | ;; look at other ring middleware for inspiration :) 17 | (fn [{:keys [body] :as request}] 18 | (let [reader (transit/reader body :json {:handlers read-handlers}) 19 | cambo (transit/read reader) 20 | request (cond-> request 21 | cambo (assoc :cambo cambo)) 22 | response (handler request)] 23 | (if cambo 24 | (let [out (ByteArrayOutputStream.) 25 | writer (transit/writer out :json {:handlers write-handlers}) 26 | _ (transit/write writer response)] 27 | {:status 200 28 | :headers {"Content-Type" "application/transit+json"} 29 | :body (.toString out)}) 30 | response)))) 31 | -------------------------------------------------------------------------------- /src/cambo/router.cljc: -------------------------------------------------------------------------------- 1 | (ns cambo.router 2 | (:refer-clojure :exclude [get set]) 3 | (:require [cambo.core :as core] 4 | [cambo.graph :as graph] 5 | [cambo.core :as cam])) 6 | 7 | ;;; ROUTES 8 | 9 | (defprotocol IRouteKey 10 | (strip [this keyset])) 11 | 12 | (defn strip-collect 13 | [route-key coll] 14 | (reduce (fn [[int diff] keyset] 15 | (let [[int' diff'] (strip route-key keyset)] 16 | [(into int int') 17 | (into diff diff')])) 18 | [[] []] 19 | coll)) 20 | 21 | (def KEYS 22 | (reify 23 | IRouteKey 24 | (strip [this keyset] 25 | (if (vector? keyset) 26 | [keyset []] 27 | [[keyset] []])))) 28 | 29 | (def INTEGERS 30 | (reify 31 | IRouteKey 32 | (strip [this keyset] 33 | (cond 34 | (integer? keyset) [[keyset] []] 35 | (core/range? keyset) [[keyset] []] 36 | (core/key? keyset) [[] [keyset]] 37 | (vector? keyset) (strip-collect this keyset) 38 | :else (strip-collect this (core/keys keyset)))))) 39 | 40 | (def RANGES 41 | (reify 42 | IRouteKey 43 | (strip [this keyset] 44 | (cond 45 | (integer? keyset) [[keyset] []] 46 | (core/range? keyset) [[keyset] []] 47 | (core/key? keyset) [[] [keyset]] 48 | (vector? keyset) (strip-collect this keyset) 49 | :else (strip-collect this (core/keys keyset)))))) 50 | 51 | (defn virtual-key? [x] 52 | (or (= RANGES x) 53 | (= INTEGERS x) 54 | (= KEYS x))) 55 | 56 | (defn route-key? [x] 57 | (or (virtual-key? x) 58 | (core/key? x))) 59 | 60 | (defn route-keyset? [x] 61 | (or (route-key? x) 62 | (and (vector? x) 63 | ;; virtual keys not allowed in keysets -- only top level 64 | ;; ranges not allowed at all 65 | (every? core/key? x)))) 66 | 67 | (defn route-keys [x] 68 | (cond 69 | (or (virtual-key? x) 70 | (core/key? x)) [x] 71 | (vector? x) x)) 72 | 73 | (defn route? [x] 74 | (and (vector? x) 75 | (every? route-key? x))) 76 | 77 | (defn routeset? [x] 78 | (and (vector? x) 79 | (every? route-keyset? x))) 80 | 81 | (defn route-hash [route] 82 | (into [] (for [key route] 83 | (condp = key 84 | RANGES ::i 85 | INTEGERS ::i 86 | KEYS ::k 87 | key)))) 88 | 89 | (defn expand-routeset [[route-keyset & routeset]] 90 | (if (seq routeset) 91 | (for [key (route-keys route-keyset) p (expand-routeset routeset)] 92 | (into [key] p)) 93 | (map vector (route-keys route-keyset)))) 94 | 95 | (defn assert-route-hashes 96 | [handlers] 97 | (doall (->> handlers 98 | (mapcat (fn [[{:keys [get set call]} route]] 99 | (let [hash (route-hash route)] 100 | (cond-> [] 101 | get (conj (conj hash ::get)) 102 | set (conj (conj hash ::set)) 103 | call (conj (conj hash ::call)))))) 104 | (reduce (fn [hashes hash] 105 | (when (contains? hashes hash) 106 | (throw (ex-info "route conflict" {:hash hash}))) 107 | (conj hashes hash)) 108 | #{})))) 109 | 110 | (defonce route-ids (atom 0)) 111 | (def id-key ::id) 112 | (def match-key ::match) 113 | 114 | ;; rename handler - route-record / route-object ... handler no beuno 115 | (defn route-tree 116 | [handlers] 117 | (let [handlers (concat (for [{:keys [route] :as handler} handlers 118 | ;; falcor does id per call / set / get -- but don't think it matters? 119 | :let [handler (assoc handler id-key (swap! route-ids inc))] 120 | route (expand-routeset route)] 121 | [handler route]))] 122 | (assert-route-hashes handlers) 123 | (loop [tree {} handlers handlers] 124 | (if-let [[{:keys [get set call] :as handler} route] (first handlers)] 125 | (recur (cond-> tree 126 | get (assoc-in (into route [match-key :get]) handler) 127 | set (assoc-in (into route [match-key :set]) handler) 128 | call (assoc-in (into route [match-key :call]) handler)) 129 | (rest handlers)) 130 | tree)))) 131 | 132 | (defn branches [keyset tree] 133 | (let [keys (into [] (core/keys keyset)) 134 | ints (into [] (filter integer? keys)) 135 | route-keys (cond-> (conj keys KEYS) 136 | (seq ints) (conj INTEGERS RANGES)) 137 | branch-keys (select-keys tree route-keys)] 138 | (map (fn [[route-key sub-tree]] 139 | (let [matched-keys (condp = route-key 140 | INTEGERS ints 141 | RANGES ints 142 | KEYS keys 143 | route-key)] 144 | [matched-keys route-key sub-tree])) 145 | branch-keys))) 146 | 147 | (defn match* [tree method pathset request virtual] 148 | ;; set / call only match full paths, get does the path optimization 149 | (let [method' (if (seq pathset) 150 | :get 151 | method) 152 | match (when-let [handler (get-in tree [::match method'])] 153 | {:method method' 154 | :request request 155 | :virtual virtual 156 | :suffix (vec pathset) 157 | :handler handler})] 158 | 159 | (cond-> [] 160 | match 161 | (conj match) 162 | 163 | (seq pathset) 164 | (into (mapcat (fn [[route-key virtual-key sub-tree]] 165 | (match* sub-tree 166 | method 167 | (rest pathset) 168 | (conj request route-key) 169 | (conj virtual virtual-key))) 170 | (branches (first pathset) tree)))))) 171 | 172 | (defn collapse-matches 173 | [matches] 174 | (let [{:keys [method handler virtual suffix]} (first matches) 175 | pathsets (core/collapse (map :request matches))] 176 | (into [] (for [pathset pathsets] 177 | {:method method 178 | :handler handler 179 | :request pathset 180 | :suffix suffix 181 | :virtual (mapv (fn [vkey key] 182 | (condp = vkey 183 | RANGES vkey 184 | KEYS vkey 185 | INTEGERS vkey 186 | key)) 187 | virtual pathset)})))) 188 | 189 | (defn match [tree method pathset] 190 | (let [matches (match* tree method pathset [] []) 191 | reduced (group-by (comp id-key :handler) matches) 192 | collapsed (into [] (mapcat (comp collapse-matches second) 193 | reduced))] 194 | collapsed)) 195 | 196 | (defn strip-primitive 197 | [prim keyset] 198 | (cond 199 | (= prim keyset) [[keyset] []] 200 | (vector? keyset) (strip-collect prim keyset) 201 | :else [[] [keyset]])) 202 | 203 | (extend-protocol IRouteKey 204 | #?(:clj clojure.lang.Keyword 205 | :cljs cljs.core.Keyword) 206 | (strip [this keyset] 207 | (strip-primitive this keyset)) 208 | #?(:clj String 209 | :cljs string) 210 | (strip [this keyset] 211 | (strip-primitive this keyset)) 212 | #?(:clj clojure.lang.Symbol 213 | :cljs cljs.core.Symbol) 214 | (strip [this keyset] 215 | (strip-primitive this keyset)) 216 | #?(:clj Number 217 | :cljs number) 218 | (strip [this keyset] 219 | (cond 220 | (= this keyset) [[keyset] []] 221 | (core/range? keyset) (let [{:keys [start end]} keyset] 222 | (cond 223 | (= this start) [[this] [(core/range (inc start) end)]] 224 | (< start this (dec end)) [[this] [(core/range start this) 225 | (core/range (inc this) end)]] 226 | (= this (dec end)) [[this] [(core/range start this)]] 227 | :else [[] [keyset]])) 228 | (vector? keyset) (strip-collect this keyset) 229 | :else [[] [keyset]])) 230 | #?(:clj clojure.lang.APersistentVector 231 | :cljs cljs.core.PersistentVector) 232 | (strip [this keyset] 233 | (reduce (fn [[int diff] route-key] 234 | (let [[int' diff'] (strip route-key diff)] 235 | [(into int int') 236 | diff'])) 237 | [[] (if (vector? keyset) 238 | keyset 239 | [keyset])] 240 | this))) 241 | 242 | (defn strip-path 243 | "assumes there is an intersection" 244 | [[routekey & routeset] [keyset & pathset :as all]] 245 | (let [[key-int key-diffs] (strip routekey keyset) 246 | key-int (if (= 1 (count key-int)) 247 | (first key-int) 248 | key-int) 249 | diffs (mapv (fn [key-diff] 250 | (into [key-diff] pathset)) 251 | key-diffs)] 252 | (if (seq routeset) 253 | (let [[path-int path-diffs] (strip-path routeset pathset)] 254 | [(into [key-int] path-int) 255 | (into diffs 256 | (map (fn [diff] (into [key-int] diff)) path-diffs))]) 257 | [[key-int] diffs]))) 258 | 259 | (defn intersects? 260 | [[routekey & route] [keyset & pathset]] 261 | (let [[int _] (strip routekey keyset)] 262 | (cond 263 | (empty? int) false 264 | (seq route) (intersects? route pathset) 265 | :else true))) 266 | 267 | (defn key-precedence [key] 268 | (condp = key 269 | KEYS 1 270 | INTEGERS 2 271 | RANGES 2 272 | 4)) 273 | 274 | (defn ranges [ns] 275 | (->> ns 276 | distinct 277 | sort 278 | (reduce (fn [acc n] 279 | (let [a (-> acc last last)] 280 | (if (or (nil? a) 281 | (not= n (inc a))) 282 | (conj acc [n]) 283 | (update acc (dec (count acc)) conj n)))) 284 | []) 285 | (map (fn [ns] 286 | (let [from (first ns) 287 | to (last ns)] 288 | (core/range from (inc to))))) 289 | (into []))) 290 | 291 | (defn conform-path [routeset pathset] 292 | (letfn [(conform-key [route-key keyset] 293 | (cond 294 | (= route-key KEYS) (into [] (core/keys keyset)) 295 | (= route-key INTEGERS) (into [] (core/keys keyset)) 296 | (= route-key RANGES) (ranges (core/keys keyset)) 297 | (vector? route-key) (if (vector? keyset) 298 | keyset 299 | [keyset]) 300 | :else (first (core/keys keyset))))] 301 | (into [] (map conform-key routeset pathset)))) 302 | 303 | (defn precedence [route] 304 | (mapv key-precedence route)) 305 | 306 | (def desc #(compare %2 %1)) 307 | 308 | (defn executable-matches 309 | [matches pathset] 310 | (letfn [(collect-matches [{:keys [virtual handler] :as match} pathsets] 311 | (reduce (fn [[results pathsets] pathset] 312 | (if (intersects? virtual pathset) 313 | (let [[intersection differences] (strip-path virtual pathset) 314 | route (:route handler)] 315 | [(conj results (assoc match :pathset (conform-path route intersection))) 316 | (into pathsets differences)]) 317 | [results (conj pathsets pathset)])) 318 | [[] []] 319 | pathsets))] 320 | (loop [matches (sort-by (comp precedence :virtual) desc matches) 321 | remaining-pathsets [pathset] 322 | results []] 323 | (if (and (seq remaining-pathsets) 324 | (seq matches)) 325 | (let [match (first matches) 326 | [match-results remaining-pathsets] (collect-matches match remaining-pathsets)] 327 | (recur (rest matches) 328 | (core/collapse remaining-pathsets) 329 | (into results match-results))) 330 | {:matches results 331 | :unhandled remaining-pathsets})))) 332 | 333 | (defn get-executable-matches 334 | [route-tree method pathsets] 335 | (reduce (fn [results pathset] 336 | (let [matches (match route-tree method pathset) 337 | {:keys [matches unhandled]} (executable-matches matches pathset)] 338 | (-> results 339 | (update :matches into matches) 340 | (update :unhandled into unhandled)))) 341 | {:matches [] 342 | :unhandled []} 343 | pathsets)) 344 | 345 | (defn indices [ranges] 346 | (mapcat core/keys ranges)) 347 | 348 | (defprotocol IRouteResult 349 | (update-context [this context match])) 350 | 351 | (extend-protocol IRouteResult 352 | cambo.core.PathValue 353 | (update-context [{:keys [path value] :as pv} context {:keys [suffix]}] 354 | (let [context (-> context 355 | (update :graph graph/set-path-value pv) 356 | (update :optimized conj path))] 357 | (cond-> context 358 | (and (core/ref? value) (seq suffix)) 359 | (update :pathsets conj (into (:path value) suffix))))) 360 | 361 | #?(:clj clojure.lang.APersistentMap 362 | :cljs cljs.core.PersistentArrayMap) 363 | (update-context [pathmap context match] 364 | (reduce (fn [context pv] 365 | (update-context pv context match)) 366 | context 367 | (core/pathmap-values pathmap))) 368 | 369 | #?(:cljs cljs.core.PersistentHashMap) 370 | #?(:cljs (update-context [pathmap context match] 371 | (reduce (fn [context pv] 372 | (update-context pv context match)) 373 | context 374 | (core/pathmap-values pathmap))))) 375 | 376 | (deftype Invalidate [path] 377 | IRouteResult 378 | (update-context [_ context _] 379 | (update context :invalidate conj path))) 380 | 381 | (defn invalidate [path] 382 | (Invalidate. path)) 383 | 384 | (deftype AdditionalPaths [pathsets] 385 | IRouteResult 386 | (update-context [_ context _] 387 | (update context :pathsets into pathsets))) 388 | 389 | (defn additional-paths [paths] 390 | (AdditionalPaths. paths)) 391 | 392 | (deftype SetMethod [method] 393 | IRouteResult 394 | (update-context [_ context _] 395 | (assoc context :method method))) 396 | 397 | (defn set-method [method] 398 | (SetMethod. method)) 399 | 400 | (defn merge-results 401 | [context results] 402 | (letfn [(optimize-pathsets [{:keys [pathsets graph] :as context}] 403 | (assoc context :pathsets (->> pathsets 404 | (core/optimize graph) 405 | core/collapse 406 | (into []))))] 407 | (let [context (reduce (fn [context [match value]] 408 | (update-context value context match)) 409 | context 410 | results) 411 | context (optimize-pathsets context)] 412 | context))) 413 | 414 | (defn init-context 415 | [method pathsets env] 416 | {:method method 417 | :pathsets pathsets 418 | :env env 419 | :graph {} 420 | ;; might want to just remove this -- want to support streaming requests at some point 421 | :request (vec pathsets) 422 | :matched [] 423 | :optimized [] 424 | :unhandled [] 425 | :invalidate []}) 426 | 427 | (defn execute 428 | [route-tree context runner] 429 | (letfn [(context-result [context] 430 | (dissoc context :method :pathsets :env)) 431 | (execute* [{:keys [pathsets method] :as context}] 432 | (lazy-seq 433 | (if (seq pathsets) 434 | (let [context (assoc context :pathsets []) 435 | {:keys [matches unhandled]} (get-executable-matches route-tree method pathsets) 436 | context (update context :unhandled into unhandled) 437 | results (for [match matches 438 | value (runner match context)] 439 | [match value]) 440 | context (merge-results context results)] 441 | (cons (context-result context) 442 | (execute* context))) 443 | nil)))] 444 | (execute* context))) 445 | 446 | (defn gets [{:keys [route-tree get-middleware]} pathsets env] 447 | (letfn [(runner [{:keys [handler pathset]} {:keys [env]}] 448 | ((:get handler) pathset env))] 449 | (execute route-tree 450 | (init-context :get pathsets env) 451 | (cond-> runner get-middleware get-middleware)))) 452 | 453 | (defn get 454 | ([router pathsets] 455 | (get router pathsets {})) 456 | ([router pathsets env] 457 | (last (gets router pathsets env)))) 458 | 459 | (defn pull 460 | ([router query] 461 | (pull router query {})) 462 | ([router query env] 463 | (let [pathsets (core/query-pathsets query) 464 | result (last (gets router pathsets env))] 465 | (into {} (for [[key value] result] 466 | (if (not= key :graph) 467 | [key (core/pathsets-query value)] 468 | [key value])))))) 469 | 470 | (defn sets [{:keys [route-tree set-middleware]} pathmaps env] 471 | (let [{:keys [graph paths]} (graph/set {} pathmaps) 472 | ;; TODO: not sure if this is necessary 473 | paths (core/expand-pathsets paths)] 474 | (letfn [(runner [{:keys [method handler pathset request virtual]} {:keys [env] :as context}] 475 | (case method 476 | :get ((:get handler) pathset env) 477 | :set (let [cache (:graph context) 478 | optimized-with-path (for [path paths 479 | :let [optimized (first (core/optimize cache [path]))] 480 | :when (intersects? virtual optimized)] 481 | [optimized path]) 482 | set-graph (reduce (fn [set-graph [optimized path]] 483 | (graph/set-path-value set-graph 484 | optimized 485 | (graph/get-value graph path))) 486 | {} 487 | optimized-with-path) 488 | pathmap (:graph (graph/get set-graph [request]))] 489 | ((:set handler) pathmap env))))] 490 | (let [pathsets (core/collapse paths)] 491 | (execute route-tree 492 | (init-context :set pathsets env) 493 | (cond-> runner set-middleware set-middleware)))))) 494 | 495 | (defn set 496 | ([router pathmaps] 497 | (set router pathmaps {})) 498 | ([router pathmaps env] 499 | (last (sets router pathmaps env)))) 500 | 501 | (defn calls [{:keys [route-tree call-middleware]} call-path args queries env] 502 | (letfn [(runner [{:keys [method handler request pathset]} {:keys [env]}] 503 | (case method 504 | :get ((:get handler) pathset env) 505 | :call (let [results ((:call handler) pathset args env) 506 | ;; expand pathmaps so we can search for refs 507 | results (into [] (mapcat (fn [result] 508 | (if (core/pathmap? result) 509 | (core/pathmap-values result) 510 | [result])) 511 | results)) 512 | refs (filter (fn [result] 513 | (and (core/path-value? result) 514 | (core/ref? (:value result)))) 515 | results) 516 | results (conj results (set-method :get)) 517 | ;; I don't think this distinction matters for our impl of falcor 518 | deopt-path (into [] (butlast call-path)) 519 | this-path (into [] (butlast pathset)) 520 | this-paths (into [] 521 | (for [suffix (:this queries)] 522 | (into this-path suffix))) 523 | ref-paths (into [] 524 | (for [ref refs 525 | :let [base-path (into deopt-path 526 | (drop (dec (count request)) (:path ref)))] 527 | suffix (:refs queries)] 528 | (into base-path suffix))) 529 | results (cond-> results 530 | (seq this-paths) (conj (additional-paths this-paths)) 531 | (seq ref-paths) (conj (additional-paths ref-paths)))] 532 | results)))] 533 | (execute route-tree 534 | (init-context :call [call-path] env) 535 | (cond-> runner call-middleware call-middleware)))) 536 | 537 | (defn call 538 | ([router path args queries] 539 | (call router path args queries {})) 540 | ([router path args queries env] 541 | (let [queries (into {} (for [[name query] queries] 542 | [name (cam/query-pathsets query)]))] 543 | (last (calls router path args queries env))))) 544 | 545 | ;; TODO: this name sucks 546 | (defn handle 547 | ([router request] 548 | (handle router request {})) 549 | ([router {:keys [method] :as request} env] 550 | (case method 551 | :get (let [{:keys [pathsets]} request] 552 | (get router pathsets env)) 553 | :pull (let [{:keys [query]} request] 554 | (pull router query env)) 555 | :set (let [{:keys [pathmaps]} request] 556 | (set router pathmaps env)) 557 | :call (let [{:keys [path args queries]} request] 558 | (call router path args queries env))))) 559 | 560 | (defrecord Router [route-tree]) 561 | 562 | (defn router 563 | ([routes] 564 | (Router. (route-tree routes))) 565 | ([routes {:keys [get set call]}] 566 | (let [config (cond-> {:route-tree (route-tree routes)} 567 | get (assoc :get-middleware get) 568 | set (assoc :set-middleware set) 569 | call (assoc :call-middleware call))] 570 | (map->Router config)))) 571 | 572 | (defrecord RouterDatasource 573 | [router env] 574 | core/IDataSource 575 | (pull [_ query cb] 576 | ;; TODO: doesn't impl `:missing` 577 | (cb (pull router query env))) 578 | (set [_ pathmaps cb] 579 | (cb (set router pathmaps env))) 580 | (call [_ path args queries cb] 581 | (cb (call router path args queries env)))) 582 | 583 | (defn as-datasource 584 | ([router] 585 | (as-datasource router {})) 586 | ([router env] 587 | (RouterDatasource. router env))) 588 | -------------------------------------------------------------------------------- /src/cambo/utils.cljc: -------------------------------------------------------------------------------- 1 | (ns cambo.utils) 2 | 3 | #?(:clj 4 | (defn- cljs-env? [env] 5 | (boolean (:ns env)))) 6 | 7 | #?(:clj 8 | (defmacro if-cljs 9 | "Return `then` if we are generating cljs code and `else` for Clojure code." 10 | [then else] 11 | (if (cljs-env? &env) then else))) 12 | -------------------------------------------------------------------------------- /src/data_readers.clj: -------------------------------------------------------------------------------- 1 | {js clojure.core/identity} -------------------------------------------------------------------------------- /src/examples/benchmarks.clj: -------------------------------------------------------------------------------- 1 | (ns examples.benchmarks 2 | (:require [examples.queries :as queries] 3 | [cambo.graph :as graph] 4 | [criterium.core :as bench])) 5 | 6 | ;; benchmark! 7 | 8 | (comment 9 | 10 | ;;; APPLICATION SCREEN 11 | 12 | (doseq [pathsets queries/application-queries] 13 | (bench/quick-bench 14 | (graph/get queries/full-uwb 15 | pathsets 16 | {:normalize false 17 | :path-info true 18 | :boxed false}))) 19 | 20 | (let [pathsets (nth queries/application-queries 5)] 21 | (bench/quick-bench 22 | (graph/get queries/full-uwb 23 | pathsets 24 | {:normalize false 25 | :path-info true 26 | :boxed false}))) 27 | 28 | (let [pathsets (reduce into queries/application-queries)] 29 | (bench/quick-bench 30 | (graph/get queries/full-uwb 31 | pathsets 32 | {:normalize false 33 | :path-info true 34 | :boxed false}))) 35 | 36 | (let [pathsets (reduce into queries/application-queries)] 37 | (bench/quick-bench 38 | (graph/missing queries/full-uwb pathsets))) 39 | 40 | ;;; QUESTIONNAIRE SCREEN 41 | 42 | 43 | 44 | ) -------------------------------------------------------------------------------- /src/examples/benchmarks.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.benchmarks 2 | (:require-macros [cambo.component :refer [defcomponent defcontainer]]) 3 | (:require [examples.queries :as queries] 4 | [cognitect.transit] 5 | [cambo.core :as core] 6 | [cambo.router] 7 | [cambo.graph :as graph] 8 | [cambo.model :as model] 9 | [cambo.component] 10 | [cljs.pprint :refer [pprint]] 11 | [cljsjs.benchmark])) 12 | 13 | (enable-console-print!) 14 | 15 | (def suite (js/Benchmark.Suite)) 16 | 17 | (deftype EmptyDataSource []) 18 | 19 | (extend-type EmptyDataSource 20 | core/IDataSource 21 | (pull [this query cb]) 22 | (set [this pathmaps cb]) 23 | (call [this path args queries cb])) 24 | 25 | (def ds (EmptyDataSource.)) 26 | 27 | (def opts {:normalize false 28 | :path-info false 29 | :boxed false}) 30 | 31 | (println "options" opts) 32 | 33 | (doseq [[name queries pulls] [["application" queries/application-queries queries/application-pulls] 34 | ["questionnaire" queries/questionnaire-queries queries/questionnaire-pulls] 35 | ["dashboard" queries/dashboard-queries queries/dashboard-pulls]] 36 | :let [pathsets (reduce into queries) 37 | model (model/model {:cache queries/full-uwb 38 | :datasource ds})]] 39 | 40 | (doseq [idx (range 0 (count queries)) 41 | :let [pathsets (nth queries idx)]] 42 | (.add suite (str name ":get:" idx) (fn [] 43 | (graph/get queries/full-uwb pathsets opts)))) 44 | 45 | (doseq [idx (range 0 (count pulls)) 46 | :let [query (nth pulls idx)]] 47 | (.add suite (str name ":pull:" idx) (fn [] 48 | (graph/pull queries/full-uwb query opts)))) 49 | 50 | (.add suite (str name ":get:all") (fn [] 51 | (graph/get queries/full-uwb pathsets opts))) 52 | 53 | (.add suite (str name ":missing") (fn [] 54 | (doall (graph/missing queries/full-uwb pathsets)))) 55 | 56 | (.add suite (str name ":missing-transient") (fn [] 57 | (doall (graph/missing-transient queries/full-uwb pathsets)))) 58 | 59 | (.add suite (str name ":prime") (fn [] 60 | (model/prime model pathsets (fn [_]))))) 61 | 62 | (.add suite "set" (fn [] 63 | (graph/set {} [queries/full-uwb]))) 64 | 65 | (.on suite "complete" (fn [] 66 | (this-as this 67 | (doseq [idx (range 0 (.-length this)) 68 | :let [bench (aget this idx)]] 69 | (println (.-name bench) 70 | "mean:" (* 1000 (.. bench -stats -mean)) "ms"))))) 71 | 72 | (defn ^:export run [] 73 | (.run suite) 74 | nil) 75 | 76 | (let [pathsets (reduce into queries/questionnaire-queries) 77 | query (reduce into [] queries/questionnaire-pulls)] 78 | (println (= (:graph (graph/get queries/full-uwb pathsets opts)) 79 | (:graph (graph/pull queries/full-uwb query opts)))) 80 | (time (graph/get queries/full-uwb pathsets opts)) 81 | (time (graph/pull queries/full-uwb query opts))) 82 | 83 | 84 | ;(let [pathsets (reduce into queries/application-queries) 85 | ; query (reduce into [] queries/application-pulls)] 86 | ; (println (= (:graph (graph/get queries/full-uwb pathsets opts)) 87 | ; (:graph (graph/pull queries/full-uwb query opts)))) 88 | ; (time (graph/get queries/full-uwb pathsets opts)) 89 | ; (time (graph/pull queries/full-uwb query opts))) 90 | ; 91 | ;(let [pathsets (reduce into queries/dashboard-queries) 92 | ; query (reduce into [] queries/dashboard-pulls)] 93 | ; (println (= (:graph (graph/get queries/full-uwb pathsets opts)) 94 | ; (:graph (graph/pull queries/full-uwb query opts)))) 95 | ; (time (graph/get queries/full-uwb pathsets opts)) 96 | ; (time (graph/pull queries/full-uwb query opts))) 97 | 98 | ;(let [pathsets (reduce into queries/questionnaire-queries)] 99 | ; (time (:missing (graph/get queries/full-uwb pathsets opts))) 100 | ; (time (graph/missing queries/full-uwb pathsets)) 101 | ; (time (graph/missing-transient queries/full-uwb pathsets)) 102 | ; 103 | ; (time (:missing (graph/get {} pathsets opts))) 104 | ; (time (graph/missing {} pathsets)) 105 | ; (time (graph/missing-transient {} pathsets))) 106 | -------------------------------------------------------------------------------- /src/examples/github.clj: -------------------------------------------------------------------------------- 1 | (ns examples.github 2 | (:require [cambo.core :as core :refer [path-value]] 3 | [cambo.router :as router :refer [KEYS INTEGERS RANGES]] 4 | [cambo.model :as model] 5 | [clj-http.client :as http] 6 | [clojure.edn :as edn] 7 | [environ.core :refer [env]])) 8 | 9 | (def base-url "https://api.github.com") 10 | 11 | (def base-options {:accept :json 12 | :as :json 13 | :query-params {"client_id" (env :github-client-id) 14 | "client_secret" (env :github-client-secret)}}) 15 | 16 | (defn api-get [resource] 17 | (:body (http/get (str base-url resource) base-options))) 18 | 19 | (defn api-count [resource] 20 | (let [result (http/get (str base-url resource "?per_page=1") base-options)] 21 | (some->> (get-in result [:links :last :href]) 22 | (re-find #"&page=(\d+)") 23 | second 24 | edn/read-string))) 25 | 26 | (def routes 27 | [{:route [:org/by-id INTEGERS [:org/description 28 | :org/email 29 | :org/name 30 | :org/login 31 | :org/id 32 | :org/url]] 33 | :get (fn [[_ ids keys] _] 34 | (for [id ids 35 | :let [org (api-get (str "/organizations/" id))] 36 | :when org 37 | key keys 38 | :let [github-key (keyword (name key))]] 39 | (path-value [:org/by-id id key] 40 | (get org github-key))))} 41 | 42 | {:route [:org/by-id INTEGERS :org/repos RANGES] 43 | :get (fn [[_ ids _ ranges] _] 44 | (for [id ids 45 | :let [repos (into [] (api-get (str "/organizations/" id "/repos")))] 46 | idx (router/indices ranges) 47 | :let [repo (get repos idx)] 48 | :when repo] 49 | (path-value [:org/by-id id :org/repos idx] 50 | (core/ref [:repo/by-id (:id repo)]))))} 51 | 52 | {:route [:org/by-id INTEGERS :org/repos :length] 53 | :get (fn [[_ ids _ _] _] 54 | (for [id ids 55 | :let [repo-count (api-count (str "/organizations/" id "/repos"))] 56 | :when repo-count] 57 | (path-value [:org/by-id id :org/repos :length] 58 | repo-count)))} 59 | 60 | {:route [:repo/by-id INTEGERS [:repo/description 61 | :repo/homepage 62 | :repo/name 63 | :repo/full-name 64 | :repo/stargazers-count 65 | :repo/size 66 | :repo/language 67 | :repo/id 68 | :repo/url 69 | :repo/forks 70 | :repo/owner]] 71 | :get (fn [[_ ids keys] _] 72 | (for [id ids 73 | :let [repo (api-get (str "/repositories/" id))] 74 | :when repo 75 | key keys 76 | :let [github-key (keyword (name key))]] 77 | (path-value [:repo/by-id id key] 78 | (case key 79 | :repo/full-name (get repo :full_name) 80 | :repo/stargazers-count (get repo :stargazers_count) 81 | :repo/owner (let [owner (get repo :owner)] 82 | (case (:type owner) 83 | "Organization" (core/ref [:org/by-id (:id owner)]))) 84 | (get repo github-key)))))}]) 85 | 86 | (comment 87 | 88 | (let [router (router/router routes)] 89 | (router/get router [[:org/by-id 913567 [:org/name :org/description]]])) 90 | 91 | (let [router (router/router routes)] 92 | (router/get router [[:org/by-id [913567] :org/repos :length] 93 | [:org/by-id [913567] :org/repos (core/range 0 1) [:repo/description 94 | :repo/name 95 | :repo/full-name 96 | :repo/forks 97 | :repo/stargazers-count]] 98 | [:org/by-id [913567] :org/repos (core/range 0 1) :repo/owner [:org/name 99 | :org/description]]])) 100 | 101 | (def query [{:org/by-id [{913567 [{:org/repos [:length 102 | {(core/range 0 1) [:repo/description 103 | :repo/name 104 | :repo/full-name 105 | :repo/forks 106 | :repo/stargazers-count 107 | {:repo/owner [:org/name 108 | :org/description]}]}]}]}]}]) 109 | 110 | (defn pull [query] 111 | (let [leafs (into [] (remove map? query)) 112 | paths (for [join (filter map? query) 113 | :let [[key query] (first join)] 114 | paths (pull query)] 115 | (into [key] paths))] 116 | (cond-> (into [] paths) 117 | (seq leafs) (conj [leafs])))) 118 | 119 | (pull [:repo/description 120 | :repo/name 121 | {:repo/owner [:org/name 122 | :org/id]} 123 | {:repo/foo [:org/name 124 | :org/id 125 | {:org/repos [:length]}]}]) 126 | 127 | (pull [{:org/by-id [{913567 [{:org/repos [:length 128 | {(core/range 0 1) [:repo/description 129 | :repo/name 130 | :repo/full-name 131 | :repo/forks 132 | :repo/stargazers-count 133 | {:repo/owner [:org/name 134 | :org/description]}]}]}]}]}]) 135 | 136 | (let [router (router/router routes)] 137 | (router/get router (pull [{:org/by-id [{913567 [{:org/repos [:length 138 | {(core/range 0 1) [:repo/description 139 | :repo/name 140 | :repo/full-name 141 | :repo/forks 142 | :repo/stargazers-count 143 | {:repo/owner [:org/name 144 | :org/description]}]}]}]}]}]))) 145 | 146 | (let [router (router/router routes) 147 | model (model/model {:datasource (router/as-datasource router)}) 148 | result (promise)] 149 | (model/get model [[:org/by-id 913567 [:org/name :org/description]] 150 | [:org/by-id 913567 :org/repos (core/range 0 5) [:repo/name :repo/description]]] 151 | (partial deliver result)) 152 | (deref result 500 :timeout)) 153 | 154 | (let [m (model/model {:datasource (router/as-datasource (router/router routes))}) 155 | result (promise)] 156 | (model/get m (pull [{:org/by-id [{913567 [:org/name 157 | :org/description 158 | {:org/repos [:length 159 | {(core/range 0 2) [:repo/description 160 | :repo/name 161 | :repo/full-name 162 | :repo/forks 163 | :repo/stargazers-count 164 | {:repo/owner [:org/name 165 | :org/description]}]}]}]}]}]) 166 | (partial deliver result)) 167 | (deref result 500 :timeout)) 168 | 169 | ) 170 | -------------------------------------------------------------------------------- /src/examples/server.clj: -------------------------------------------------------------------------------- 1 | (ns examples.server 2 | (:require [ring.adapter.jetty :refer [run-jetty]] 3 | [cambo.core :as core] 4 | [cambo.ring :refer [wrap-cambo]] 5 | [cambo.router :as router :refer [RANGES INTEGERS KEYS]])) 6 | 7 | ;; DATABASE 8 | 9 | (def user-db (atom {13 {:user/id 13 10 | :user/name "Huey" 11 | :user/age 35 12 | :user/gender :gender/male 13 | :user/todos [1 2 3]}})) 14 | 15 | (defn get-users 16 | [users ids] 17 | (select-keys @users ids)) 18 | 19 | (defn get-user 20 | [users id] 21 | (get @users id)) 22 | 23 | (defn add-user-todo 24 | [users id todo-id] 25 | (swap! users update-in [id :user/todos] (fnil conj []) todo-id) 26 | (dec (count (get-in @users [id :user/todos])))) 27 | 28 | (defn remove-user-todo 29 | [users id todo-id] 30 | (let [idx (.indexOf (get-in @users [id :user/todos]) todo-id)] 31 | (swap! users update-in [id :user/todos] (fn [todos] 32 | (into [] (remove #{todo-id} todos)))) 33 | idx)) 34 | 35 | (def todo-db (atom {1 {:todo/id 1 36 | :todo/text "Buy a unicorn" 37 | :todo/complete false} 38 | 2 {:todo/id 2 39 | :todo/text "Learn an clojurescript" 40 | :todo/complete false} 41 | 3 {:todo/id 3 42 | :todo/text "pokemon" 43 | :todo/complete true}})) 44 | 45 | (defn get-todos 46 | [todos ids] 47 | (select-keys @todos ids)) 48 | 49 | (defn get-todo 50 | [todos id] 51 | (get @todos id)) 52 | 53 | (defn todo-set-complete 54 | [todos id complete] 55 | (swap! todos assoc-in [id :todo/complete] complete)) 56 | 57 | (def todo-ids (atom 3)) 58 | 59 | (defn create-todo 60 | [todos args] 61 | (let [id (swap! todo-ids inc)] 62 | (swap! todos assoc id (merge {:todo/id id} 63 | args)) 64 | id)) 65 | 66 | (defn delete-todo 67 | [todos id] 68 | (swap! todos dissoc id)) 69 | 70 | ;; ROUTES 71 | 72 | (def routes 73 | [{:route [:current-user] 74 | :get (fn [_ {:keys [session/user-id]}] 75 | [(core/path-value [:current-user] 76 | (core/ref [:user/by-id user-id]))])} 77 | 78 | {:route [:user/by-id INTEGERS [:user/id :user/name :user/age :user/gender]] 79 | :get (fn [[_ ids keys] {:keys [db/users]}] 80 | (for [[id user] (get-users users ids)] 81 | {:user/by-id {id (select-keys user keys)}}))} 82 | 83 | {:route [:user/by-id INTEGERS :user/todos RANGES] 84 | :get (fn [[_ ids _ ranges] {:keys [db/users]}] 85 | (for [[id {:keys [user/todos]}] (get-users users ids) 86 | idx (router/indices ranges) 87 | :let [todo-id (get todos idx)] 88 | :when todo-id] 89 | (core/path-value [:user/by-id id :user/todos idx] 90 | (core/ref [:todo/by-id todo-id]))))} 91 | 92 | {:route [:user/by-id INTEGERS :user/todos :length] 93 | :get (fn [[_ ids] {:keys [db/users]}] 94 | (for [[id {:keys [user/todos]}] (get-users users ids)] 95 | (core/path-value [:user/by-id id :user/todos :length] 96 | (count todos))))} 97 | 98 | {:route [:todo/by-id INTEGERS [:todo/id :todo/text :todo/complete]] 99 | :get (fn [[_ ids keys] {:keys [db/todos]}] 100 | (for [[id todo] (get-todos todos ids)] 101 | {:todo/by-id {id (select-keys todo keys)}}))} 102 | 103 | {:route [:todo/by-id INTEGERS :todo/complete] 104 | :set (fn [pathmap {:keys [db/todos]}] 105 | (doseq [[id {:keys [todo/complete]}] (:todo/by-id pathmap)] 106 | (todo-set-complete todos id complete)) 107 | [pathmap])} 108 | 109 | {:route [:user/by-id INTEGERS :user/todos :todo/add] 110 | :call (fn [[_ [id]] args {:keys [db/todos db/users]}] 111 | (let [todo-id (create-todo todos args) 112 | idx (add-user-todo users id todo-id)] 113 | [(core/path-value [:user/by-id id :user/todos idx] 114 | (core/ref [:todo/by-id todo-id]))]))} 115 | 116 | {:route [:user/by-id INTEGERS :user/todos :todo/delete] 117 | :call (fn [[_ [id]] args {:keys [db/todos db/users]}] 118 | (let [todo-id (:todo/id args) 119 | idx (remove-user-todo users id todo-id)] 120 | (delete-todo todos todo-id) 121 | [(router/invalidate [:user/by-id id :user/todos]) 122 | (router/invalidate [:todos/by-id todo-id :user/todos idx])]))}]) 123 | 124 | ;; ROUTER 125 | 126 | (def router (router/router routes)) 127 | 128 | ;; HTTP 129 | 130 | (defn cambo-handler 131 | [{:keys [cambo context]}] 132 | (router/handle router cambo context)) 133 | 134 | (defn wrap-cors 135 | [handler] 136 | (fn [{:keys [request-method] :as request}] 137 | (let [response (if (= :options request-method) 138 | {:status 200} 139 | (handler request))] 140 | (update response :headers merge {"Access-Control-Allow-Origin" "*" 141 | "Access-Control-Allow-Headers" "Content-Type, X-Csrf-Token"})))) 142 | 143 | (def cambo-handler' (-> cambo-handler wrap-cambo wrap-cors)) 144 | 145 | (defn handler [{:keys [uri] :as request}] 146 | (let [request (assoc request :context {:session/user-id 13 147 | :db/users user-db 148 | :db/todos todo-db})] 149 | (case uri 150 | "/cambo" (cambo-handler' request) 151 | {:status 200 152 | :headers {"Content-Type" "text/html"} 153 | :body uri}))) 154 | 155 | (defonce server nil) 156 | 157 | (defn start-server 158 | [] 159 | (when server 160 | (.stop server)) 161 | (alter-var-root 162 | #'server 163 | (fn [_] 164 | (run-jetty handler 165 | {:host "0.0.0.0" :port 4000 :join? false})))) 166 | 167 | (start-server) 168 | -------------------------------------------------------------------------------- /src/examples/ssr.clj: -------------------------------------------------------------------------------- 1 | (ns examples.ssr 2 | (:require [cambo.component :as comp :refer [defcomponent defcontainer]] 3 | [cambo.core :as cam] 4 | [cambo.model :as model])) 5 | 6 | (defcomponent NotFound 7 | (render [this] 8 | (js/console.log "Hello world"))) 9 | 10 | (defcontainer Todo 11 | :fragments {:todo [:todo/id 12 | :todo/text 13 | :todo/complete?]} 14 | (render [this] 15 | (js/console.log "Hello from Todos!"))) 16 | 17 | (defcontainer TodoList 18 | :fragments {:todos [{(cam/range 0 10) [:todo/id 19 | (comp/get-fragment Todo :todo)]}]} 20 | (render [this] 21 | (js/console.log "Hello from Todos!"))) 22 | 23 | (macroexpand '(defcontainer TodoList 24 | :fragments {:todos [{(cam/range 0 10) [:todo/id 25 | :todo/text 26 | :todo/complete?]}]} 27 | (render [this] 28 | #js {} 29 | (js/console.log "Hello from Todos!")))) 30 | 31 | (let [fragment (comp/get-fragment TodoList :todos)] 32 | (comp/full-fragment fragment)) 33 | 34 | (comp/build-query {:todos [:todos/all]} TodoList) 35 | 36 | (comment 37 | 38 | (let [model (model/model {:cache {:foo {:bar "baz"}}})] 39 | @(model/pull model [{:foo [:bar]}])) 40 | 41 | (let [model (model/model {:cache {:foo {:bar "baz"}}})] 42 | (model/pull model [{:foo [:bar]}] (fn [value] 43 | (println "value!" value)))) 44 | 45 | ) 46 | -------------------------------------------------------------------------------- /src/examples/todo.cljs: -------------------------------------------------------------------------------- 1 | (ns examples.todo 2 | (:require-macros [cambo.component :refer [defcomponent defcontainer]]) 3 | (:require [cambo.component :as comp :refer [props get-fragment]] 4 | [cambo.core :as core] 5 | [cambo.http :refer [http-datasource]] 6 | [cambo.model :as model] 7 | [cljsjs.react] 8 | [cljsjs.react.dom] 9 | [cljs.pprint :refer [pprint]])) 10 | 11 | (enable-console-print!) 12 | 13 | (defn tag [tag] 14 | (fn [props & children] 15 | (apply js/React.createElement tag props children))) 16 | 17 | (def div (tag "div")) 18 | (def pre (tag "pre")) 19 | (def h1 (tag "h1")) 20 | (def h3 (tag "h3")) 21 | (def ul (tag "ul")) 22 | (def li (tag "li")) 23 | (def input (tag "input")) 24 | (def button (tag "button")) 25 | 26 | (defcomponent Foo 27 | (render [this] 28 | (div "Hello"))) 29 | 30 | (def foo (comp/factory Foo)) 31 | 32 | (defcontainer TodoDetails 33 | :fragments {:todo [:todo/id 34 | :todo/text 35 | :todo/complete]} 36 | (handleCompleteChange [this ev] 37 | (let [complete (get-in (props this) [:todo :todo/complete])] 38 | (comp/set-model this :todo {:todo/complete (not complete)}))) 39 | (handleDeleteClick [this ev] 40 | (when-let [on-delete (get (props this) :on-delete)] 41 | (let [id (get-in (props this) [:todo :todo/id])] 42 | (on-delete id)))) 43 | (render [this] 44 | (let [{:keys [todo]} (props this) 45 | {:keys [todo/text todo/complete]} todo] 46 | (li nil 47 | (div nil 48 | (input #js {:type "checkbox" 49 | :checked complete 50 | :onChange #(.handleCompleteChange this %)}) 51 | (div nil text) 52 | (foo {:ref #(println %)}) 53 | (button #js {:onClick #(.handleDeleteClick this %)} "delete")))))) 54 | 55 | (def todo-details (comp/factory TodoDetails)) 56 | 57 | (defcontainer TodoList 58 | :initial-variables {:count 10} 59 | :fragments {:user (fn [{:keys [count]}] 60 | [{:user/todos [{(core/range 0 count) [:todo/id 61 | (get-fragment TodoDetails :todo)]} 62 | :length]}])} 63 | (initLocalState [this] 64 | {:foo "bar"}) 65 | (handleTodoDelete [this todo-id] 66 | (let [{:keys [count]} (comp/variables this)] 67 | (comp/call-model this 68 | [:current-user :user/todos :todo/delete] 69 | {:todo/id todo-id} 70 | {:this [{(core/range 0 count) [:todo/id 71 | :todo/text 72 | :todo/complete]} 73 | :length]}))) 74 | (render [this] 75 | (let [{:keys [user]} (props this) 76 | {:keys [user/todos]} user] 77 | (println "state!" (comp/state this)) 78 | (div nil 79 | (ul nil 80 | (vec (for [idx (core/range-keys (core/range 0 10)) 81 | :let [{:keys [todo/id] :as todo} (get todos idx)] 82 | :when todo] 83 | (todo-details {:todo todo 84 | :key id 85 | :ref #(println %) 86 | :on-delete #(.handleTodoDelete this %)})))))))) 87 | 88 | (def todo-list (comp/factory TodoList)) 89 | 90 | (defcomponent TodoEntry 91 | (handleClick [this ev] 92 | (.preventDefault ev) 93 | (when-let [on-entry (get (props this) :on-entry)] 94 | (on-entry "Here is a todo!"))) 95 | (render [this] 96 | (div nil 97 | (input #js {:type "text"}) 98 | (button #js {:onClick #(.handleClick this %)} 99 | "create")))) 100 | 101 | (def todo-entry (comp/factory TodoEntry)) 102 | 103 | (defcontainer TodoApp 104 | :fragments {:user [:user/id 105 | :user/name 106 | (get-fragment TodoList :user)]} 107 | (handleEntry [this text] 108 | (let [user-id (get-in (props this) [:user :user/id])] 109 | (comp/call-model this 110 | ;; or :current-user if you prefer! 111 | [:user/by-id user-id :user/todos :todo/add] 112 | {:todo/text text 113 | :todo/complete false} 114 | {:refs [:todo/id 115 | :todo/text 116 | :todo/complete]}))) 117 | (render [this] 118 | (let [{:keys [user]} (props this) 119 | {:keys [user/name]} user] 120 | (div nil 121 | (h1 nil "Todos") 122 | (h3 nil name) 123 | (todo-entry {:on-entry #(.handleEntry this %)}) 124 | (todo-list {:user user}) 125 | (comp/children this))))) 126 | 127 | (def todo-app (comp/factory TodoApp)) 128 | 129 | (defcontainer Answer 130 | :fragments {:answer [:answer/type 131 | :answer/state 132 | :answer/value]} 133 | (render [this] nil)) 134 | 135 | (defcontainer Field 136 | :fragments {:question (fn [_] 137 | [:question/id 138 | :question/type 139 | {:question/answer [(get-fragment Answer :answer)]} 140 | {:question/questions [{(core/range 0 10) [(get-fragment Field :question 4)]}]}])} 141 | (render [this] nil)) 142 | 143 | (deftype LoggingDataSource [ds] 144 | core/IDataSource 145 | (pull [this query cb] 146 | (core/pull ds query (fn [{:keys [unhandled] :as result}] 147 | ;; TODO: use a warn or something from the real dev tools? 148 | ;; - should never see this -- means your queries are wrong 149 | (when (seq unhandled) 150 | (println "CAMBO: unhandled" unhandled)) 151 | (cb result)))) 152 | (set [this pathmaps cb] 153 | (core/set ds pathmaps cb)) 154 | (call [this path args queries cb] 155 | (core/call ds path args queries (fn [result] 156 | ;; TODO: use a warn or something from the real dev tools? 157 | ;; - should never see this -- means your queries are wrong 158 | (println "CALL:" result) 159 | (cb result))))) 160 | 161 | (defn logging-datasource [ds] 162 | (LoggingDataSource. ds)) 163 | 164 | (def model (model/model {:datasource (logging-datasource 165 | (http-datasource "http://localhost:4000/cambo" 166 | {"X-CSRF-TOKEN" "abc123"}))})) 167 | 168 | (js/ReactDOM.render 169 | (comp/renderer {:queries {:user [:current-user]} 170 | :container TodoApp 171 | :model model} 172 | (h1 #js{} "I am a child")) 173 | (.getElementById js/document "app")) 174 | -------------------------------------------------------------------------------- /test/cambo/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns cambo.core-test 2 | (:refer-clojure :exclude [get set atom ref keys range]) 3 | (:require [cambo.core :refer :all] 4 | [clojure.test :refer :all])) 5 | 6 | 7 | ;;; KEYS 8 | 9 | (deftest range-keys-test 10 | (is (= [4 5 6 7 8 9] 11 | (range-keys (range 4 10)))) 12 | (is (= [4] 13 | (range-keys (range 4 5)))) 14 | (is (= [] 15 | (range-keys (range 4 4)))) 16 | (is (= [] 17 | (range-keys (range 4 3))))) 18 | 19 | (deftest keys-test 20 | (is (= [:foo] 21 | (keys :foo))) 22 | (is (= [0 1 :foo] 23 | (keys [0 1 :foo]))) 24 | (is (= [0 1 2 3 4 5] 25 | (keys (range 0 6))))) 26 | 27 | (deftest keyset-seq-test 28 | (is (= [:foo] 29 | (keyset-seq :foo))) 30 | (is (= [(range 0 5)] 31 | (keyset-seq (range 0 5)))) 32 | (is (= [:foo "bar" 13] 33 | (keyset-seq [:foo "bar" 13]))) 34 | (is (= [:foo "bar" (range 0 5) 13] 35 | (keyset-seq [:foo "bar" (range 0 5) 13])))) 36 | 37 | (deftest keyset-test 38 | (is (= :foo 39 | (keyset [:foo]))) 40 | (is (= ["bar" (range 13 14) :foo] 41 | (keyset [:foo "bar" 13])))) 42 | 43 | ;;; PATHS 44 | 45 | ;; TODO: a normalize pathset, etc for keyset comparison (make everything a set for order-independent comparison) 46 | 47 | (deftest expand-paths-test 48 | (is (= [[:user 0 :name] 49 | [:user 0 :age] 50 | [:user 1 :name] 51 | [:user 1 :age]] 52 | (expand-pathsets [[:user (range 0 2) [:name :age]]]))) 53 | (is (= [[:user 0 :name] 54 | [:user 0 :age] 55 | [:user 1 :name] 56 | [:user 1 :age] 57 | [:video 0 :title] 58 | [:video 5 :title] 59 | [:video 6 :title] 60 | [:video 7 :title]] 61 | (expand-pathsets [[:user (range 0 2) [:name :age]] 62 | [:video [0 (range 5 8)] :title]])))) 63 | 64 | (deftest pathsets-test 65 | (is (= [[:one [:three :two]] 66 | [:one "four" [(range 0 2)]]] 67 | (pathsets {:one {:two leaf 68 | :three leaf 69 | "four" {0 leaf 70 | 1 leaf}}})))) 71 | 72 | (deftest length-tree-pathsets-test 73 | (testing "simple path" 74 | (is (= [[:one :two]] 75 | (length-tree-pathsets {2 {:one {:two leaf}}})))) 76 | (testing "complex path" 77 | (is (= [[:one [:three :two]]] 78 | (length-tree-pathsets {2 {:one {:two leaf 79 | :three leaf}}})))) 80 | (testing "simple and complex path" 81 | (is (= [[:one [:three :two]] 82 | [:one [(range 0 4)] :summary]] 83 | (length-tree-pathsets {2 {:one {:two leaf 84 | :three leaf}} 85 | 3 {:one {0 {:summary leaf} 86 | 1 {:summary leaf} 87 | 2 {:summary leaf} 88 | 3 {:summary leaf}}}})))) 89 | (testing "pathmap that has overlapping branch and leaf nodes" 90 | (is (= [[:lolomo] 91 | [:lolomo [(range 13 15) :summary]] 92 | [:lolomo [(range 15 18)] [:summary :rating]] 93 | [:lolomo [(range 13 15)] :summary]] 94 | (length-tree-pathsets {1 {:lolomo leaf} 95 | 2 {:lolomo {:summary leaf 96 | 13 leaf 97 | 14 leaf}} 98 | 3 {:lolomo {15 {:rating leaf 99 | :summary leaf} 100 | 13 {:summary leaf} 101 | 16 {:rating leaf 102 | :summary leaf} 103 | 14 {:summary leaf} 104 | 17 {:rating leaf 105 | :summary leaf}}}}))))) 106 | 107 | (deftest tree-test 108 | (testing "simple path" 109 | (is (= {:one {:two leaf}} 110 | (tree [[:one :two]])))) 111 | (testing "complex path" 112 | (is (= {:one {:two leaf 113 | :three leaf}} 114 | (tree [[:one [:two :three]]])))) 115 | (testing "set of complex and simple paths" 116 | (is (= {:one {:two leaf 117 | :three leaf 118 | 0 {:summary leaf} 119 | 1 {:summary leaf} 120 | 2 {:summary leaf} 121 | 3 {:summary leaf}}} 122 | (tree [[:one [:two :three]] 123 | [:one (range 0 4) :summary]]))))) 124 | 125 | (deftest collapse-test 126 | (is (= [[:genres 0 :titles [(range 0 2)] [:name :rating]]] 127 | (collapse [[:genres 0 :titles 0 :name] 128 | [:genres 0 :titles 0 :rating] 129 | [:genres 0 :titles 1 :name] 130 | [:genres 0 :titles 1 :rating]]))) 131 | (is (= [[:genres 0 :titles [(range 0 2)] [:name :rating]]] 132 | (collapse [[:genres 0 :titles 0 [:name :rating]] 133 | [:genres 0 :titles 1 :name] 134 | [:genres 0 :titles 1 :rating]])))) 135 | 136 | (deftest optimize-test 137 | (let [cache {:videos-list {3 (ref [:videos 956]) 138 | 5 (ref [:videos 5]) 139 | :double (ref [:videos-list 3]) 140 | :short (ref [:videos 5 :more-keys]) 141 | :inner (ref [:videos-list 3 :inner])} 142 | :videos {5 (atom "title") 143 | 6 "a" 144 | 7 1 145 | 8 true 146 | 9 nil} 147 | :falsey {:string "" 148 | :number 0 149 | :boolean false 150 | :nil nil}}] 151 | (testing "simple path" 152 | (is (= [[:videos 956 :summary]] 153 | (optimize cache [[:videos-list 3 :summary]])))) 154 | (testing "complex path" 155 | (is (= [[:videos-list 0 :summary] 156 | [:videos 956 :summary]] 157 | (optimize cache [[:videos-list [0 3] :summary]])))) 158 | (testing "remove found paths" 159 | (is (= [[:videos-list 0 :summary] 160 | [:videos 956 :summary]] 161 | (optimize cache [[:videos-list [0 3 5] :summary]])))) 162 | (testing "follow double reference" 163 | (is (= [[:videos 956 :summary]] 164 | (optimize cache [[:videos-list :double :summary]])))) 165 | (testing "short circuit ref" 166 | (is (= [] 167 | (optimize cache [[:videos-list :short :summary]])))) 168 | (testing "short circuit string" 169 | (is (= [] 170 | (optimize cache [[:videos 6 :summary]])))) 171 | (testing "short circuit number" 172 | (is (= [] 173 | (optimize cache [[:videos 7 :summary]])))) 174 | (testing "short circuit boolean" 175 | (is (= [] 176 | (optimize cache [[:videos 8 :summary]])))) 177 | (testing "short circuit nil" 178 | (is (= [] 179 | (optimize cache [[:videos 9 :summary]])))) 180 | (testing "falsey string not missing" 181 | (is (= [] 182 | (optimize cache [[:falsey :string]])))) 183 | (testing "falsey number not missing" 184 | (is (= [] 185 | (optimize cache [[:falsey :number]])))) 186 | (testing "falsey boolean not missing" 187 | (is (= [] 188 | (optimize cache [[:falsey :boolean]])))) 189 | (testing "falsey nil not missing" 190 | (is (= [] 191 | (optimize cache [[:falsey :nil]])))) 192 | (testing "inner reference" 193 | (is (thrown? Exception 194 | (optimize cache [[:videos-list :inner :summary]])))))) 195 | 196 | (deftest pathmap-paths-test 197 | (is (= [[:user/by-id 0 :user/name] 198 | [:user/by-id 0 :user/age] 199 | [:user/by-id 1 :user/name] 200 | [:user/by-id 1 :user/age]] 201 | (pathmap-paths {:user/by-id {0 {:user/name "Erik" :user/age 31} 202 | 1 {:user/name "Huey" :user/age 13}}}))) 203 | (is (= [[:user/by-id 0 :user/name] 204 | [:user/by-id 0 :user/age] 205 | [:user/by-id 1 :user/name] 206 | [:user/by-id 1 :user/age]] 207 | (pathmap-paths {:user/by-id {0 {:user/name (atom {:first "Erik" :last "Petersen"}) :user/age 31} 208 | 1 {:user/name "Huey" :user/age 13}}})))) 209 | 210 | (deftest pathmap-path-values-test 211 | (is (= [(path-value [:user/by-id 0 :user/name] "Erik") 212 | (path-value [:user/by-id 0 :user/age] 31) 213 | (path-value [:user/by-id 1 :user/name] "Huey") 214 | (path-value [:user/by-id 1 :user/age] 13)] 215 | (pathmap-values {:user/by-id {0 {:user/name "Erik" :user/age 31} 216 | 1 {:user/name "Huey" :user/age 13}}}))) 217 | (is (= [(path-value [:user/by-id 0 :user/name] (atom {:first "Erik" :last "Petersen"})) 218 | (path-value [:user/by-id 0 :user/age] 31) 219 | (path-value [:user/by-id 0 :user/friend] (ref [:user/by-id 1])) 220 | (path-value [:user/by-id 1 :user/name] "Huey") 221 | (path-value [:user/by-id 1 :user/age] 13)] 222 | (pathmap-values {:user/by-id {0 {:user/name (atom {:first "Erik" :last "Petersen"}) 223 | :user/age 31 224 | :user/friend (ref [:user/by-id 1])} 225 | 1 {:user/name "Huey" :user/age 13}}})))) 226 | -------------------------------------------------------------------------------- /test/cambo/graph_test.clj: -------------------------------------------------------------------------------- 1 | (ns cambo.graph-test 2 | (:refer-clojure :exclude [get set range atom ref]) 3 | (:require [cambo.graph :refer :all] 4 | [cambo.core :as core :refer [range atom ref]] 5 | [clojure.test :refer :all])) 6 | 7 | (def cache 8 | {:users {0 (ref [:user/by-id 123]) 9 | 1 nil 10 | 2 (atom nil) 11 | 3 (atom)} 12 | :user/by-id {123 {:user/name (atom "Erik") 13 | :user/age (atom 31) 14 | :user/gender :gender/male 15 | :user/complex-name (atom {:first "Erik" 16 | :last "Petersen"})}}}) 17 | 18 | (deftest get-basic-test 19 | (let [get #(get %1 %2 {:normalize true 20 | :path-info false 21 | :boxed false})] 22 | (testing "unoptimized get" 23 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 24 | :user/age 31}}} 25 | :missing []} 26 | (get cache [[:user/by-id 123 [:user/name :user/age]]])))) 27 | (testing "getting an non-boxed value" 28 | (is (= {:graph {:user/by-id {123 {:user/gender :gender/male}}} 29 | :missing []} 30 | (get cache [[:user/by-id 123 :user/gender]])))) 31 | (testing "getting a branch" 32 | (is (= {:graph {} 33 | :missing []} 34 | (get cache [[:user/by-id 123]])))) 35 | (testing "getting into an atom" 36 | (is (= {:graph {:user/by-id {123 {:user/name "Erik"}}} 37 | :missing []} 38 | (get cache [[:user/by-id 123 :user/name :name/first]])))) 39 | (testing "getting into non-boxed value" 40 | (is (= {:graph {:user/by-id {123 {:user/gender :gender/male}}} 41 | :missing []} 42 | (get cache [[:user/by-id 123 :user/gender :gender/string]])))) 43 | (testing "getting a ref" 44 | (is (= {:graph {:users {0 (ref [:user/by-id 123])} 45 | :user/by-id {123 {:user/name "Erik" 46 | :user/age 31}}} 47 | :missing []} 48 | (get cache [[:users 0 [:user/name :user/age]]])))) 49 | (testing "getting a nil value" 50 | (is (= {:graph {:users {1 nil}} 51 | :missing []} 52 | (get cache [[:users 1 [:user/name :user/age]]])))) 53 | (testing "getting a nil atom" 54 | (is (= {:graph {:users {2 nil}} 55 | :missing []} 56 | (get cache [[:users 2 [:user/name :user/age]]])))) 57 | (testing "getting an empty atom" 58 | (is (= {:graph {} 59 | :missing []} 60 | (get cache [[:users 3 [:user/name :user/age]]])))) 61 | (testing "getting a missing path" 62 | (is (= {:graph {} 63 | :missing [[:user/by-id 123 :user/height]]} 64 | (get cache [[:user/by-id 123 :user/height]])))) 65 | (testing "getting a deep missing path" 66 | (is (= {:graph {} 67 | :missing [[:user/by-id 456 :user/email [:email/address 68 | :email/domain]]]} 69 | (get cache [[:user/by-id 456 :user/email [:email/address 70 | :email/domain]]])))) 71 | (testing "getting a missing path with ref" 72 | (is (= {:graph {} 73 | :missing [[:users 4 [:user/name :user/age]]]} 74 | (get cache [[:users 4 [:user/name :user/age]]])))) 75 | (testing "getting a deep missing path with ref" 76 | (is (= {:graph {} 77 | :missing [[:users 4 :user/email [:email/address 78 | :email/domain]]]} 79 | (get cache [[:users 4 :user/email [:email/address 80 | :email/domain]]])))) 81 | (testing "getting a partial missing path" 82 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 83 | :user/age 31}}} 84 | :missing [[:user/by-id 123 :user/height]]} 85 | (get cache [[:user/by-id 123 [:user/name 86 | :user/age 87 | :user/height]]])))) 88 | (testing "getting a partial missing path with ref" 89 | (is (= {:graph {:users {0 (ref [:user/by-id 123])} 90 | :user/by-id {123 {:user/name "Erik" 91 | :user/age 31}}} 92 | :missing [[:user/by-id 123 :user/height]]} 93 | (get cache [[:users 0 [:user/name 94 | :user/age 95 | :user/height]]])))) 96 | (testing "getting a range" 97 | (is (= {:graph {:users {0 (ref [:user/by-id 123]) 98 | 1 nil 99 | 2 nil} 100 | :user/by-id {123 {:user/name "Erik" 101 | :user/age 31}}} 102 | :missing [[:users 4 [:user/name :user/age]]]} 103 | (get cache [[:users (range 0 5) [:user/name :user/age]]])))))) 104 | 105 | (deftest get-denormalized-with-paths-test 106 | (let [get #(get %1 %2 {:normalize false 107 | :path-info true 108 | :boxed false})] 109 | (testing "unoptimized get" 110 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 111 | :user/age 31 112 | :cambo/path [:user/by-id 123]} 113 | :cambo/path [:user/by-id]}} 114 | :missing []} 115 | (get cache [[:user/by-id 123 [:user/name :user/age]]])))) 116 | (testing "getting an non-boxed value" 117 | (is (= {:graph {:user/by-id {123 {:user/gender :gender/male 118 | :cambo/path [:user/by-id 123]} 119 | :cambo/path [:user/by-id]}} 120 | :missing []} 121 | (get cache [[:user/by-id 123 :user/gender]])))) 122 | (testing "getting a branch" 123 | (is (= {:graph {:user/by-id {123 {:cambo/path [:user/by-id 123]} 124 | :cambo/path [:user/by-id]}} 125 | :missing []} 126 | (get cache [[:user/by-id 123]])))) 127 | (testing "getting into an atom" 128 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 129 | :cambo/path [:user/by-id 123]} 130 | :cambo/path [:user/by-id]}} 131 | :missing []} 132 | (get cache [[:user/by-id 123 :user/name :name/first]])))) 133 | (testing "getting into non-boxed value" 134 | (is (= {:graph {:user/by-id {123 {:user/gender :gender/male 135 | :cambo/path [:user/by-id 123]} 136 | :cambo/path [:user/by-id]}} 137 | :missing []} 138 | (get cache [[:user/by-id 123 :user/gender :gender/string]])))) 139 | (testing "getting a ref" 140 | (is (= {:graph {:users {0 {:user/name "Erik" 141 | :user/age 31 142 | :cambo/path [:user/by-id 123]} 143 | :cambo/path [:users]}} 144 | :missing []} 145 | (get cache [[:users 0 [:user/name :user/age]]])))) 146 | (testing "getting a nil value" 147 | (is (= {:graph {:users {1 nil 148 | :cambo/path [:users]}} 149 | :missing []} 150 | (get cache [[:users 1 [:user/name :user/age]]])))) 151 | (testing "getting a nil atom" 152 | (is (= {:graph {:users {2 nil 153 | :cambo/path [:users]}} 154 | :missing []} 155 | (get cache [[:users 2 [:user/name :user/age]]])))) 156 | (testing "getting an empty atom" 157 | (is (= {:graph {:users {:cambo/path [:users]}} 158 | :missing []} 159 | (get cache [[:users 3 [:user/name :user/age]]])))) 160 | (testing "getting a missing path" 161 | (is (= {:graph {:user/by-id {123 {:cambo/path [:user/by-id 123]} 162 | :cambo/path [:user/by-id]}} 163 | :missing [[:user/by-id 123 :user/height]]} 164 | (get cache [[:user/by-id 123 :user/height]])))) 165 | (testing "getting a deep missing path" 166 | (is (= {:graph {:user/by-id {:cambo/path [:user/by-id]}} 167 | :missing [[:user/by-id 456 :user/email [:email/address 168 | :email/domain]]]} 169 | (get cache [[:user/by-id 456 :user/email [:email/address 170 | :email/domain]]])))) 171 | (testing "getting a missing path with ref" 172 | (is (= {:graph {:users {:cambo/path [:users]}} 173 | :missing [[:users 4 [:user/name :user/age]]]} 174 | (get cache [[:users 4 [:user/name :user/age]]])))) 175 | (testing "getting a deep missing path with ref" 176 | (is (= {:graph {:users {:cambo/path [:users]}} 177 | :missing [[:users 4 :user/email [:email/address 178 | :email/domain]]]} 179 | (get cache [[:users 4 :user/email [:email/address 180 | :email/domain]]])))) 181 | (testing "getting a partial missing path" 182 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 183 | :user/age 31 184 | :cambo/path [:user/by-id 123]} 185 | :cambo/path [:user/by-id]}} 186 | :missing [[:user/by-id 123 :user/height]]} 187 | (get cache [[:user/by-id 123 [:user/name 188 | :user/age 189 | :user/height]]])))) 190 | (testing "getting a partial missing path with ref" 191 | (is (= {:graph {:users {0 {:user/name "Erik" 192 | :user/age 31 193 | :cambo/path [:user/by-id 123]} 194 | :cambo/path [:users]}} 195 | :missing [[:user/by-id 123 :user/height]]} 196 | (get cache [[:users 0 [:user/name 197 | :user/age 198 | :user/height]]])))) 199 | (testing "getting a range" 200 | (is (= {:graph {:users {0 {:user/name "Erik" 201 | :user/age 31 202 | :cambo/path [:user/by-id 123]} 203 | 1 nil 204 | 2 nil 205 | :cambo/path [:users]}} 206 | :missing [[:users 4 [:user/name :user/age]]]} 207 | (get cache [[:users (range 0 5) [:user/name :user/age]]])))) 208 | (testing "getting a leaf ref range" 209 | #_(is (= {:graph {:users {0 {:cambo/path [:user/by-id 123]} 210 | 1 nil 211 | 2 nil 212 | :cambo/path [:users]}} 213 | :missing [[:users 4]]} 214 | (get cache [[:users (range 0 5)]])))))) 215 | 216 | (deftest pull-basic-test 217 | (let [pull #(pull %1 %2 {:normalize true 218 | :path-info false 219 | :boxed false})] 220 | (testing "unoptimized get" 221 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 222 | :user/age 31}}} 223 | :missing []} 224 | (pull cache [{:user/by-id [{123 [:user/name 225 | :user/age]}]}])))) 226 | (testing "getting an non-boxed value" 227 | (is (= {:graph {:user/by-id {123 {:user/gender :gender/male}}} 228 | :missing []} 229 | (pull cache [{:user/by-id [{123 [:user/gender]}]}])))) 230 | (testing "getting a branch" 231 | (is (= {:graph {} 232 | :missing []} 233 | (pull cache [{:user/by-id [123]}])))) 234 | (testing "getting into an atom" 235 | (is (= {:graph {:user/by-id {123 {:user/name "Erik"}}} 236 | :missing []} 237 | (pull cache [{:user/by-id [{123 [{:user/name [:name/first]}]}]}])))) 238 | (testing "getting into non-boxed value" 239 | (is (= {:graph {:user/by-id {123 {:user/gender :gender/male}}} 240 | :missing []} 241 | (pull cache [{:user/by-id [{123 [{:user/gender [:gender/string]}]}]}])))) 242 | (testing "getting a ref" 243 | (is (= {:graph {:users {0 (ref [:user/by-id 123])} 244 | :user/by-id {123 {:user/name "Erik" 245 | :user/age 31}}} 246 | :missing []} 247 | (pull cache [{:users [{0 [:user/name 248 | :user/age]}]}])))) 249 | (testing "getting a nil value" 250 | (is (= {:graph {:users {1 nil}} 251 | :missing []} 252 | (pull cache [{:users [{1 [:user/name 253 | :user/age]}]}])))) 254 | (testing "getting a nil atom" 255 | (is (= {:graph {:users {2 nil}} 256 | :missing []} 257 | (pull cache [{:users [{2 [:user/name 258 | :user/age]}]}])))) 259 | (testing "getting an empty atom" 260 | (is (= {:graph {} 261 | :missing []} 262 | (pull cache [{:users [{3 [:user/name 263 | :user/age]}]}])))) 264 | (testing "getting a missing path" 265 | (is (= {:graph {} 266 | :missing [{:user/by-id [{123 [:user/height]}]}]} 267 | (pull cache [{:user/by-id [{123 [:user/height]}]}])))) 268 | (testing "getting a deep missing path" 269 | (is (= {:graph {} 270 | :missing [{:user/by-id [{456 [{:user/email [:email/address 271 | :email/domain]}]}]}]} 272 | (pull cache [{:user/by-id [{456 [{:user/email [:email/address 273 | :email/domain]}]}]}])))) 274 | (testing "getting a missing path with ref" 275 | (is (= {:graph {} 276 | :missing [{:users [{4 [:user/name 277 | :user/age]}]}]} 278 | (pull cache [{:users [{4 [:user/name 279 | :user/age]}]}])))) 280 | (testing "getting a deep missing path with ref" 281 | (is (= {:graph {} 282 | :missing [{:users [{4 [{:user/email [:email/address 283 | :email/domain]}]}]}]} 284 | (pull cache [{:users [{4 [{:user/email [:email/address 285 | :email/domain]}]}]}])))) 286 | (testing "getting a partial missing path" 287 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 288 | :user/age 31}}} 289 | :missing [{:user/by-id [{123 [:user/height]}]}]} 290 | (pull cache [{:user/by-id [{123 [:user/name 291 | :user/age 292 | :user/height]}]}])))) 293 | (testing "getting a partial missing path with ref" 294 | (is (= {:graph {:users {0 (ref [:user/by-id 123])} 295 | :user/by-id {123 {:user/name "Erik" 296 | :user/age 31}}} 297 | :missing [{:user/by-id [{123 [:user/height]}]}]} 298 | (pull cache [{:users [{0 [:user/name 299 | :user/age 300 | :user/height]}]}])))) 301 | (testing "getting a range" 302 | (is (= {:graph {:users {0 (ref [:user/by-id 123]) 303 | 1 nil 304 | 2 nil} 305 | :user/by-id {123 {:user/name "Erik" 306 | :user/age 31}}} 307 | :missing [{:users [{4 [:user/name 308 | :user/age]}]}]} 309 | (pull cache [{:users [{(range 0 5) [:user/name 310 | :user/age]}]}])))))) 311 | 312 | (deftest pull-denormalized-with-paths-test 313 | (let [pull #(pull %1 %2 {:normalize false 314 | :path-info true 315 | :boxed false})] 316 | (testing "unoptimized get" 317 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 318 | :user/age 31 319 | :cambo/path [:user/by-id 123]} 320 | :cambo/path [:user/by-id]}} 321 | :missing []} 322 | (pull cache [{:user/by-id [{123 [:user/name 323 | :user/age]}]}])))) 324 | (testing "getting an non-boxed value" 325 | (is (= {:graph {:user/by-id {123 {:user/gender :gender/male 326 | :cambo/path [:user/by-id 123]} 327 | :cambo/path [:user/by-id]}} 328 | :missing []} 329 | (pull cache [{:user/by-id [{123 [:user/gender]}]}])))) 330 | (testing "getting a branch" 331 | (is (= {:graph {:user/by-id {123 {:cambo/path [:user/by-id 123]} 332 | :cambo/path [:user/by-id]}} 333 | :missing []} 334 | (pull cache [{:user/by-id [123]}])))) 335 | (testing "getting into an atom" 336 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 337 | :cambo/path [:user/by-id 123]} 338 | :cambo/path [:user/by-id]}} 339 | :missing []} 340 | (pull cache [{:user/by-id [{123 [{:user/name [:name/first]}]}]}])))) 341 | (testing "getting into non-boxed value" 342 | (is (= {:graph {:user/by-id {123 {:user/gender :gender/male 343 | :cambo/path [:user/by-id 123]} 344 | :cambo/path [:user/by-id]}} 345 | :missing []} 346 | (pull cache [{:user/by-id [{123 [{:user/gender [:gender/string]}]}]}])))) 347 | (testing "getting a ref" 348 | (is (= {:graph {:users {0 {:user/name "Erik" 349 | :user/age 31 350 | :cambo/path [:user/by-id 123]} 351 | :cambo/path [:users]}} 352 | :missing []} 353 | (pull cache [{:users [{0 [:user/name 354 | :user/age]}]}])))) 355 | (testing "getting a nil value" 356 | (is (= {:graph {:users {1 nil 357 | :cambo/path [:users]}} 358 | :missing []} 359 | (pull cache [{:users [{1 [:user/name 360 | :user/age]}]}])))) 361 | (testing "getting a nil atom" 362 | (is (= {:graph {:users {2 nil 363 | :cambo/path [:users]}} 364 | :missing []} 365 | (pull cache [{:users [{2 [:user/name 366 | :user/age]}]}])))) 367 | (testing "getting an empty atom" 368 | (is (= {:graph {:users {:cambo/path [:users]}} 369 | :missing []} 370 | (pull cache [{:users [{3 [:user/name 371 | :user/age]}]}])))) 372 | (testing "getting a missing path" 373 | (is (= {:graph {:user/by-id {123 {:cambo/path [:user/by-id 123]} 374 | :cambo/path [:user/by-id]}} 375 | :missing [{:user/by-id [{123 [:user/height]}]}]} 376 | (pull cache [{:user/by-id [{123 [:user/height]}]}])))) 377 | (testing "getting a deep missing path" 378 | (is (= {:graph {:user/by-id {:cambo/path [:user/by-id]}} 379 | :missing [{:user/by-id [{456 [{:user/email [:email/address 380 | :email/domain]}]}]}]} 381 | (pull cache [{:user/by-id [{456 [{:user/email [:email/address 382 | :email/domain]}]}]}])))) 383 | (testing "getting a missing path with ref" 384 | (is (= {:graph {:users {:cambo/path [:users]}} 385 | :missing [{:users [{4 [:user/name 386 | :user/age]}]}]} 387 | (pull cache [{:users [{4 [:user/name 388 | :user/age]}]}])))) 389 | (testing "getting a deep missing path with ref" 390 | (is (= {:graph {:users {:cambo/path [:users]}} 391 | :missing [{:users [{4 [{:user/email [:email/address 392 | :email/domain]}]}]}]} 393 | (pull cache [{:users [{4 [{:user/email [:email/address 394 | :email/domain]}]}]}])))) 395 | (testing "getting a partial missing path" 396 | (is (= {:graph {:user/by-id {123 {:user/name "Erik" 397 | :user/age 31 398 | :cambo/path [:user/by-id 123]} 399 | :cambo/path [:user/by-id]}} 400 | :missing [{:user/by-id [{123 [:user/height]}]}]} 401 | (pull cache [{:user/by-id [{123 [:user/name 402 | :user/age 403 | :user/height]}]}])))) 404 | (testing "getting a partial missing path with ref" 405 | (is (= {:graph {:users {0 {:user/name "Erik" 406 | :user/age 31 407 | :cambo/path [:user/by-id 123]} 408 | :cambo/path [:users]}} 409 | :missing [{:user/by-id [{123 [:user/height]}]}]} 410 | (pull cache [{:users [{0 [:user/name 411 | :user/age 412 | :user/height]}]}])))) 413 | (testing "getting a range" 414 | (is (= {:graph {:users {0 {:user/name "Erik" 415 | :user/age 31 416 | :cambo/path [:user/by-id 123]} 417 | 1 nil 418 | 2 nil 419 | :cambo/path [:users]}} 420 | :missing [{:users [{4 [:user/name 421 | :user/age]}]}]} 422 | (pull cache [{:users [{(range 0 5) [:user/name 423 | :user/age]}]}])))) 424 | (testing "getting a leaf ref range" 425 | (is (= {:graph {:users {0 {:cambo/path [:user/by-id 123]} 426 | 1 nil 427 | 2 nil 428 | :cambo/path [:users]}} 429 | :missing [{:users [4]}]} 430 | (pull cache [{:users [(range 0 5)]}])))))) 431 | 432 | (deftest missing-test 433 | (testing "unoptimized get" 434 | (is (= [] 435 | (missing cache [[:user/by-id 123 [:user/name :user/age]]])))) 436 | (testing "getting an non-boxed value" 437 | (is (= [] 438 | (missing cache [[:user/by-id 123 :user/gender]])))) 439 | (testing "getting a branch" 440 | (is (= [] 441 | (missing cache [[:user/by-id 123]])))) 442 | (testing "getting into an atom" 443 | (is (= [] 444 | (missing cache [[:user/by-id 123 :user/name :name/first]])))) 445 | (testing "getting into non-boxed value" 446 | (is (= [] 447 | (missing cache [[:user/by-id 123 :user/gender :gender/string]])))) 448 | (testing "getting a ref" 449 | (is (= [] 450 | (missing cache [[:users 0 [:user/name :user/age]]])))) 451 | (testing "getting a nil value" 452 | (is (= [] 453 | (missing cache [[:users 1 [:user/name :user/age]]])))) 454 | (testing "getting a nil atom" 455 | (is (= [] 456 | (missing cache [[:users 2 [:user/name :user/age]]])))) 457 | (testing "getting an empty atom" 458 | (is (= [] 459 | (missing cache [[:users 3 [:user/name :user/age]]])))) 460 | (testing "getting a missing path" 461 | (is (= [[:user/by-id 123 :user/height]] 462 | (missing cache [[:user/by-id 123 :user/height]])))) 463 | (testing "getting a deep missing path" 464 | (is (= [[:user/by-id 456 :user/email [:email/address 465 | :email/domain]]] 466 | (missing cache [[:user/by-id 456 :user/email [:email/address 467 | :email/domain]]])))) 468 | (testing "getting a missing path with ref" 469 | (is (= [[:users 4 [:user/name :user/age]]] 470 | (missing cache [[:users 4 [:user/name :user/age]]])))) 471 | (testing "getting a deep missing path with ref" 472 | (is (= [[:users 4 :user/email [:email/address 473 | :email/domain]]] 474 | (missing cache [[:users 4 :user/email [:email/address 475 | :email/domain]]])))) 476 | (testing "getting a partial missing path" 477 | (is (= [[:user/by-id 123 :user/height]] 478 | (missing cache [[:user/by-id 123 [:user/name 479 | :user/age 480 | :user/height]]])))) 481 | (testing "getting a partial missing path with ref" 482 | (is (= [[:user/by-id 123 :user/height]] 483 | (missing cache [[:users 0 [:user/name 484 | :user/age 485 | :user/height]]])))) 486 | (testing "getting a range" 487 | (is (= [[:users 4 [:user/name :user/age]]] 488 | (missing cache [[:users (range 0 5) [:user/name :user/age]]]))))) 489 | 490 | (deftest missing-transient-test 491 | (testing "unoptimized get" 492 | (is (= [] 493 | (missing-transient cache [[:user/by-id 123 [:user/name :user/age]]])))) 494 | (testing "getting an non-boxed value" 495 | (is (= [] 496 | (missing-transient cache [[:user/by-id 123 :user/gender]])))) 497 | (testing "getting a branch" 498 | (is (= [] 499 | (missing-transient cache [[:user/by-id 123]])))) 500 | (testing "getting into an atom" 501 | (is (= [] 502 | (missing-transient cache [[:user/by-id 123 :user/name :name/first]])))) 503 | (testing "getting into non-boxed value" 504 | (is (= [] 505 | (missing-transient cache [[:user/by-id 123 :user/gender :gender/string]])))) 506 | (testing "getting a ref" 507 | (is (= [] 508 | (missing-transient cache [[:users 0 [:user/name :user/age]]])))) 509 | (testing "getting a nil value" 510 | (is (= [] 511 | (missing-transient cache [[:users 1 [:user/name :user/age]]])))) 512 | (testing "getting a nil atom" 513 | (is (= [] 514 | (missing-transient cache [[:users 2 [:user/name :user/age]]])))) 515 | (testing "getting an empty atom" 516 | (is (= [] 517 | (missing-transient cache [[:users 3 [:user/name :user/age]]])))) 518 | (testing "getting a missing path" 519 | (is (= [[:user/by-id 123 :user/height]] 520 | (missing-transient cache [[:user/by-id 123 :user/height]])))) 521 | (testing "getting a deep missing path" 522 | (is (= [[:user/by-id 456 :user/email [:email/address 523 | :email/domain]]] 524 | (missing-transient cache [[:user/by-id 456 :user/email [:email/address 525 | :email/domain]]])))) 526 | (testing "getting a missing path with ref" 527 | (is (= [[:users 4 [:user/name :user/age]]] 528 | (missing-transient cache [[:users 4 [:user/name :user/age]]])))) 529 | (testing "getting a deep missing path with ref" 530 | (is (= [[:users 4 :user/email [:email/address 531 | :email/domain]]] 532 | (missing-transient cache [[:users 4 :user/email [:email/address 533 | :email/domain]]])))) 534 | (testing "getting a partial missing path" 535 | (is (= [[:user/by-id 123 :user/height]] 536 | (missing-transient cache [[:user/by-id 123 [:user/name 537 | :user/age 538 | :user/height]]])))) 539 | (testing "getting a partial missing path with ref" 540 | (is (= [[:user/by-id 123 :user/height]] 541 | (missing-transient cache [[:users 0 [:user/name 542 | :user/age 543 | :user/height]]])))) 544 | (testing "getting a range" 545 | (is (= [[:users 4 [:user/name :user/age]]] 546 | (missing-transient cache [[:users (range 0 5) [:user/name :user/age]]]))))) 547 | 548 | (comment 549 | 550 | 551 | (require '[criterium.core :as bench]) 552 | 553 | (bench/quick-bench 554 | (missing-transient cache [[:users (range 0 100) [:user/name 555 | :user/age 556 | :user/height]]])) 557 | 558 | (bench/quick-bench 559 | (missing cache [[:users (range 0 100) [:user/name 560 | :user/age 561 | :user/height]]])) 562 | 563 | (bench/quick-bench 564 | (:missing (get cache [[:users (range 0 100) [:user/name 565 | :user/age 566 | :user/height]]]))) 567 | 568 | 569 | (= (missing-transient cache [[:users (range 0 100) [:user/name 570 | :user/age 571 | :user/height]]]) 572 | (missing cache [[:users (range 0 100) [:user/name 573 | :user/age 574 | :user/height]]]) 575 | (:missing (get cache [[:users (range 0 100) [:user/name 576 | :user/age 577 | :user/height]]]))) 578 | 579 | (bench/quick-bench 580 | (pull cache [{:users [{(range 0 5) [:user/name 581 | :user/age]}]}] 582 | {:normalize true 583 | :path-info false 584 | :boxed false})) 585 | 586 | ;; 16us w/ no range check 587 | ;; 27us w/ cond range check 588 | ;; 32us w/ true range check 589 | ;; 16us w/ false range check 590 | (bench/quick-bench 591 | (get cache [[:users (range 0 5) [:user/name :user/age]]] 592 | {:normalize true 593 | :path-info false 594 | :boxed false})) 595 | 596 | ;; 98us w/ no range check 597 | ;; 25us w/ cond range check 598 | ;; 32us w/ true range check 599 | ;; 90us w/ false range check 600 | (bench/quick-bench 601 | (get cache [[:users (range 0 100) [:user/name :user/age]]] 602 | {:normalize true 603 | :path-info false 604 | :boxed false})) 605 | 606 | 607 | (= (:graph (pull cache [{:users [{(range 0 5) [:user/name 608 | :user/age]}]}] 609 | {:normalize true 610 | :path-info false 611 | :boxed false})) 612 | (:graph (get cache [[:users (range 0 5) [:user/name :user/age]]] 613 | {:normalize true 614 | :path-info false 615 | :boxed false}))) 616 | 617 | ) -------------------------------------------------------------------------------- /test/cambo/router_test.clj: -------------------------------------------------------------------------------- 1 | (ns cambo.router-test 2 | (:refer-clojure :exclude [get set range atom ref]) 3 | (:require [cambo.router :refer :all] 4 | [cambo.core :as core :refer [path-value range atom ref]] 5 | [clojure.test :refer :all])) 6 | 7 | (deftest route-hash-test 8 | (is (= [:foo 0 :cambo.router/i "bar"] 9 | (route-hash [:foo 0 INTEGERS "bar"]))) 10 | (is (= (route-hash [:foo 0 INTEGERS "bar"]) 11 | (route-hash [:foo 0 RANGES "bar"])))) 12 | 13 | (deftest expand-routeset-test 14 | (is (= [[:user/by-id 0 :name] 15 | [:user/by-id 0 :age] 16 | [:user/by-id 1 :name] 17 | [:user/by-id 1 :age] 18 | [:users 0 :name] 19 | [:users 0 :age] 20 | [:users 1 :name] 21 | [:users 1 :age]] 22 | (expand-routeset [[:user/by-id :users] [0 1] [:name :age]])))) 23 | 24 | (deftest route-tree-test 25 | (let [route1 {:route [:users/by-id RANGES [:name :age]] 26 | :get true} 27 | route2 {:route [:users] 28 | :get true}] 29 | (is (= {:users/by-id {RANGES {:name {match-key {:get route1}} 30 | :age {match-key {:get route1}}}} 31 | :users {match-key {:get route2}}} 32 | (clojure.walk/postwalk #(cond-> % (map? %) (dissoc id-key)) 33 | (route-tree [route1 route2]))))) 34 | (is (thrown? Exception 35 | (let [route1 {:route [:users/by-id RANGES [:name :age]] 36 | :get true} 37 | route2 {:route [:users/by-id INTEGERS [:name :age]] 38 | :get true}] 39 | (route-tree [route1 route2]))))) 40 | 41 | (deftest strip-keys-test 42 | (is (= [[:a] []] 43 | (strip KEYS :a))) 44 | (is (= [["a"] []] 45 | (strip KEYS ["a"]))) 46 | (is (= [[1] []] 47 | (strip KEYS 1))) 48 | (is (= [[1] []] 49 | (strip KEYS [1]))) 50 | (is (= [[1 2 3] []] 51 | (strip KEYS [1 2 3]))) 52 | (is (= [[1 :a 2 :b 3 :c :d] []] 53 | (strip KEYS [1 :a 2 :b 3 :c :d]))) 54 | (is (= [[(core/range 0 10)] []] 55 | (strip KEYS (core/range 0 10)))) 56 | (is (= [[(core/range 0 10) (core/range 5 15)] []] 57 | (strip KEYS [(core/range 0 10) (core/range 5 15)]))) 58 | (is (= [[(core/range 0 10) :a (core/range 5 15) :b 1 2] []] 59 | (strip KEYS [(core/range 0 10) :a (core/range 5 15) :b 1 2])))) 60 | 61 | (deftest strip-integers-test 62 | (is (= [[1] []] 63 | (strip INTEGERS 1))) 64 | (is (= [[1] []] 65 | (strip INTEGERS [1]))) 66 | (is (= [[] [:a]] 67 | (strip INTEGERS :a))) 68 | (is (= [[] ["a"]] 69 | (strip INTEGERS ["a"]))) 70 | (is (= [[1 2 3] []] 71 | (strip INTEGERS [1 2 3]))) 72 | (is (= [[1 2 3] [:a :b :c :d]] 73 | (strip INTEGERS [1 :a 2 :b 3 :c :d]))) 74 | (is (= [[(core/range 0 10)] []] 75 | (strip INTEGERS (core/range 0 10)))) 76 | (is (= [[(core/range 0 10) (core/range 5 15)] []] 77 | (strip INTEGERS [(core/range 0 10) (core/range 5 15)]))) 78 | (is (= [[(core/range 0 10) (core/range 5 15) 1 2 ] [:a :b]] 79 | (strip INTEGERS [(core/range 0 10) :a (core/range 5 15) :b 1 2])))) 80 | 81 | (deftest strip-ranges-test 82 | (is (= [[1] []] 83 | (strip RANGES 1))) 84 | (is (= [[1] []] 85 | (strip RANGES [1]))) 86 | (is (= [[] [:a]] 87 | (strip RANGES :a))) 88 | (is (= [[] ["a"]] 89 | (strip RANGES ["a"]))) 90 | (is (= [[1 2 3] []] 91 | (strip RANGES [1 2 3]))) 92 | (is (= [[1 2 3] [:a :b :c :d]] 93 | (strip RANGES [1 :a 2 :b 3 :c :d]))) 94 | (is (= [[(core/range 0 10)] []] 95 | (strip RANGES (core/range 0 10)))) 96 | (is (= [[(core/range 0 10) (core/range 5 15)] []] 97 | (strip RANGES [(core/range 0 10) (core/range 5 15)]))) 98 | (is (= [[(core/range 0 10) (core/range 5 15) 1 2 ] [:a :b]] 99 | (strip RANGES [(core/range 0 10) :a (core/range 5 15) :b 1 2])))) 100 | 101 | (deftest strip-keyword-test 102 | (is (= [[:a] []] 103 | (strip :a :a))) 104 | (is (= [[:a] []] 105 | (strip :a [:a]))) 106 | (is (= [[] ["a"]] 107 | (strip :a "a"))) 108 | (is (= [[] ["a" 1 (core/range 1 10)]] 109 | (strip :a ["a" 1 (core/range 1 10)])))) 110 | 111 | (deftest strip-long-test 112 | (is (= [[] [(core/range 5 10)]] 113 | (strip 1 (core/range 5 10)))) 114 | (is (= [[] [(core/range 5 10)]] 115 | (strip 15 (core/range 5 10)))) 116 | (is (= [[0] [(core/range 1 10)]] 117 | (strip 0 (core/range 0 10)))) 118 | (is (= [[] [(core/range 0 10)]] 119 | (strip 10 (core/range 0 10)))) 120 | (is (= [[1] [(core/range 0 1) (core/range 2 10)]] 121 | (strip 1 (core/range 0 10)))) 122 | (is (= [[5] [(core/range 0 5) (core/range 6 10)]] 123 | (strip 5 (core/range 0 10)))) 124 | (is (= [[8] [(core/range 0 8) (core/range 9 10)]] 125 | (strip 8 (core/range 0 10)))) 126 | (is (= [[9] [(core/range 0 9)]] 127 | (strip 9 (core/range 0 10))))) 128 | 129 | (deftest strip-vector-test 130 | (is (= [[1 8] [(core/range 0 1) (core/range 2 8) (core/range 9 10)]] 131 | (strip [1 8] (core/range 0 10)))) 132 | (is (= [[1 8] [2 3 4 5 6 7 9 10]] 133 | (strip [1 8] [1 2 3 4 5 6 7 8 9 10]))) 134 | (is (= [[8] ["a" :b]] 135 | (strip [1 8] ["a" :b 8])))) 136 | 137 | (deftest strip-path-test 138 | (testing "simple keys" 139 | (is (= [[:a :b :c] 140 | []] 141 | (strip-path [:a :b :c] 142 | [:a :b :c])))) 143 | (testing "simple keys with route token" 144 | (is (= [[:a :b :c] 145 | []] 146 | (strip-path [:a KEYS :c] 147 | [:a :b :c])))) 148 | (testing "path with array args" 149 | (is (= [[:a [:b :d] :c] 150 | []] 151 | (strip-path [:a KEYS :c] 152 | [:a [:b :d] :c])))) 153 | (testing "path with range args" 154 | (is (= [[:a (range 0 6) :c] 155 | []] 156 | (strip-path [:a RANGES :c] 157 | [:a (range 0 6) :c])))) 158 | (testing "path with array keys" 159 | (is (= [[:a :b :c] 160 | [[:a :d :c]]] 161 | (strip-path [:a :b :c] 162 | [:a [:b :d] :c])))) 163 | (testing "path with range" 164 | (is (= [[:a 1 :c] 165 | [[:a (range 0 1) :c] 166 | [:a (range 2 6) :c]]] 167 | (strip-path [:a 1 :c] 168 | [:a (range 0 6) :c])))) 169 | (testing "path with array range" 170 | (is (= [[:a 1 :c] 171 | [[:a (range 0 1) :c] 172 | [:a (range 2 3) :c] 173 | [:a (range 5 6) :c]]] 174 | (strip-path [:a 1 :c] 175 | [:a [(range 0 3) (range 5 6)] :c])))) 176 | (testing "path with complement partial match" 177 | (is (= [[:a :c :e] 178 | [[:b [:c :d] [:e :f]] 179 | [:a :d [:e :f]] 180 | [:a :c :f]]] 181 | (strip-path [:a :c :e] 182 | [[:a :b] [:c :d] [:e :f]]))))) 183 | 184 | (deftest get-test 185 | (let [noop (fn [& _]) 186 | video-routes {:summary (fn [f] 187 | [{:route [:videos :summary] 188 | :get (fn [path _] 189 | (when f (f path)) 190 | [(path-value [:videos :summary] (atom 75))])}])} 191 | precedence-router (fn [on-title on-rating] 192 | (router [{:route [:videos INTEGERS :title] 193 | :get (fn [[_ ids _ :as path] _] 194 | (when on-title (on-title path)) 195 | (for [id ids] 196 | (path-value [:videos id :title] (str "title " id))))} 197 | {:route [:videos INTEGERS :rating] 198 | :get (fn [[_ ids _ :as path] _] 199 | (when on-rating (on-rating path)) 200 | (for [id ids] 201 | (path-value [:videos id :rating] (str "rating " id))))} 202 | {:route [:lists KEYS INTEGERS] 203 | :get (fn [[_ ids idxs] _] 204 | (for [id ids 205 | idx idxs] 206 | (path-value [:lists id idx] (ref [:videos idx]))))}]))] 207 | (testing "simple route" 208 | (let [router (router ((video-routes :summary) noop))] 209 | (is (= {:videos {:summary (atom 75)}} 210 | (:graph (get router [[:videos :summary]])))))) 211 | (testing "should validate that optimizedPathSets strips out already found data." 212 | (let [calls (clojure.core/atom 0) 213 | router (router [{:route [:lists KEYS] 214 | :get (fn [[_ ids] _] 215 | (for [id ids] 216 | (if (= 0 id) 217 | (path-value [:lists id] (ref [:two :be 956])) 218 | (path-value [:lists id] (ref [:lists 0])))))} 219 | {:route [:two :be INTEGERS :summary] 220 | :get (fn [[_ _ ids] _] 221 | (for [id ids] 222 | (do 223 | (swap! calls inc) 224 | (path-value [:two :be id :summary] "hello world"))))}]) 225 | result (get router [[:lists [0 1] :summary]])] 226 | (is (= {:lists {0 (ref [:two :be 956]) 227 | 1 (ref [:lists 0])} 228 | :two {:be {956 {:summary (atom "hello world")}}}} 229 | (:graph result))) 230 | (is (= 1 @calls)))) 231 | (testing "should do precedence stripping." 232 | (let [rating (clojure.core/atom 0) 233 | title (clojure.core/atom 0) 234 | router (precedence-router 235 | (fn [path] 236 | (swap! title inc) 237 | (is (= [:videos [123] :title] 238 | path))) 239 | (fn [path] 240 | (swap! rating inc) 241 | (is (= [:videos [123] :rating] 242 | path)))) 243 | results (gets router [[:videos 123 [:title :rating]]] {}) 244 | result (first results)] 245 | (is (= 1 (count results))) 246 | (is (= {:videos {123 {:title (atom "title 123") 247 | :rating (atom "rating 123")}}} 248 | (:graph result))) 249 | (is (= 1 @title)) 250 | (is (= 1 @rating)))) 251 | (testing "should do precedence matching." 252 | (let [specific (clojure.core/atom 0) 253 | keys (clojure.core/atom 0) 254 | router (router [{:route [:a :specific] 255 | :get (fn [_ _] 256 | (swap! specific inc) 257 | [(path-value [:a :specific] "hello world")])} 258 | {:route [:a KEYS] 259 | :get (fn [_ _] 260 | (swap! keys inc) 261 | [(path-value [:a :specific] "hello world")])}]) 262 | _ (get router [[:a :specific]])] 263 | (is (= 1 @specific)) 264 | (is (= 0 @keys)))) 265 | (testing "should grab a reference." 266 | (let [router (precedence-router nil nil) 267 | results (gets router [[:lists :abc 0]] {})] 268 | (is (= 1 (count results))) 269 | (is (= {:lists {:abc {0 (ref [:videos 0])}}} 270 | (:graph (last results)))))) 271 | (testing "should not follow references if no keys specified after path to reference" 272 | (let [router (router [{:route [:products-by-id KEYS KEYS] 273 | :get (fn [_ _] (throw (ex-info "reference followed in error" {})))} 274 | {:route [:proffers-by-id INTEGERS :products-list RANGES] 275 | :get (fn [_ _] [(path-value [:proffers-by-id 1 :products-list 0] 276 | (ref [:products-by-id "CSC1471105X"])) 277 | (path-value [:proffers-by-id 1 :products-list 1] 278 | (ref [:products-by-id "HON4033T"]))])}])] 279 | (is (= {:proffers-by-id {1 {:products-list {0 (ref [:products-by-id "CSC1471105X"]) 280 | 1 (ref [:products-by-id "HON4033T"])}}}} 281 | (:graph (get router [[:proffers-by-id 1 :products-list (range 0 2)]])))))))) 282 | 283 | ;; TODO: copy the falcor set tests ... not 100% sold on impl working on harder examples! 284 | (deftest router-set-test 285 | (let [users-router (fn [] 286 | (let [users (clojure.core/atom {1 "Erik" 287 | 2 "Jack"}) 288 | router (router [{:route [:users RANGES] 289 | :get (fn [[_ ranges] _] 290 | (let [users (vec @users)] 291 | (for [idx (indices ranges) 292 | :let [[id _] (clojure.core/get users idx)]] 293 | (core/path-value [:users idx] 294 | (if id 295 | (ref [:user/by-id id]) 296 | (atom))))))} 297 | {:route [:user/by-id INTEGERS :user/name] 298 | :set (fn [pathmap _] 299 | (doall (for [[id {:keys [user/name]}] (clojure.core/get pathmap :user/by-id)] 300 | (do (swap! users assoc id name) 301 | (core/path-value [:user/by-id id :user/name] 302 | name)))))}])] 303 | [users router]))] 304 | (testing "can set path without a ref" 305 | (let [[users router] (users-router) 306 | result (set router [{:user/by-id {1 {:user/name "Huey"}}}])] 307 | (is (= "Huey" 308 | (clojure.core/get @users 1))) 309 | (is (= {:user/by-id {1 {:user/name (atom "Huey")}}} 310 | (:graph result))))) 311 | (testing "can set path with a ref" 312 | (let [[users router] (users-router) 313 | result (set router [{:users {0 {:user/name "Huey" 314 | :user/age 13}}}])] 315 | (is (= "Huey" 316 | (clojure.core/get @users 1))) 317 | (is (= {:users {0 (ref [:user/by-id 1])} 318 | :user/by-id {1 {:user/name (atom "Huey")}}} 319 | (:graph result))))))) 320 | 321 | (deftest router-call-test 322 | (let [users-router (fn [] 323 | (let [users (clojure.core/atom {1 {:user/name "Erik"} 324 | 2 {:user/name "Jack"}}) 325 | router (router [{:route [:users RANGES] 326 | :get (fn [[_ ranges] _] 327 | (for [idx (indices ranges)] 328 | (core/path-value [:users idx] 329 | (ref [:user/by-id 330 | (clojure.core/get (into [] (keys @users)) idx)]))))} 331 | {:route [:users :add] 332 | :call (fn [_ {:keys [user/name]} _] 333 | (let [user-id 7 334 | count (count @users)] 335 | (swap! users assoc user-id {:user/name name}) 336 | [{:users {count (ref [:user/by-id user-id])}}]))} 337 | {:route [:users :length] 338 | :get (fn [_ _] 339 | [(core/path-value [:users :length] 340 | (count @users))])} 341 | {:route [:user/by-id INTEGERS :user/name] 342 | :get (fn [[_ ids] _] 343 | (for [id ids] 344 | (core/path-value [:user/by-id id :user/name] 345 | (get-in @users [id :user/name]))))} 346 | {:route [:user/by-id INTEGERS :user/friend] 347 | :get (fn [[_ ids] _] 348 | (for [id ids 349 | :let [friend-id (get-in @users [id :user/friend])] 350 | :when friend-id] 351 | (core/path-value [:user/by-id id :user/friend] 352 | (ref [:user/by-id friend-id]))))} 353 | {:route [:user/by-id INTEGERS :user/set-name] 354 | :call (fn [[_ [id]] {:keys [user/name]} _] 355 | (swap! users assoc-in [id :user/name] name) 356 | [])} 357 | {:route [:user/by-id INTEGERS :user/set-friend] 358 | :call (fn [[_ [id]] args _] 359 | (let [friend-id (:user/id args)] 360 | (swap! users assoc-in [id :user/friend] friend-id) 361 | [(core/path-value [:user/by-id id :user/friend] 362 | (ref [:user/by-id friend-id]))]))}] 363 | {:call (fn [runner] 364 | (fn [match context] 365 | (runner match context)))})] 366 | [users router]))] 367 | (testing "can call a mutation" 368 | (let [[users router] (users-router) 369 | result (call router [:users :add] {:user/name "Mike"} {:refs [[:user/name]] 370 | :this [[:length]]})] 371 | (is (= #{"Erik" "Jack" "Mike"} 372 | (into #{} (map :user/name (vals @users))))) 373 | (is (= {:users {2 (ref [:user/by-id 7]) 374 | :length (atom 3)} 375 | :user/by-id {7 {:user/name (atom "Mike")}}} 376 | (:graph result))))) 377 | (testing "can call a mutation with optimization" 378 | (let [[users router] (users-router) 379 | result (call router [:users 0 :user/set-name] {:user/name "Huey"} {:this [[:user/name]]})] 380 | (is (= #{"Huey" "Jack"} 381 | (into #{} (map :user/name (vals @users))))) 382 | (is (= {:users {0 (ref [:user/by-id 1])} 383 | :user/by-id {1 {:user/name (atom "Huey")}}} 384 | (:graph result))))) 385 | (testing "can call a mutation with optimization and ref" 386 | (let [[users router] (users-router) 387 | result (call router [:users 0 :user/set-friend] {:user/id 2} {:refs [[:user/name]]})] 388 | (is (= 2 389 | (get-in @users [1 :user/friend]))) 390 | (is (= {:users {0 (ref [:user/by-id 1])} 391 | :user/by-id {1 {:user/friend (ref [:user/by-id 2])} 392 | 2 {:user/name (atom "Jack")}}} 393 | (:graph result))))))) 394 | --------------------------------------------------------------------------------