├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── graph-router-logo.png ├── project.clj ├── src └── graph_router │ ├── core.clj │ ├── dispatch.clj │ ├── graph.clj │ ├── query.clj │ └── type.clj └── test └── graph_router ├── dispatch_test.clj ├── graph_test.clj └── query_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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure -------------------------------------------------------------------------------- /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 | [![Build Status](https://travis-ci.org/LockedOn/graph-router.svg?branch=master)](https://travis-ci.org/LockedOn/graph-router) 2 | 3 | # ![graph-router](https://github.com/lockedon/graph-router/blob/master/graph-router-logo.png) 4 | 5 | A Clojure library for declarative data fetching in derived graphs! 6 | 7 | Graph-router *aims* to provide [GraphQL](https://github.com/facebook/graphql) like query power with the elegance of 8 | the [Datomic Pull API](http://docs.datomic.com/pull.html) whilst sitting on top 9 | of a [Falcor](https://github.com/Netflix/falcor) like Router. 10 | 11 | Graph-router leverages the fact that Clojure keywords are also functions for 12 | accessing data in Clojure hash maps. This means that Graph-router will work with 13 | any data type that allows keywords to access values, this include Datomic entities. 14 | 15 | ## Installation 16 | 17 | [![Clojars Project](http://clojars.org/lockedon/graph-router/latest-version.svg)](http://clojars.org/lockedon/graph-router) 18 | 19 | Graph-router is available on clojars. Once you have added the latest version to your project.clj you are ready to go! 20 | 21 | ## Getting Started 22 | 23 | Requiring graph-router.core provides you with two functions `dispatch` and `with`. 24 | 25 | `dispatch` is used to process a query with a graph. 26 | 27 | `with` is a utility funciton to help compose your graphs. 28 | 29 | ```clojure 30 | (ns example.core 31 | (:require [graph-router.core :refer :all])) 32 | ``` 33 | 34 | __NOTE:__ 35 | All future examples assume both `dispatch` and `with` are imported. 36 | 37 | ### Composing Graph Descriptions 38 | 39 | Graphs descriptions are data structures defining the shape of data in Clojure hash maps available for querying. 40 | 41 | ```clojure 42 | (def data {:Hello "Hello" :World "World"}) 43 | 44 | (def data-keys [:Hello :World]) 45 | ``` 46 | 47 | In the example above two bindings are defined: 48 | 49 | `data` is a Clojure data structure accessable via Clojure keywords. 50 | 51 | `data-keys` is a description of how to consume the values in `data`. 52 | 53 | #### `with` 54 | 55 | Graph-router requires at the top level a hash-map of data generators. 56 | 57 | `with` is mainly used to define what function to use in place of a keyword in accessing data. 58 | 59 | ```clojure 60 | (defn generate-data 61 | [_] 62 | {:Hello "Hello" :World "World"}) 63 | 64 | (def graph {(with :Root generate-data) [:Hello :World]}) 65 | ``` 66 | 67 | In the above example the keyword `:Root` is declared as an alias for `generate-data` that returns the data. 68 | 69 | `generate-data` is called once per dispatch. This is very useful as `generate-data` could be used to return data from an external source like a SQL database, datomic, a third party REST API or any other data source you like. 70 | 71 | __NOTE:__ 72 | When using `with` to swap what function is used to access data, the function used must receive at least one argument. 73 | Just as a keyword receives one argument when being used as a function to access data in a hash map. 74 | 75 | ```clojure 76 | (defn generate-data 77 | [_] 78 | {:Hello "Hello" :World "World"}) 79 | 80 | (defn get-hello 81 | [& args] 82 | (apply :Hello args)) 83 | 84 | ;; This will produce an identical graph to the above example. 85 | (def graph {(with :Root generate-data) [(with :Hello get-hello) :World]}) 86 | ``` 87 | 88 | ### Putting It All Together 89 | 90 | `dispatch` processes a query in relation to a graph. Fortunately the query looks very similar to a graph. 91 | 92 | ```clojure 93 | (defn generate-data 94 | [_] 95 | {:Hello "Hello" :World "World"}) 96 | 97 | (defn say-hi 98 | [e] 99 | (str (:Hello e) " " (:World e) "!")) 100 | 101 | (def graph {(with :Root generate-data) [(with :Hi say-hi)]}) 102 | 103 | (def query '{:Root [:Hi :Hello]}) 104 | 105 | (dispatch graph query) ;; => {:Root {:Hi "Hello World!", :Hello nil}} 106 | ``` 107 | 108 | There is quite a bit going on in the previous example. 109 | 110 | `graph` builds on what we have already seen, exposing only `:Hi` from `:Root`. `:Hi` is an alias for `say-hi` 111 | which in turn uses `generate-data` being passed in as `e`. 112 | 113 | `query` is a quoted form, defining the shape of the data required. 114 | `query` is requesting `:Hi` and `:Hello` from `:Root`. Given only `:Hi` is exposed from `:Root`, the result of `:Hello` is `nil`. 115 | 116 | 117 | ### Passing Arguments 118 | 119 | Arguments can be passed to keywords, anywhere keywords are in the query. 120 | 121 | ```clojure 122 | (defn generate-data 123 | [_] 124 | {:Hello "Hello" :World "World"}) 125 | 126 | (defn speak 127 | [e greeting] 128 | (str greeting " " (:World e) "!")) 129 | 130 | (def graph {(with :Root generate-data) [(with :Say speak)]}) 131 | 132 | (def query '{:Root [(:Say "Hi")]}) 133 | 134 | (dispatch graph query) ;; => {:Root {:Say "Hi World!"}} 135 | ``` 136 | 137 | In `query` we are passing "Hi" to the function that is aliased for `:Say`. 138 | 139 | There is no hard limit to the number of arguments that can be passed. 140 | 141 | ### Collections 142 | 143 | Collections are handled transparently, the graph description and query stay the same, the only thing that changes is the data and result. 144 | 145 | ```clojure 146 | (defn generate-data 147 | [_] 148 | [{:Hello "Hello" :World "World"}]) 149 | 150 | (def graph {(with :Root generate-data) [:Hello :World]}) 151 | 152 | (def query '{:Root [:Hello]}) 153 | 154 | (dispatch graph query) ;; => {:Root [{:Hello "Hello"}]} 155 | ``` 156 | 157 | ### Nested Data 158 | 159 | Graph-router borrows the concept of nesting queries to access nested data. 160 | 161 | ```clojure 162 | (defn generate-data 163 | [_] 164 | {:One {:Two "Deep" :Three "3"}}) 165 | 166 | (def graph {(with :Root generate-data) [{:One [:Two]}]}) 167 | 168 | (def query '{:Root [{:One [:Two]}]}) 169 | 170 | (dispatch graph query) ;; => {:Root {:One {:Two "Deep"}}} 171 | ``` 172 | 173 | In both `graph` and `query` all the same rules are applied to define the nested graph description and query. 174 | 175 | ### Multiple Roots 176 | 177 | The graph description can have multiple roots, however the query can only access one. 178 | 179 | ```clojure 180 | (defn generate-data 181 | [_] 182 | {:Hello "Hello" :World "World"}) 183 | 184 | (defn second-root 185 | [_] 186 | {:Hi "Hi" :Moon "Moon"}) 187 | 188 | (def graph {(with :Root generate-data) [:Hello :World] 189 | (with :Root2 second-root) [:Hi :Moon]}) 190 | 191 | (def query '{:Root2 [:Hi :Moon]}) 192 | 193 | (dispatch graph query) ;; => {:Root2 {:Hi "Hi", :Moon "Moon"}} 194 | ``` 195 | 196 | ### Recursive Graph Descriptions 197 | 198 | Graph-router uses Clojure references to build recusive graph descriptions. 199 | 200 | ```clojure 201 | (defn generate-data 202 | [_] 203 | {:left {:right {:value "Hello"}} :right {:value 2}}) 204 | 205 | (def b-tree [{:left #'b-tree} {:right #'b-tree} :value]) 206 | 207 | (def graph {(with :Root generate-data) b-tree}) 208 | 209 | (def query '{:Root [{:left [{:right [:value]}]}]}) 210 | 211 | (dispatch graph query) ;; => {:Root {:left {:right {:value "Hello"}}}} 212 | ``` 213 | 214 | This also allows you to do mutual recursion. 215 | 216 | ```clojure 217 | (defn generate-data 218 | [_] 219 | {:A {:B {:A {:value "Hello Word!"}}}}) 220 | 221 | (declare b) 222 | 223 | (def a [{:A #'b} :value]) 224 | 225 | (def b [{:B a} :value]) 226 | 227 | (def graph {(with :Root generate-data) a}) 228 | 229 | (def query '{:Root [{:A [{:B [{:A [:value]}]}]}]}) 230 | 231 | (dispatch graph query) ;; => {:Root {:A {:B {:A {:value "Hello Word!"}}}}} 232 | ``` 233 | 234 | Notice how `a` and `b` reference each other. 235 | 236 | ### Weaving Functions 237 | 238 | `with` has two jobs; aliasing functions as described earlier and attaching functions that the result of the function can be weaved through. 239 | 240 | ```clojure 241 | (defn generate-data 242 | [_] 243 | [{:Hello "Hello" :World "World"} {:Hello "Hi" :World "Moon"}]) 244 | 245 | (def graph {(with :Root generate-data {'take take}) [:Hello :World]}) 246 | 247 | (def query '{(->> :Root (take 1)) [:Hello]}) 248 | 249 | (dispatch graph query) ;; => {:Root [{:Hello "Hello"}]} 250 | ``` 251 | 252 | `with` accepts an additional argument of a hash map with the key being a symbol and the value being the function. In the above example we are declaring the symbol `'take` to use the clojure.core function `take`. 253 | 254 | Graph-router borrows the syntax from the Clojure thead last macro `->>`. The weave function has no limit to the number of functions to weave the result through. Each function can take additional arguments as in the example above where `take` is being given the additional argument `1`. 255 | 256 | __NOTE:__ The result is always passed as the last argument to the weave functions. 257 | 258 | ## Road Map 259 | 260 | * Add thread first weave. 261 | * Allow variant graph nodes where attributes are dependant on type. 262 | * Statically analyze the query in relation to the graph to validate that the query is a strict subset of graph. 263 | * Statically analyze the arity of all functions to ensure the correct number of arguments are being passed before the query is processed. 264 | * Better error reporting when query or graph description does not match schema. 265 | 266 | ## License 267 | 268 | Copyright © 2016 LockedOn 269 | 270 | Distributed under the Eclipse Public License either version 1.0 or (at 271 | your option) any later version. 272 | -------------------------------------------------------------------------------- /graph-router-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LockedOn/graph-router/25062b1eeac2dbf3318c585a404f20b5b1a6ab6c/graph-router-logo.png -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject lockedon/graph-router "0.1.7" 2 | :description "A Clojure library for declarative data fetching in derived graphs!" 3 | :url "http://github.com/LockedOn/graph-router" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[prismatic/schema "1.0.4"]] 7 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.8.0"]] 8 | :plugins [[com.jakemccrary/lein-test-refresh "0.10.0"]] 9 | :main graph-router.core}} 10 | :deploy-repositories [["releases" :clojars]]) 11 | -------------------------------------------------------------------------------- /src/graph_router/core.clj: -------------------------------------------------------------------------------- 1 | (ns graph-router.core 2 | (:require [graph-router.dispatch :as dispatch] 3 | [graph-router.type :refer :all] 4 | [graph-router.graph :as gr]) 5 | (:gen-class)) 6 | 7 | (def dispatch dispatch/dispatch) 8 | 9 | (defn- process-with 10 | "Expand `with` form." 11 | [k f m] 12 | (let [k' (if (nil? f) 13 | k 14 | [Attribute k f])] 15 | (if (nil? m) 16 | k' 17 | [Weave k' m]))) 18 | 19 | (defmacro named-fn 20 | "Convert (fn []) functions into a (defn symbol []) to allow reflection on arity count. 21 | Don't pass through #() funcitons." 22 | [f] 23 | (let [symbol-fn (and (symbol? f) (ifn? f)) 24 | list-fn (and (list? f) (= 'fn (first f)))] 25 | (or (symbol? symbol-fn) 26 | (if list-fn 27 | (list 'defn (symbol (str (java.util.UUID/randomUUID))) (rest f)) 28 | f)))) 29 | 30 | (defn- arg-length 31 | "Takes a vecor of args and returns minimum required arity." 32 | [args] 33 | (let [amp-index (first (keep-indexed #(if (= %2 '&) %1) args)) 34 | args' (if amp-index 35 | (subvec args 0 amp-index) 36 | args)] 37 | [(if amp-index >= =) (count args')])) 38 | 39 | (defn- correct-args 40 | "Check if arglists is the correct arity." 41 | [arglists arity] 42 | (->> arglists 43 | (map arg-length) 44 | (reduce (fn [ok [pred len]] 45 | (or ok (pred arity len))) false))) 46 | 47 | (defmacro with 48 | "Alias a keyword with another function and or register weave functions. 49 | See https://github.com/LockedOn/graph-router for more information." 50 | [k f & [m & _]] 51 | (let [f' (if (map? f) m f) 52 | m' (if (map? f) f m) 53 | f# (macroexpand `(named-fn ~f'))] 54 | (process-with k f# m'))) 55 | -------------------------------------------------------------------------------- /src/graph_router/dispatch.clj: -------------------------------------------------------------------------------- 1 | (ns graph-router.dispatch 2 | (:require [graph-router.type :refer :all] 3 | [graph-router.graph :as gr] 4 | [graph-router.query :as qu])) 5 | 6 | (declare step) 7 | 8 | (defn- context-attribute 9 | [attrib] 10 | (if (= Attribute (:type attrib)) 11 | attrib 12 | (let [{value :value} attrib 13 | {:keys [type] :or {:type nil}} value] 14 | (cond (= Attribute type) 15 | value 16 | 17 | (= Weave type) 18 | (recur value))))) 19 | 20 | (defn- get-context 21 | [context-list attrib-key] 22 | (first (filter (fn [context] 23 | (= attrib-key (:value (context-attribute context)))) 24 | (:value context-list)))) 25 | 26 | (defn- same-attribute? 27 | [{av :value at :type} {bv :value bt :type}] 28 | (and (= Attribute at bt) 29 | (= av bv))) 30 | 31 | (defn- same-type? 32 | [a b] 33 | (let [ca (context-attribute a) 34 | cb (context-attribute b)] 35 | (if (and (some? ca) 36 | (some? cb)) 37 | (same-attribute? ca cb)))) 38 | 39 | (defn- find-match 40 | [attrib-list needle] 41 | (->> attrib-list 42 | (reduce 43 | (fn [ret attrib] 44 | (cond ret 45 | ret 46 | 47 | (= ContextList (:type attrib)) 48 | (let [match (find-match (:value attrib) needle)] 49 | (if match 50 | match 51 | ret)) 52 | 53 | (same-type? attrib needle) 54 | attrib 55 | 56 | :else 57 | ret)) 58 | nil))) 59 | 60 | (defn- find-function 61 | [function-list sym] 62 | (->> function-list 63 | (filter (fn [f] 64 | (= (:value f) (:value sym)))) 65 | (first) 66 | :fn)) 67 | 68 | (defn- process-attribs-fn [g-list q-list] 69 | (fn [e] 70 | (->> q-list 71 | (map (fn [attrib] 72 | (let [k (:value (context-attribute attrib)) 73 | match (find-match g-list attrib) 74 | result (if match 75 | (step match attrib e))] 76 | {k result}))) 77 | (reduce merge)))) 78 | 79 | (defmulti ^:private step 80 | (fn [graph _ _] 81 | (:type graph))) 82 | 83 | (defmethod step Weave 84 | [graph query entities] 85 | (let [fns (:functions graph)] 86 | (reduce 87 | (fn [res f] 88 | (let [args (concat (:args f) [res])] 89 | (apply (find-function fns f) args))) 90 | entities (:functions query)))) 91 | 92 | (defmethod step Attribute 93 | [graph query entity] 94 | (let [f (:fn graph) 95 | args (:args query)] 96 | (apply f (cons entity args)))) 97 | 98 | (defmethod step Recursive 99 | [graph query entity] 100 | (gr/parse (deref (:value graph)) gr/attribute-list-schema)) 101 | 102 | (defmethod step Context 103 | [graph query entity] 104 | (let [es (step (context-attribute graph) 105 | (context-attribute query) 106 | entity) 107 | g-attrib (:attributes graph) 108 | g-list (if (= Recursive (:type g-attrib)) 109 | (step g-attrib query entity) 110 | g-attrib) 111 | process (process-attribs-fn g-list (:attributes query)) 112 | gvalue (:value graph) 113 | qvalue (:value query) 114 | ; will be able to change to follow the query only once analyzer is written 115 | es (if (= Weave (:type gvalue) (:type qvalue)) 116 | (step gvalue qvalue es) 117 | es)] 118 | (if ((some-fn sequential? set?) es) 119 | (vec (map process es)) 120 | (process es)))) 121 | 122 | (defmethod step ContextList 123 | [graph query entity] 124 | (let [attrib-key (:value (context-attribute query))] 125 | {attrib-key (step (get-context graph attrib-key) query entity)})) 126 | 127 | (defn dispatch 128 | "Process a graph description with a query. 129 | See https://github.com/LockedOn/graph-router for more information." 130 | [graph query & [entity]] 131 | (step (gr/parse graph gr/context-list-schema) 132 | (qu/parse query qu/context-schema) 133 | entity)) 134 | -------------------------------------------------------------------------------- /src/graph_router/graph.clj: -------------------------------------------------------------------------------- 1 | (ns graph-router.graph 2 | (:require [schema.core :as s] 3 | [graph-router.type :refer :all])) 4 | 5 | (declare parse 6 | parse-map 7 | parse-vector 8 | parse-keyword 9 | parse-function 10 | attribute-list-schema 11 | attribute-schema 12 | weave-schema 13 | context-schema 14 | context-list-schema 15 | function-schema 16 | recursive-schema 17 | graph-schema) 18 | 19 | (defn f? [x] 20 | (and (ifn? x) (not (map? x)))) 21 | 22 | (def recursive-schema 23 | {:type (s/eq Recursive) 24 | :value (s/pred var? "var?")}) 25 | 26 | (def attribute-schema 27 | {:type (s/eq Attribute) 28 | :value s/Keyword 29 | :fn (s/pred f? "ifn?")}) 30 | 31 | (def function-schema 32 | {:type (s/eq Function) 33 | :value s/Symbol 34 | :fn (s/pred f? "ifn?")}) 35 | 36 | (def weave-schema 37 | {:type (s/eq Weave) 38 | :value attribute-schema 39 | :functions (s/conditional seq 40 | [function-schema])}) 41 | 42 | (def attribute-list-schema 43 | (s/if #(= (:type %) Recursive) 44 | recursive-schema 45 | (s/conditional seq 46 | [(s/if #(= (:type %) Attribute) 47 | attribute-schema 48 | (s/recursive #'context-list-schema))]))) 49 | 50 | (def context-schema 51 | {:type (s/eq Context) 52 | :value (s/if #(= (:type %) Weave) 53 | weave-schema 54 | attribute-schema) 55 | :attributes attribute-list-schema}) 56 | 57 | (def context-list-schema 58 | {:type (s/eq ContextList) 59 | :value (s/conditional seq 60 | [context-schema])}) 61 | 62 | (def graph-schema 63 | (s/conditional #(= (:type %) ContextList) 64 | context-list-schema 65 | 66 | #(= (:type %) Context) 67 | context-schema 68 | 69 | #(= (:type %) Weave) 70 | weave-schema 71 | 72 | :else 73 | attribute-list-schema)) 74 | 75 | (defn- parse-vector 76 | [form] 77 | (let [[t k f] form] 78 | (cond (= Attribute t) 79 | (->Attrib Attribute k f) 80 | 81 | (= Weave t) 82 | (->Wea Weave (parse k) (map parse-function f)) 83 | 84 | :else 85 | (map parse form)))) 86 | 87 | (defn- parse-keyword 88 | [form] 89 | (->Attrib Attribute form form)) 90 | 91 | (defn- parse-function 92 | [form] 93 | (let [[s f] form] 94 | (->Attrib Function s f))) 95 | 96 | (defn- parse-map 97 | [form] 98 | (->ContList ContextList 99 | (map (fn [k] 100 | (->Cont Context (parse k) (parse (get form k)))) 101 | (keys form)))) 102 | 103 | (def validate 104 | (memoize 105 | (fn [schema-validator form] 106 | (schema-validator (parse form))))) 107 | 108 | (def validator 109 | (memoize 110 | #(s/validator %))) 111 | 112 | (defn parse 113 | ([form schema] 114 | (validate (validator schema) form)) 115 | ([form] 116 | (cond (vector? form) 117 | (parse-vector form) 118 | 119 | (map? form) 120 | (parse-map form) 121 | 122 | (keyword? form) 123 | (parse-keyword form) 124 | 125 | (var? form) 126 | (->ContList Recursive form)))) 127 | -------------------------------------------------------------------------------- /src/graph_router/query.clj: -------------------------------------------------------------------------------- 1 | (ns graph-router.query 2 | (:require [schema.core :as s] 3 | [graph-router.type :refer :all])) 4 | 5 | (declare parse-weave 6 | parse-function 7 | parse-keyword 8 | parse-map 9 | parse 10 | attribute-schema 11 | function-schema 12 | weave-schema 13 | attribute-list-schema 14 | context-schema 15 | query-schema) 16 | 17 | (def attribute-schema 18 | {:type (s/eq Attribute) 19 | :value s/Keyword 20 | :args [s/Any]}) 21 | 22 | (def function-schema 23 | {:type (s/eq Function) 24 | :value s/Symbol 25 | :args [s/Any]}) 26 | 27 | (def weave-schema 28 | {:type (s/eq Weave) 29 | :value attribute-schema 30 | :functions (s/conditional seq [function-schema])}) 31 | 32 | (def attribute-list-schema 33 | (s/conditional seq [(s/conditional #(= (:type %) Attribute) 34 | attribute-schema 35 | 36 | #(= (:type %) Weave) 37 | weave-schema 38 | 39 | #(= (:type %) Context) 40 | (s/recursive #'context-schema))])) 41 | 42 | (def context-schema 43 | {:type (s/eq Context) 44 | :value (s/if #(= (:type %) Attribute) 45 | attribute-schema 46 | weave-schema) 47 | :attributes attribute-list-schema}) 48 | 49 | (def query-schema 50 | (s/conditional #(= (:type %) Attribute) 51 | attribute-schema 52 | 53 | #(= (:type %) Function) 54 | function-schema 55 | 56 | #(= (:type %) Weave) 57 | weave-schema 58 | 59 | #(= (:type %) Context) 60 | context-schema 61 | 62 | :else 63 | attribute-list-schema)) 64 | 65 | (defn- parse-weave 66 | [fun] 67 | (let [[value & args] (:args fun)] 68 | (->Wea Weave (parse value) (map parse args)))) 69 | 70 | (defn- parse-list 71 | [form] 72 | (let [value (first form) 73 | fun (->Func Function value (rest form))] 74 | 75 | (cond (= '->> value) 76 | (parse-weave fun) 77 | 78 | (keyword? value) 79 | (assoc fun :type Attribute) 80 | 81 | :else 82 | fun))) 83 | 84 | (defn- parse-keyword 85 | [form] 86 | (->Func Attribute form '())) 87 | 88 | (defn- parse-map 89 | [form] 90 | (let [[context & _] (keys form)] 91 | (->Cont Context (parse context) (parse (get form context))))) 92 | 93 | (def validate 94 | (memoize 95 | (fn [schema-validator form] 96 | (schema-validator (parse form))))) 97 | 98 | (def validator 99 | (memoize 100 | #(s/validator %))) 101 | 102 | (defn parse 103 | ([form schema] 104 | (validate (validator schema) form)) 105 | ([form] 106 | (cond (list? form) 107 | (parse-list form) 108 | 109 | (keyword? form) 110 | (parse-keyword form) 111 | 112 | (vector? form) 113 | (map parse form) 114 | 115 | (map? form) 116 | (parse-map form)))) 117 | -------------------------------------------------------------------------------- /src/graph_router/type.clj: -------------------------------------------------------------------------------- 1 | (ns graph-router.type) 2 | 3 | (def Attribute ::Attribute) 4 | 5 | (def Context ::Context) 6 | 7 | (def ContextList ::ContextList) 8 | 9 | (def Function ::Function) 10 | 11 | (def Weave ::Weave) 12 | 13 | (def Recursive ::Recursive) 14 | 15 | (defrecord Attrib [type value fn]) 16 | (defrecord Func [type value args]) 17 | (defrecord ContList [type value]) 18 | (defrecord Cont [type value attributes]) 19 | (defrecord Wea [type value functions]) -------------------------------------------------------------------------------- /test/graph_router/dispatch_test.clj: -------------------------------------------------------------------------------- 1 | (ns graph-router.dispatch-test 2 | (:require [clojure.test :refer :all] 3 | [graph-router.core :refer :all])) 4 | 5 | (deftest ok-tests 6 | (testing "Full Graph Query Dispatch" 7 | (is (= {:Root {:name "Hello"}} (dispatch {:Root [:name]} 8 | '{:Root [:name]} 9 | {:Root {:name "Hello"}})))) 10 | 11 | (testing "Full Graph Query Dispatch - generating root" 12 | (is (= {:Root {:name "Hello"}} (dispatch {(with :Root (fn [& _] 13 | {:name "Hello"})) [:name]} 14 | '{:Root [:name]})))) 15 | 16 | (testing "Full Graph Query Dispatch - Pass Args" 17 | (is (= {:Root {:name "World"}} (dispatch {(with :Root (fn [_ s] 18 | {:name s})) [:name]} 19 | '{(:Root "World") [:name]}))) 20 | 21 | (is (= {:Root {:name "World"}} (dispatch {(with :Root (fn [& _] nil)) [(with :name (fn [_ s] s))]} 22 | '{:Root [(:name "World")]})))) 23 | 24 | (testing "Full Graph Query Dispatch - collection" 25 | (is (= {:Root [{:name "Hello"}]} (dispatch {:Root [:name]} 26 | '{:Root [:name]} 27 | {:Root [{:name "Hello"}]})))) 28 | 29 | (testing "Part Graph Query Dispatch - weave" 30 | (is (= {:Root [{:name "World"}]} (dispatch {(with :Root (fn [& _] 31 | [{:name "Hello"} {:name "World"}]) 32 | {'taker take 'droper drop}) [:name]} 33 | '{(->> :Root (droper 1) (taker 1)) [:name]})))) 34 | 35 | (testing "Part Graph Query Dispatch" 36 | (is (= {:Root {:name "Hello"}} (dispatch {:Root [:name :sound]} 37 | '{:Root [:name]} 38 | {:Root {:name "Hello" :sound "Bark"}}))))) 39 | 40 | 41 | (deftest failing-tests 42 | (testing "Invalid Query" 43 | (is (thrown? Exception (dispatch {:Root [:name]} 44 | '{Root [:name]} 45 | {:Root {:name "Hello"}})))) 46 | (testing "Invalid Graph" 47 | (is (thrown? Exception (dispatch {"hello" [:name]} 48 | '{:Root [:name]} 49 | {:Root {:name "Hello"}}))))) -------------------------------------------------------------------------------- /test/graph_router/graph_test.clj: -------------------------------------------------------------------------------- 1 | (ns graph-router.graph-test 2 | (:require [clojure.test :refer :all] 3 | [graph-router.graph :refer :all] 4 | [graph-router.core :refer :all])) 5 | 6 | (deftest ok-tests 7 | (testing "Basic Graph" 8 | (is (parse {:Root [:name]} graph-schema))) 9 | 10 | (testing "Multi Roots Graph" 11 | (is (parse {:Root [:name] :Root2 [:attrib]} graph-schema))) 12 | 13 | (testing "With Function Swap" 14 | (is (parse {(with :Root (fn [])) [:name]} graph-schema))) 15 | 16 | (testing "With Weave Functions" 17 | (is (parse {(with :Root {'drop drop}) [:name]} graph-schema))) 18 | 19 | (testing "With Function Swap and Weave Functions" 20 | (is (parse {(with :Root (fn []) {'drop drop}) [:name]} graph-schema))) 21 | 22 | (testing "With Function Swap in the Attribute List" 23 | (is (parse {:Root [(with :name (fn []))]} graph-schema))) 24 | 25 | (testing "With Function Swap and Weave Functions in wrong order" 26 | (is (parse {(with :Root {'drop drop} (fn [])) [:name]} graph-schema))) 27 | 28 | (testing "Nested Graph" 29 | (is (parse {:Root [:name {:sub-graph [:sub/name]}]} graph-schema))) 30 | 31 | (testing "Recursive Attributes" 32 | (def recusive-attribs [:wow {:nested #'recusive-attribs}]) 33 | (is (parse {:Root recusive-attribs} graph-schema)))) 34 | 35 | 36 | (deftest failing-tests 37 | (testing "Empty Attributes" 38 | (is (thrown? Exception (parse {:Root []} graph-schema)))) 39 | 40 | (testing "Root not a keyword" 41 | (is (thrown? Exception (parse {'Root [:node]} graph-schema))) 42 | (is (thrown? Exception (parse {"wok" [:node]} graph-schema))) 43 | (is (thrown? Exception (parse {(fn []) [:node]} graph-schema))) 44 | (is (thrown? Exception (parse {1 [:node]} graph-schema)))) 45 | 46 | (testing "Attribute not a keyword" 47 | (is (thrown? Exception (parse {:Root ['Root]} graph-schema))) 48 | (is (thrown? Exception (parse {:Root ["wok"]} graph-schema))) 49 | (is (thrown? Exception (parse {:Root [(fn [])]} graph-schema))) 50 | (is (thrown? Exception (parse {:Root [1]} graph-schema)))) 51 | 52 | (testing "With Weave Functions in the Attribute List" 53 | (is (thrown? Exception (parse {:Root [(with :node {'drop drop})]} graph-schema))))) -------------------------------------------------------------------------------- /test/graph_router/query_test.clj: -------------------------------------------------------------------------------- 1 | (ns graph-router.query-test 2 | (:require [clojure.test :refer :all] 3 | [graph-router.query :refer :all])) 4 | 5 | (deftest ok-tests 6 | (testing "Basic Query" 7 | (is (parse '{(:Artist 17592186047077) [:artist/name]} query-schema))) 8 | 9 | (testing "Basic Query - No Args on Context" 10 | (is (parse '{:Artist [:artist/name]} query-schema))) 11 | 12 | (testing "Basic Query - Args on Attribute" 13 | (is (parse '{:Artist [(:artist/name 999)]} query-schema))) 14 | 15 | (testing "Complex Query - got everything!" 16 | (is (parse '{(->> (:Artists 123) (drop 10) (take 10)) 17 | [(->> :artist/name (d)) (:artist/format "yyyy-MM-dd") {:artist/country 18 | [:country/code]}]} query-schema)))) 19 | 20 | (deftest failing-tests 21 | (testing "Empty Args List" 22 | (is (thrown? Exception (parse '[] attribute-schema)))) 23 | 24 | (testing "Symbol in Arg List" 25 | (is (thrown? Exception (parse '[symbol] attribute-schema)))) 26 | 27 | (testing "Function in Arg List" 28 | (is (thrown? Exception (parse [take] attribute-schema)))) 29 | 30 | (testing "nil is not an arglist" 31 | (is (thrown? Exception (parse nil attribute-schema)))) 32 | 33 | (testing "map is not an arglist" 34 | (is (thrown? Exception (parse '{:ok world} attribute-schema)))) 35 | 36 | (testing "Weave missing functions" 37 | (is (thrown? Exception (parse '(->> :Hello) weave-schema)))) 38 | 39 | (testing "Weave Invalid functions" 40 | (is (thrown? Exception (parse '(->> :Hello (:wow 1)) weave-schema)))) 41 | 42 | (testing "Weave missing Attribute" 43 | (is (thrown? Exception (parse '(->> ) weave-schema))))) --------------------------------------------------------------------------------