├── boot.properties ├── src └── workflo │ └── brahman │ ├── authnz.clj │ ├── backends │ ├── datomic.clj │ └── datascript.cljc │ ├── courier.cljc │ └── model.cljc ├── test └── workflo │ └── brahman │ └── test │ ├── setup.clj │ ├── model.clj │ ├── examples.clj │ └── backends │ └── datascript.clj ├── README.md └── LICENSE.txt /boot.properties: -------------------------------------------------------------------------------- 1 | BOOT_CLOJURE_VERSION=1.8.0 2 | BOOT_VERSION=2.5.5 3 | BOOT_EMIT_TARGET=no 4 | -------------------------------------------------------------------------------- /src/workflo/brahman/authnz.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.authnz 2 | (:refer-clojure :exclude [set?])) 3 | 4 | (defn set? [x] 5 | (not (nil? x))) 6 | 7 | (defn equals? [x y] 8 | (= x y)) 9 | -------------------------------------------------------------------------------- /test/workflo/brahman/test/setup.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.test.setup 2 | (:import [datomic.peer Connection]) 3 | (:require [clojure.test :refer [deftest is]] 4 | [datomic.api :as d])) 5 | 6 | (def db-uri "datomic:mem://brahman-test") 7 | 8 | (defn create-db [] 9 | (d/create-database db-uri)) 10 | 11 | (defn delete-db [] 12 | (d/delete-database db-uri)) 13 | 14 | (defn connect-to-db [] 15 | (d/connect db-uri)) 16 | 17 | (defmacro with-conn 18 | [& body] 19 | `(let [~(symbol "conn") (connect-to-db)] 20 | (do 21 | ~@body))) 22 | -------------------------------------------------------------------------------- /src/workflo/brahman/backends/datomic.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.backends.datomic 2 | (:require [datomic.api :as d] 3 | [datomic-schema.schema :as s] 4 | [workflo.brahman.backends.datascript :as bds] 5 | [workflo.brahman.model :as bm])) 6 | 7 | ;;;; Schema attribute extraction 8 | 9 | (def model->attrs bds/model->attrs) 10 | 11 | ;;;; Schema generation 12 | 13 | (defn datomic-schema [[model attrs]] 14 | (let [fields (bds/attrs->fields attrs)] 15 | (eval `(s/schema ~(bm/model-name model) 16 | (s/fields ~@fields))))) 17 | 18 | ;;;; Schema installation 19 | 20 | (defn install-schemas 21 | [conn schemas] 22 | (let [dschemas (mapv datomic-schema schemas)] 23 | (->> (concat (s/generate-parts [(s/part "app")]) 24 | (s/generate-schema dschemas)) 25 | (d/transact conn) 26 | (deref)))) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introducing Brahman 2 | 3 | _A universe of backends for Om Next & other CQRS systems._ 4 | 5 | ## What is Brahman and what can it do for me? 6 | 7 | Brahman aims to solve the problem of connecting client-driven CQRS 8 | systems like Om Next with server side databases, event sourcing and 9 | business logic. It offers a mostly declarative way of writing 10 | colocated specifications for data models, data validation, commands, 11 | command validation and command authorization, and a simple interface 12 | to query/process these models and commands. 13 | 14 | ### Motivation 15 | 16 | With Om Next, most of the client side complexity of querying and 17 | mutating data goes away. However, there is no equivalent story for an 18 | equally tidy, declarative, colocated, extensible way of handling 19 | queries and mutations on the server side. Brahman aims to provide one. 20 | 21 | We realized that there are all these nice libraries and tools out there 22 | for writing backends - Sente, Buddy / Friend, Datomic, Bouncer / 23 | Prismatic Schema, Onyx, Kafka to name just a few - but integrating these 24 | with Om Next queries and mutations is not straight forward enough. 25 | 26 | Also, even though Datomic performs data-level validation and Buddy / 27 | Friend provide general authnz functionality, our feeling was that almost 28 | everyone will be looking for a way to perform validation that yields 29 | human-readable errors and authorization that operates on commands 30 | rather than e.g. HTTP requests, typically from within an Om Next server 31 | side parser or similar. 32 | 33 | Which leads us to Brahman's features: 34 | 35 | ### Features 36 | 37 | * Models 38 | - Define models as materialized views on top of _any_ number 39 | of databases 40 | - Fetch and query model data using _any_ query language 41 | - Specify model version (in any format), schemas (in any format), 42 | validation rules (in any format) and data stores (of any kind) 43 | all in one place (colocation), in a declarative way, as data 44 | - Extend data with attributes derived from any of the databases 45 | on the fly 46 | * Commands 47 | - Define commands in the system, including their versions, validation 48 | and authorization rules, in one place (colocation), declaratively, 49 | as data 50 | - Validate commands and command parameters 51 | * Using model schemas and validations 52 | * Using database queries 53 | * Using arbitrary functions / predicates 54 | - Authorize commands 55 | * Using the command environment (can include arbitrary information 56 | about client, authenticated user, database etc.) 57 | * Using database queries 58 | * Using arbitrary functions / predicates 59 | * Producing validation and authorization errors that are readable 60 | for humans and machines at the same time 61 | - Verify that all commands used in the system are validated 62 | and authorized 63 | 64 | ## How does it work? 65 | 66 | TODO 67 | 68 | ## License 69 | 70 | Brahman is (C) 2016 Workflo, Inc. & Jannis Pohlmann. 71 | 72 | It is licensed under GNU LGPL 2.1. For more details see LICENSE.txt. 73 | -------------------------------------------------------------------------------- /src/workflo/brahman/courier.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.courier 2 | #?(:clj (:import [clojure.lang ExceptionInfo])) 3 | (:refer-clojure :exclude [deliver]) 4 | (:require [workflo.brahman.model :as bm] 5 | #?(:cljs [cljs.core :refer [ExceptionInfo]]))) 6 | 7 | ;;;; Courier protocol 8 | 9 | (defprotocol ICourier 10 | (dispatch [this cmd] [this cmd env]) 11 | (get-model [this model-name])) 12 | 13 | ;;;; Command validation 14 | 15 | (defmulti validate-rule (fn [_ _ _ _ rule] (:type rule))) 16 | 17 | (defmethod validate-rule :model 18 | [courier cmd spec env {:keys [select model]}] 19 | (let [params (second cmd) 20 | selector (cond->> select (not (vector? select)) (vector)) 21 | param (get-in params selector) 22 | model (get-model courier model) 23 | errors (bm/validate model param)] 24 | (when errors 25 | (throw 26 | (ex-info (str "Command '" (first cmd) "' is invalid") 27 | {:cmd cmd 28 | :validation-errors errors}))))) 29 | 30 | (defmethod validate-rule :default 31 | [courier cmd spec env rule] 32 | (throw 33 | (ex-info "Validation rule '" rule "' unknown"))) 34 | 35 | (defn validate [courier cmd spec env] 36 | (let [rules (:validations spec)] 37 | (every? (fn [rule] 38 | (validate-rule courier cmd spec env rule)) 39 | rules))) 40 | 41 | ;;;; Command authorization 42 | 43 | (defmulti authorize-rule* (fn [_ _ _ _ rule] (:type rule))) 44 | 45 | (defmethod authorize-rule* :env 46 | [courier cmd spec env rule] 47 | {:pre [(contains? rule :select-env) 48 | (contains? rule :test)]} 49 | (let [env-select (:select-env rule) 50 | env-selector (cond-> env-select 51 | (not (vector? env-select)) vector) 52 | env-value (get-in env env-selector) 53 | test-fn (:test rule)] 54 | (if (cond 55 | (contains? rule :test-param) 56 | (let [params (second cmd) 57 | param-select (:test-param rule) 58 | param-selector (cond-> param-select 59 | (not (vector? param-select)) vector) 60 | param-value (get-in params param-selector)] 61 | (test-fn env-value param-value)) 62 | 63 | (contains? rule :test-value) 64 | (test-fn env-value (:test-value rule)) 65 | 66 | :else 67 | (test-fn env-value)) 68 | true 69 | (throw 70 | (ex-info (str "Command '" (first cmd) "' failed to authorize") 71 | {:cmd cmd}))))) 72 | 73 | (declare authorize-rule) 74 | 75 | (defn authorize-or-rules 76 | [courier cmd spec env rules] 77 | (letfn [(authorize-or-rule [res rule] 78 | (try 79 | (do 80 | (authorize-rule courier cmd spec env rule) 81 | (assoc res :one-ok? true)) 82 | (catch ExceptionInfo e 83 | (update res :errors conj e))))] 84 | (let [res (reduce authorize-or-rule 85 | {:errors [] :one-ok? false} 86 | rules)] 87 | (if (:one-ok? res) 88 | true 89 | (throw (first (:errors res))))))) 90 | 91 | (defn authorize-rule 92 | [courier cmd spec env rule] 93 | (if (set? rule) 94 | (authorize-or-rules courier cmd spec env rule) 95 | (authorize-rule* courier cmd spec env rule))) 96 | 97 | (defn authorize [courier cmd spec env] 98 | (let [rules (:authorizations spec)] 99 | (every? #(authorize-rule courier cmd spec env %) rules))) 100 | 101 | ;;;; Command delivery 102 | 103 | (defn deliver [courier cmd spec env] 104 | (let [config (:config courier)] 105 | ((:deliver config) courier cmd env))) 106 | 107 | ;;;; Courier implementation 108 | 109 | (defrecord Courier [config] 110 | ICourier 111 | (dispatch [this cmd] 112 | (dispatch this cmd {})) 113 | 114 | (dispatch [this cmd env] 115 | {:pre [(list? cmd) 116 | (symbol? (first cmd)) 117 | (or (= 1 (count cmd)) 118 | (map? (second cmd)))]} 119 | (let [name (first cmd) 120 | spec (get-in config [:commands name])] 121 | (if spec 122 | (do 123 | (validate this cmd spec env) 124 | (authorize this cmd spec env) 125 | (deliver this cmd spec env)) 126 | (throw 127 | (ex-info (str "The command '" name "' is unknown") 128 | {:cmd cmd}))))) 129 | 130 | (get-model [this model] 131 | (let [config (:config this)] 132 | ((:get-model config) this model)))) 133 | 134 | (defn courier 135 | [{:keys [commands 136 | deliver 137 | get-model] 138 | :or {commands [] 139 | deliver #() 140 | get-model #()}}] 141 | (let [config {:commands commands 142 | :deliver deliver 143 | :get-model get-model} 144 | courier' (Courier. config)] 145 | courier')) 146 | -------------------------------------------------------------------------------- /test/workflo/brahman/test/model.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.test.model 2 | (:require [clojure.pprint :refer [pprint]] 3 | [clojure.test.check.clojure-test :refer [defspec]] 4 | [clojure.test :refer [deftest is use-fixtures]] 5 | [clojure.test.check.generators :as gen] 6 | [clojure.test.check.properties :as prop] 7 | [com.rpl.specter :as specter :refer :all] 8 | [workflo.brahman.model :as bm])) 9 | 10 | ;;;; Specter transformations 11 | 12 | (defspec transform-specter-works-on-vectors 13 | (prop/for-all [numbers (gen/vector gen/nat)] 14 | (is (= (mapv inc numbers) 15 | (bm/transform-specter [[ALL] inc] numbers))))) 16 | 17 | (defspec transform-specter-works-on-maps 18 | (prop/for-all [numbers (gen/map gen/string gen/nat)] 19 | (is (= (zipmap (keys numbers) (map dec (vals numbers))) 20 | (bm/transform-specter [[ALL LAST] dec] numbers))))) 21 | 22 | (defspec transform-specter-works-on-scalars 23 | (prop/for-all [val gen/simple-type] 24 | (is (= [:x val] 25 | (bm/transform-specter [[ALL] #(conj [:x] %)] val))))) 26 | 27 | ;;;; Simple properties / IModel interface functions 28 | 29 | (defspec models-satisfy-imodel 30 | (prop/for-all [model-names (gen/vector gen/symbol)] 31 | (let [specs (mapv (fn [schema-name] 32 | {:name schema-name 33 | :schema {}}) 34 | model-names) 35 | modeler (bm/modeler {:models specs}) 36 | models (mapv #(bm/get-model modeler %) model-names)] 37 | (and (is (every? #(not (nil? %)) models)) 38 | (is (every? #(satisfies? bm/IModel %) models)) 39 | (is (= (into #{} model-names) 40 | (into #{} (map bm/model-name) models))) 41 | (is (= (into #{} (map :schema) specs) 42 | (into #{} (map bm/schema) models))))))) 43 | 44 | (defspec models-remember-their-schema 45 | (prop/for-all [model-name gen/symbol 46 | schema (gen/map gen/keyword gen/keyword)] 47 | (let [spec {:name model-name 48 | :schema schema} 49 | modeler (bm/modeler {:models [spec]}) 50 | model (bm/get-model modeler model-name)] 51 | (and (is (not (nil? model))) 52 | (is (= (:schema spec) 53 | (bm/schema model))))))) 54 | 55 | ;;;; Simple data models for regular collections 56 | 57 | (defspec vectors-can-be-used-as-dbs 10 58 | (prop/for-all [values (gen/vector gen/any)] 59 | (let [spec {:name 'item 60 | :stores [:vector]} 61 | modeler (bm/modeler 62 | {:models [spec] 63 | :query-store (fn [{:keys [store]} _ _] 64 | (case store :vector values))}) 65 | items (bm/get-model modeler 'item)] 66 | (is (= (into #{} values) (bm/query items nil)))))) 67 | 68 | (defspec queries-can-be-used-to-select-keys 10 69 | (prop/for-all [values (gen/vector (gen/map gen/keyword 70 | gen/simple-type 71 | {:num-elements 200}) 72 | 10)] 73 | (let [spec {:name 'item 74 | :stores [:vector]} 75 | modeler (bm/modeler 76 | {:models [spec] 77 | :query-store 78 | (fn [{:keys [store]} q _] 79 | (case store 80 | :vector (mapv #(select-keys % q) values)))}) 81 | items (bm/get-model modeler 'item) 82 | common-keys (reduce clojure.set/intersection 83 | (into #{} (keys (first values))) 84 | (map (comp (partial into #{}) keys) 85 | (rest values)))] 86 | (is (= (into #{} (map #(select-keys % common-keys)) values) 87 | (into #{} (bm/query items (into [] common-keys)))))))) 88 | 89 | 90 | (defspec queries-with-derived-attrs-work 10 91 | (prop/for-all [collections (gen/vector-distinct-by :collection/id 92 | (gen/hash-map 93 | :collection/id gen/nat 94 | :collection/values (gen/vector 95 | gen/nat)))] 96 | (let [spec {:name 'collection 97 | :stores [:custom] 98 | :derived-attrs [{:name :derived.sum 99 | :prefixed? true 100 | :store :custom} 101 | {:name :derived/count 102 | :prefixed? false 103 | :store :custom}]} 104 | modeler (bm/modeler 105 | {:models [spec] 106 | :query-store (fn [_ q _] 107 | (mapv #(select-keys % q) 108 | collections)) 109 | :query-derived-attr 110 | (fn [_ {:keys [name]} _ collection] 111 | (let [values (:collection/values collection)] 112 | (case name 113 | :derived.sum (reduce + values) 114 | :derived/count (count values))))}) 115 | model (bm/get-model modeler 'collection)] 116 | (is (= (into #{} 117 | (map (fn [{:keys [collection/values] :as collection}] 118 | (assoc collection 119 | :collection/derived.sum 120 | (reduce + values) 121 | :derived/count 122 | (count values)))) 123 | collections) 124 | (into #{} (bm/query model [:collection/id 125 | :collection/values 126 | :collection/derived.sum 127 | :derived/count]))))))) 128 | -------------------------------------------------------------------------------- /test/workflo/brahman/test/examples.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.test.examples 2 | (:import [clojure.lang ExceptionInfo]) 3 | (:require [bouncer.core :as b] 4 | [bouncer.validators :as v] 5 | [clojure.pprint :refer [pprint]] 6 | [clojure.test :refer [deftest is use-fixtures]] 7 | [com.rpl.specter :as s] 8 | [datomic.api :as d] 9 | [workflo.brahman.authnz :as ba] 10 | [workflo.brahman.courier :as bc] 11 | [workflo.brahman.model :as bm] 12 | [workflo.brahman.backends.datomic :as bd] 13 | [workflo.brahman.test.setup :as setup 14 | :refer [create-db 15 | delete-db 16 | with-conn 17 | connect-to-db]])) 18 | 19 | ;;;; Datomic setup 20 | 21 | (defn datomic-fixture [f] 22 | (delete-db) 23 | (create-db) 24 | (f) 25 | (delete-db)) 26 | 27 | (use-fixtures :each datomic-fixture) 28 | 29 | ;;;; Model configuration 30 | 31 | (defn install-schemas [schemas] 32 | (doseq [[store models] schemas] 33 | (case store 34 | :datomic (bd/install-schemas (connect-to-db) models)))) 35 | 36 | (defn query-store 37 | [{:keys [store] :as env} query extra] 38 | (with-conn 39 | (case store 40 | :datomic (d/q `[:find [(~'pull ~'?user ~query)] 41 | :where [~'?user :user/name]] 42 | (d/db conn))))) 43 | 44 | ;;;; Courier configuration 45 | 46 | (defmulti deliver-command (fn [_ [key _] _] key)) 47 | 48 | (defmethod deliver-command 'user/create 49 | [courier [_ params] env] 50 | (with-conn 51 | (let [tempid (d/tempid :db.part/app)] 52 | @(d/transact conn [(merge {:db/id tempid} 53 | (:user params))])))) 54 | 55 | (defmethod deliver-command 'user/update 56 | [courier [_ params] env] 57 | (with-conn 58 | (let [user (d/q '[:find (pull ?user [:db/id]) . 59 | :in $ ?name 60 | :where [?user :user/name ?name]] 61 | (d/db conn) 62 | (:user/name (:user params)))] 63 | @(d/transact conn [(merge user (:user params))])))) 64 | 65 | ;;;; Tests 66 | 67 | (deftest user-model-and-commands 68 | (let [;; Define a simple user model 69 | user-model {:name :user 70 | :version 1 71 | :schema {:username [:string :indexed] 72 | :email [:string :indexed] 73 | :name [:string] 74 | :role [:enum [:regular :admin]] 75 | :friend {[:ref :many] '...}} 76 | :validation {:user/username [v/required v/string] 77 | :user/email [v/required v/email] 78 | :user/name [v/string]} 79 | :stores [:datomic] 80 | :derived-attrs 81 | [{:name :popular? 82 | :store :datomic 83 | :query '[:find (count ?friend) . 84 | :in $ ?user 85 | :where [?user :user/friend ?friend]] 86 | :transform [[s/ALL] (fn [friends] 87 | (and (not (nil? friends)) 88 | (pos? friends)))]}]} 89 | modeler (bm/modeler {:models [user-model] 90 | :install-schemas install-schemas 91 | :model->attrs bd/model->attrs 92 | :validate (fn [model rules data] 93 | (-> data 94 | (b/validate rules) 95 | (first))) 96 | :entity-id :db/id 97 | :query-store query-store}) 98 | commands {'user/create 99 | {:version 1 100 | :authorizations [] 101 | :validations [;; Validate using the user model 102 | {:type :model 103 | :select :user 104 | :model :user}]} 105 | 'user/update 106 | {:version 1 107 | :validations [;; Validate using the user model 108 | {:type :model 109 | :select :user 110 | :model :user}] 111 | :authorizations [;; Is the user executing the command 112 | ;; authenticated? 113 | {:type :env 114 | :select-env :auth-user 115 | :test ba/set?} 116 | ;; Is the user executing the command 117 | ;; either an admin or the same as the 118 | ;; user being modified? 119 | #{{:type :env 120 | :select-env [:auth-user 121 | :user/username] 122 | :test ba/equals? 123 | :test-param [:user 124 | :user/username]} 125 | {:type :env 126 | :select-env [:auth-user 127 | :user/role] 128 | :test ba/equals? 129 | :test-value :admin}}]}} 130 | courier (bc/courier {:commands commands 131 | :deliver deliver-command 132 | :get-model (fn [courier model] 133 | (bm/get-model modeler 134 | model))})] 135 | ;; Assert there are no users in the beginning 136 | (let [users (bm/query (bm/get-model modeler :user) '[*])] 137 | (is (and (set? users) (empty? users)))) 138 | 139 | ;; Dispatch an invalid create command with no user 140 | (let [cmd '(user/create)] 141 | (is (thrown? ExceptionInfo (bc/dispatch courier cmd)))) 142 | 143 | ;; Dispatch an invalid create command with an incomplete user 144 | (let [cmd '(user/create {:user {:user/name "John Doe"}})] 145 | (is (thrown? ExceptionInfo (bc/dispatch courier cmd)))) 146 | 147 | ;; Dispatch a valid create command 148 | (let [cmd '(user/create {:user {:user/name "Jeff Doe" 149 | :user/email "jeff@doe.name" 150 | :user/username "jeff"}})] 151 | (is (bc/dispatch courier cmd))) 152 | 153 | ;; Assert there is a single user "jeff" now 154 | (let [users (bm/query (bm/get-model modeler :user) '[*])] 155 | (and (is (set? users)) 156 | (is (= 1 (count users))) 157 | (is (= {:user/name "Jeff Doe" 158 | :user/email "jeff@doe.name" 159 | :user/username "jeff"} 160 | (select-keys (first users) [:user/name 161 | :user/email 162 | :user/username]))))) 163 | 164 | ;; Dispatch an invalid update command with no user 165 | (let [cmd '(user/update)] 166 | (is (thrown? ExceptionInfo (bc/dispatch courier cmd)))) 167 | 168 | ;; Dispatch an invalid update command with an incomplete user 169 | (let [cmd '(user/update {:user {:user/name "John Doe"}})] 170 | (is (thrown? ExceptionInfo (bc/dispatch courier cmd)))) 171 | 172 | ;; Dispatch a valid update command with a missing auth user 173 | (let [cmd '(user/update {:user {:user/name "Jeff Doe" 174 | :user/email "jeff@doe.name" 175 | :user/username "jeff"}}) 176 | env {}] 177 | (is (thrown? ExceptionInfo (bc/dispatch courier cmd env)))) 178 | 179 | ;; Dispatch a valid update command with the wrong auth user 180 | (let [cmd '(user/update {:user {:user/name "Jeff Doe" 181 | :user/email "jeff@doe.name" 182 | :user/username "jeff"}}) 183 | env {:auth-user {:user/username "joe"}}] 184 | (is (thrown? ExceptionInfo (bc/dispatch courier cmd env)))) 185 | 186 | ;; Dispatch a valid update command with the auth user being 187 | ;; the same as the user being updated 188 | (let [cmd '(user/update {:user {:user/name "Jeff Doe" 189 | :user/email "jeff@doe.name" 190 | :user/username "jeff"}}) 191 | env {:auth-user {:user/username "jeff"}}] 192 | (is (bc/dispatch courier cmd env))) 193 | 194 | ;; Dispatch a valid update command with the auth user being 195 | ;; a different user - but an admin 196 | (let [cmd '(user/update {:user {:user/name "Jeff Doe" 197 | :user/email "jeff@doe.name" 198 | :user/username "jeff"}}) 199 | env {:auth-user {:user/username "joe" 200 | :user/role :admin}}] 201 | (is (bc/dispatch courier cmd env))))) 202 | -------------------------------------------------------------------------------- /src/workflo/brahman/backends/datascript.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.backends.datascript 2 | (:require [datascript.core :as d] 3 | [om.next.impl.parser :as om-parser] 4 | [workflo.brahman.model :as bm])) 5 | 6 | ;;;; Schema attribute extraction 7 | 8 | (defn- attr-name 9 | "Takes a a schema name 'foo' and a short attribute name 'bar' and 10 | returns a prefixed attribute name as a keyword (e.g. :foo/bar)." 11 | [schema-name short-name] 12 | (keyword (name schema-name) (name short-name))) 13 | 14 | (defn- join? 15 | "Takes an attribute name and attribute specification name and 16 | returns whether or not the specification is a join with another 17 | model." 18 | [[short-name spec]] 19 | (and (map? spec) 20 | (= 1 (count spec)))) 21 | 22 | (defn- gather-regular-attr-names 23 | "Returns a vector of the prefixed names of all regular, non-join 24 | attributes in the model." 25 | [model-name schema] 26 | {:pre [(map? schema)]} 27 | (into [] 28 | (comp (remove join?) 29 | (map (fn [[name attr-spec]] 30 | (attr-name model-name name)))) 31 | schema)) 32 | 33 | (declare model->attrs) 34 | 35 | (defn model->joins 36 | "Takes a model and returns a map of model-prefixed attribute names 37 | to {:model :attr } map. It does 38 | not recurse into target models and their attributes." 39 | [model] 40 | (letfn [(join [model-name short-name attr] 41 | (let [[spec target] (first attr) 42 | modeler (bm/get-modeler model) 43 | target-model (if (= target '...) 44 | model 45 | (bm/get-model modeler target))] 46 | (hash-map (attr-name model-name short-name) 47 | {:model target-model 48 | :attr attr}))) 49 | (gather-join [res [short-name spec :as attr]] 50 | (cond-> res 51 | (join? attr) (merge (join (bm/model-name model) 52 | short-name 53 | spec))))] 54 | (reduce gather-join {} (bm/schema model)))) 55 | 56 | (defn- gather-join-attrs 57 | "Returns a vector of all join attributes in the model, each 58 | represented by a { } 59 | map. Resolves joins recursively to fill in the target model 60 | attributes." 61 | [model recursive?] 62 | (let [schema (bm/schema model)] 63 | (letfn [(recur-join [target] 64 | (model->attrs target recursive?)) 65 | (join-attr [[attr-name attr-info]] 66 | (hash-map attr-name 67 | (if (and recursive? 68 | (not= (:model attr-info) model)) 69 | (recur-join (:model attr-info)) 70 | [:db/id])))] 71 | (into [] (map join-attr) (model->joins model))))) 72 | 73 | (defn model->attrs 74 | "Returns a vector of all attributes in the model, with joins 75 | resolved into the target models recursively." 76 | [model recursive?] 77 | (let [name (bm/model-name model) 78 | schema (bm/schema model) 79 | attr-names (gather-regular-attr-names name schema) 80 | joins (gather-join-attrs model recursive?) 81 | ret (into [] (concat [:db/id] attr-names joins))] 82 | ret)) 83 | 84 | ;;;; Schema generation 85 | 86 | (defn kv->field 87 | [res [k v]] 88 | (cond 89 | (vector? v) (conj res (into [] (concat [k] v))) 90 | (map? v) (kv->field res [k (ffirst v)]) 91 | :else (conj res [k v]))) 92 | 93 | (defn attrs->fields 94 | [schema] 95 | (reduce kv->field [] schema)) 96 | 97 | (defn datascript-schema 98 | [[model attrs]] 99 | (letfn [(ref? [[attr-name & attr-spec]] 100 | (some #{:ref} attr-spec)) 101 | (ref-attr-schema [model [attr-name & attr-spec]] 102 | (let [attr-schema [(keyword (name (bm/model-name model)) 103 | (name attr-name)) 104 | (cond 105 | #(some #{:ref} attr-spec) 106 | (if (some #{:many} attr-spec) 107 | {:db.valueType :db.type/ref 108 | :db/cardinality :db.cardinality/many} 109 | {:db/valueType :db.type/ref}))]] 110 | attr-schema))] 111 | (into {} 112 | (comp (filter ref?) 113 | (map #(ref-attr-schema model %))) 114 | (attrs->fields attrs)))) 115 | 116 | ;;;; Schema installation 117 | 118 | (defn install-schemas 119 | [stores schemas] 120 | (let [matching-schemas (->> (select-keys schemas stores) 121 | (vals) 122 | (apply concat) 123 | (mapv datascript-schema) 124 | (apply merge))] 125 | (d/create-conn matching-schemas))) 126 | 127 | ;;;; Query execution 128 | 129 | (defn query-derived-attr 130 | [conn env attr query entity] 131 | (let [entity-id (or (:entity-id env) :db/id)] 132 | (d/q (:query attr) @conn (entity-id entity) query))) 133 | 134 | (defn- identifying-model-attr 135 | [model] 136 | (first (filter 137 | (fn [attr] 138 | (let [attr-name (cond-> attr 139 | (map? attr) ffirst)] 140 | (= (name (bm/model-name model)) 141 | (namespace attr-name)))) 142 | (bm/attrs model false)))) 143 | 144 | (defn- param-clause 145 | [[attr value]] 146 | (if (= :db/id attr) 147 | `[(~'= ~'?e ~value)] 148 | ['?e attr value])) 149 | 150 | (defn datascript-query 151 | "Translate an Om Next query to a DataScript query based 152 | on the current DataScript schema." 153 | ([env query] 154 | (datascript-query env query [])) 155 | ([{:keys [model fetch-one?]} query params] 156 | (let [model-attr (identifying-model-attr model) 157 | param-clauses (map param-clause params)] 158 | (if fetch-one? 159 | ;; Query a single entity 160 | `[:find (~'pull ~'?e ~query) . 161 | :where [~'?e ~model-attr] 162 | ~@param-clauses] 163 | ;; Query a collection of entities 164 | `[:find [(~'pull ~'?e ~query) ~'...] 165 | :where [~'?e ~model-attr] 166 | ~@param-clauses])))) 167 | 168 | (defn extract-links 169 | "Takes an Om Next query and extracts all links from it, returning 170 | the Om Next query without links and a mapping of query paths to 171 | links (as Om Next AST fragments) in the following form: 172 | 173 | {:query 174 | :links {[] [ ] 175 | [:foo :bar] [ ]}}." 176 | [query] 177 | (letfn [(ast-link? [{:keys [key]}] 178 | (and (vector? key) 179 | (= 2 (count key)) 180 | (or (number? (second key)) 181 | (string? (second key)) 182 | (keyword? (second key)) 183 | (= '_ (second key))))) 184 | (extract-links* [ast path links] 185 | (cond-> ast 186 | (:children ast) 187 | (update :children 188 | (fn [children] 189 | (let [child-links (filter ast-link? children)] 190 | (when-not (empty? child-links) 191 | (swap! links update path 192 | (comp set concat) 193 | child-links))) 194 | (into [] 195 | (comp (remove ast-link?) 196 | (map (fn [{:keys [key] :as child}] 197 | (extract-links* child 198 | (conj path 199 | key) 200 | links)))) 201 | children)))))] 202 | (let [ast (om-parser/query->ast query) 203 | links (atom {}) 204 | ast-without-links (extract-links* ast [] links)] 205 | {:query (om-parser/ast->expr ast-without-links) 206 | :links @links}))) 207 | 208 | (defn query-links 209 | "Queries all links individually and returns a mapping of query 210 | paths to link results, each of which is represented as a map 211 | storing the original link in :link and its query result in 212 | :result." 213 | [conn env links] 214 | (letfn [(query-link [{:keys [key query] :as link}] 215 | {:link link 216 | :result ((:query-link env) env key query)})] 217 | (into {} 218 | (map (fn [[path links]] 219 | [path (mapv query-link links)])) 220 | links))) 221 | 222 | (defn merge-link-results 223 | "Merges the results from link queries into a a DataScript query 224 | result." 225 | [query-result link-results] 226 | (letfn [(merge-link-results-at-path [entity-or-entities [path results]] 227 | (if (bm/collection? entity-or-entities) 228 | (into #{} 229 | (map #(merge-link-results-at-path 230 | % [path results])) 231 | entity-or-entities) 232 | (if (empty? path) 233 | (reduce (fn [entity {:keys [link result]}] 234 | (assoc entity (:dispatch-key link) result)) 235 | entity-or-entities 236 | results) 237 | (update entity-or-entities 238 | (first path) 239 | merge-link-results-at-path 240 | [(rest path) results]))))] 241 | (reduce merge-link-results-at-path 242 | query-result 243 | link-results))) 244 | 245 | (defn query-store 246 | "Executes an Om Next query against DataScript." 247 | [conn env query params] 248 | (let [{:keys [query links]} (extract-links query) 249 | ds-query (datascript-query env query params) 250 | ds-result (d/q ds-query @conn) 251 | link-results (query-links conn env links) 252 | result (merge-link-results ds-result 253 | link-results)] 254 | result)) 255 | -------------------------------------------------------------------------------- /test/workflo/brahman/test/backends/datascript.clj: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.test.backends.datascript 2 | (:require [clojure.test :refer [deftest is]] 3 | [clojure.test.check.clojure-test :refer [defspec]] 4 | [clojure.test.check.generators :as gen] 5 | [clojure.test.check.properties :as prop] 6 | [datascript.core :as d] 7 | [workflo.brahman.backends.datascript :as bds] 8 | [workflo.brahman.model :as bm])) 9 | 10 | (def ^:const +models+ 11 | {:user {:name :user 12 | :schema {:name [:string :indexed] 13 | :email [:string :indexed] 14 | :friend {[:ref :many] '...} 15 | :post {[:ref :many] :post}} 16 | :stores [:datascript] 17 | :derived-attrs [{:name :friend-count 18 | :store :datascript 19 | :query '[:find (count ?f) . 20 | :in $ ?n 21 | :where [?u :user/name ?n] 22 | [?u :user/friend ?f]]}]} 23 | :post {:name :post 24 | :schema {:author {[:ref :one] :user} 25 | :title [:string :indexed]} 26 | :stores [:datascript]}}) 27 | 28 | (deftest fetch-one-query-for-a-model-is-correct 29 | (let [modeler (bm/modeler {:models (vals +models+) 30 | :model->attrs bds/model->attrs 31 | :model->joins bds/model->joins}) 32 | model (bm/get-model modeler :user)] 33 | (is (= '[:find (pull ?e [:user/name :user/email]) . 34 | :where [?e :user/name] 35 | [?e :db/id 10]] 36 | (bds/datascript-query {:model model 37 | :fetch-one? true} 38 | [:user/name :user/email] 39 | '[[?e :db/id 10]]))))) 40 | 41 | (deftest fetch-one-query-for-another-model-is-correct 42 | (let [modeler (bm/modeler {:models (vals +models+) 43 | :model->attrs bds/model->attrs 44 | :model->joins bds/model->joins}) 45 | model (bm/get-model modeler :post)] 46 | (is (= '[:find (pull ?e [:post/title]) . 47 | :where [?e :post/title] 48 | [?e :post/title "Hello"]] 49 | (bds/datascript-query {:model model 50 | :fetch-one? true} 51 | [:post/title] 52 | '[[?e :post/title "Hello"]]))))) 53 | 54 | (deftest fetch-many-query-for-a-model-is-correct 55 | (let [modeler (bm/modeler {:models (vals +models+) 56 | :model->attrs bds/model->attrs 57 | :model->joins bds/model->joins}) 58 | model (bm/get-model modeler :user)] 59 | (is (= '[:find [(pull ?e [:user/name :user/email]) ...] 60 | :where [?e :user/name]] 61 | (bds/datascript-query {:model model 62 | :fetch-one? false} 63 | [:user/name :user/email]))))) 64 | 65 | (deftest fetch-many-query-for-another-model-is-correct 66 | (let [modeler (bm/modeler {:models (vals +models+) 67 | :model->attrs bds/model->attrs 68 | :model->joins bds/model->joins}) 69 | model (bm/get-model modeler :post)] 70 | (is (= '[:find [(pull ?e [:post/title]) ...] 71 | :where [?e :post/title]] 72 | (bds/datascript-query {:model model 73 | :fetch-one? false} 74 | [:post/title]))))) 75 | 76 | (defn install-schemas 77 | [conn schemas] 78 | (->> schemas 79 | (bds/install-schemas [:datascript]) 80 | (reset! conn))) 81 | 82 | (defn query-derived-attr 83 | [conn env attr query entity] 84 | (bds/query-derived-attr @conn env attr query entity)) 85 | 86 | (defn query-link 87 | [conn env [link-name id] query] 88 | (case link-name 89 | :link-one (cond-> {:foo "Foo" :bar "Bar"} 90 | query (select-keys query)) 91 | :link-two (* id 10))) 92 | 93 | (defn query-store 94 | [conn env query params] 95 | (bds/query-store @conn env query params)) 96 | 97 | (defn add-entities 98 | [conn entities] 99 | (d/transact! conn entities)) 100 | 101 | (deftest fetching-one-entity-from-datascript-works 102 | (let [conn (atom nil) 103 | modeler (bm/modeler 104 | {:models (vals +models+) 105 | :model->attrs bds/model->attrs 106 | :model->joins bds/model->joins 107 | :entity-id :user/name 108 | :install-schemas (partial install-schemas conn) 109 | :query-derived-attr (partial query-derived-attr conn) 110 | :query-store (partial query-store conn)}) 111 | users (bm/get-model modeler :user)] 112 | (add-entities @conn 113 | [{:db/id -1 114 | :user/name "Jeff" 115 | :user/email "jeff@jeff.org" 116 | :user/friend [-2 -3] 117 | :user/post -4} 118 | {:db/id -2 119 | :user/name "Linda" 120 | :user/email "linda@linda.org" 121 | :user/friend -1 122 | :user/post -5} 123 | {:db/id -3 124 | :user/name "Joe" 125 | :user/email "joe@joe.org"} 126 | {:db/id -4 127 | :post/title "Jeff's post" 128 | :post/author -1} 129 | {:db/id -5 130 | :post/title "Linda's post" 131 | :post/author -2}]) 132 | (and (is (= {:user/name "Jeff" 133 | :user/email "jeff@jeff.org" 134 | :user/friend-count 2} 135 | (bm/query users [:user/name 136 | :user/email 137 | :user/friend-count] 138 | {:fetch-one? true} 139 | '[[?e :user/name "Jeff"]]))) 140 | (is (= {:user/email "linda@linda.org" 141 | :user/post [{:post/title "Linda's post"}] 142 | :user/friend [{:user/name "Jeff"}] 143 | :user/friend-count nil} 144 | (bm/query users 145 | [:user/email 146 | :user/friend-count 147 | {:user/post [:post/title]} 148 | {:user/friend [:user/name]}] 149 | {:fetch-one? true} 150 | '[[?e :user/name "Linda"]])))))) 151 | 152 | (deftest fetching-many-entities-from-datascript-works 153 | (let [conn (atom nil) 154 | modeler (bm/modeler 155 | {:models (vals +models+) 156 | :model->attrs bds/model->attrs 157 | :model->joins bds/model->joins 158 | :entity-id :user/name 159 | :install-schemas (partial install-schemas conn) 160 | :query-derived-attr (partial query-derived-attr conn) 161 | :query-store (partial query-store conn)}) 162 | users (bm/get-model modeler :user) 163 | posts (bm/get-model modeler :post)] 164 | (add-entities @conn 165 | [{:db/id -1 166 | :user/name "Jeff" 167 | :user/email "jeff@jeff.org" 168 | :user/friend -2 169 | :user/post -3} 170 | {:db/id -2 171 | :user/name "Linda" 172 | :user/email "linda@linda.org" 173 | :user/friend -1 174 | :user/post -4} 175 | {:db/id -3 176 | :post/title "Jeff's post" 177 | :post/author -1} 178 | {:db/id -4 179 | :post/title "Linda's post" 180 | :post/author -2}]) 181 | (and (is (= #{{:user/name "Jeff" :user/email "jeff@jeff.org"} 182 | {:user/name "Linda" :user/email "linda@linda.org"}} 183 | (bm/query users [:user/name :user/email]))) 184 | (is (= #{{:post/title "Jeff's post" 185 | :post/author {:user/name "Jeff"}}} 186 | (bm/query posts 187 | [:post/title 188 | {:post/author [:user/name]}] 189 | {} 190 | '[[?e :post/author ?u] 191 | [?u :user/name "Jeff"]])))))) 192 | 193 | (deftest queries-with-links-work 194 | (let [conn (atom nil) 195 | modeler (bm/modeler 196 | {:models (vals +models+) 197 | :model->attrs bds/model->attrs 198 | :model->joins bds/model->joins 199 | :entity-id :user/name 200 | ;; :model->links bds/model->links 201 | :install-schemas (partial install-schemas conn) 202 | :query-derived-attr (partial query-derived-attr conn) 203 | :query-link (partial query-link conn) 204 | :query-store (partial query-store conn)}) 205 | users (bm/get-model modeler :user)] 206 | (add-entities @conn 207 | [{:db/id -1 208 | :user/name "Jeff" 209 | :user/email "jeff@jeff.org" 210 | :user/friend -2 211 | :user/post -3} 212 | {:db/id -2 213 | :user/name "Linda" 214 | :user/email "linda@linda.org" 215 | :user/friend -1 216 | :user/post -4} 217 | {:db/id -3 218 | :post/title "Jeff's post" 219 | :post/author -1} 220 | {:db/id -4 221 | :post/title "Linda's post" 222 | :post/author -2}]) 223 | (and (is (= {:user/name "Jeff" 224 | :link-one {:foo "Foo" :bar "Bar"}} 225 | (bm/query users 226 | [:user/name [:link-one '_]] 227 | {:fetch-one? true} 228 | '[[?e :user/name "Jeff"]]))) 229 | (is (= #{{:user/name "Jeff" 230 | :link-one {:foo "Foo"}} 231 | {:user/name "Linda" 232 | :link-one {:foo "Foo"}}} 233 | (bm/query users 234 | [:user/name {[:link-one '_] [:foo]}]))) 235 | (is (= {:user/name "Jeff" 236 | :link-two 100} 237 | (bm/query users 238 | [:user/name [:link-two 10]] 239 | {:fetch-one? true} 240 | '[[?e :user/name "Jeff"]])))))) 241 | -------------------------------------------------------------------------------- /src/workflo/brahman/model.cljc: -------------------------------------------------------------------------------- 1 | (ns workflo.brahman.model 2 | #?(:cljs (:require-macros [com.rpl.specter.macros :refer [transform]])) 3 | (:require #?(:clj [com.rpl.specter.macros :refer [transform]]) 4 | [om.next.impl.parser :as om-parser])) 5 | 6 | ;;;; Transformations 7 | 8 | (defn transform-specter [tspec raw-data] 9 | (let [seq-data (if (coll? raw-data) raw-data [raw-data]) 10 | tform (into [] (conj tspec seq-data)) 11 | res (apply #(transform %1 %2 %3) tform)] 12 | (cond-> res 13 | (not (coll? raw-data)) first))) 14 | 15 | ;;;; Model protocol 16 | 17 | (defprotocol IModel 18 | (model-name [this] "The name of the model as a symbol") 19 | (schema [this] "The raw schema as specified originally") 20 | (validation [this] "Validation rules defined for the model") 21 | (stores [this] "The data stores used in this model") 22 | (derived-attrs [this] "The derived attributes used in this model") 23 | (attrs [this] 24 | [this recursive?] 25 | "Attributes for use in queries, computed 26 | from the raw schema (with joins etc.)") 27 | (get-modeler [this] "Returns the modeler that manages the 28 | models.") 29 | (query [this q] 30 | [this q env] 31 | [this q env params] 32 | "Queries the model based on its data 33 | stores and derived attributes, given the 34 | query q and extra information (e.g. query 35 | clauses).") 36 | (validate [this data])) 37 | 38 | ;;;; Modeler protocol 39 | 40 | (defprotocol IModeler 41 | (models [this] "Returns all models known to the modeler.") 42 | (get-model [this name] "Returns the model with the given name") 43 | (schemas [this] "Returns a map of the following structure: 44 | { { 45 | } 46 | ...}")) 47 | 48 | ;;;; Utilities 49 | 50 | (defn collection? 51 | [query-result] 52 | (and (coll? query-result) 53 | (not (map? query-result)))) 54 | 55 | (defn derived-attr-name 56 | [model attr] 57 | (if (or (:prefixed? attr) 58 | (not (some #{:prefixed?} (keys attr)))) 59 | (keyword (name (model-name model)) 60 | (name (:name attr))) 61 | (keyword (namespace (:name attr)) 62 | (name (:name attr))))) 63 | 64 | ;;;; Query execution 65 | 66 | (defn query-derived-attr 67 | [{:keys [merge-derived-attr model query-derived-attr] :as env} 68 | attr attr-q result] 69 | (letfn [(derive-attr [entity] 70 | (->> (query-derived-attr env attr attr-q entity) 71 | (merge-derived-attr model entity attr)))] 72 | (if (collection? result) 73 | (into #{} (map derive-attr result)) 74 | (derive-attr result)))) 75 | 76 | (defn has-join-attr? 77 | [query-result attr-name] 78 | (if (collection? query-result) 79 | (contains? (first query-result) attr-name) 80 | (contains? query-result attr-name))) 81 | 82 | (declare query-derived-attrs) 83 | (declare extract-attr-query) 84 | 85 | (defn query-derived-attrs-follow-joins 86 | [model env ast query-result] 87 | (let [config (:config model) 88 | joins ((:model->joins config) model)] 89 | (reduce (fn [query-result [attr-name attr :as join]] 90 | (letfn [(follow-join [entity-or-entities] 91 | (let [attr-query (extract-attr-query 92 | ast attr-name)] 93 | (query-derived-attrs (:model attr) env 94 | attr-query 95 | entity-or-entities)))] 96 | (if (has-join-attr? query-result attr-name) 97 | (if (collection? query-result) 98 | (into #{} 99 | (map #(update % attr-name follow-join)) 100 | query-result) 101 | (update query-result attr-name follow-join)) 102 | query-result))) 103 | query-result 104 | joins))) 105 | 106 | (defn extract-attr-query 107 | [ast attr-name] 108 | (first (filter #(= attr-name (:key %)) (:children ast)))) 109 | 110 | (defn query-derived-attrs 111 | [model env ast query-result] 112 | (let [config (:config model) 113 | result (reduce (fn [query-result {:keys [name] :as attr}] 114 | (let [env' (merge env config {:model model}) 115 | attr-n (derived-attr-name model attr) 116 | attr-q (extract-attr-query ast attr-n)] 117 | (cond->> query-result 118 | (not (nil? attr-q)) 119 | (query-derived-attr env' attr 120 | (:query attr-q))))) 121 | query-result 122 | (derived-attrs model)) 123 | result (query-derived-attrs-follow-joins model env ast result)] 124 | result)) 125 | 126 | (defn query-store 127 | [{:keys [model->attrs query-store model] :as env} q params] 128 | (let [attrs (or q (model->attrs model false))] 129 | (query-store env attrs params))) 130 | 131 | (defn query-stores 132 | [model env q params] 133 | (let [config (:config model) 134 | merge-store (:merge-store config)] 135 | (reduce (fn [ret store] 136 | (let [env' (assoc (merge env config) 137 | :model model 138 | :store store) 139 | result (query-store env' q params)] 140 | (if (:fetch-one? env) 141 | (merge ret result) 142 | (merge-store store ret result)))) 143 | nil 144 | (stores model)))) 145 | 146 | ;;;; Model implementation 147 | 148 | (defrecord Model [props config modeler] 149 | IModel 150 | (model-name [this] 151 | (:name props)) 152 | 153 | (schema [this] 154 | (:schema props)) 155 | 156 | (validation [this] 157 | (:validation props)) 158 | 159 | (stores [this] 160 | (:stores props)) 161 | 162 | (derived-attrs [this] 163 | (:derived-attrs props)) 164 | 165 | (attrs [this] 166 | (attrs this true)) 167 | 168 | (attrs [this recursive?] 169 | ((:model->attrs config) this recursive?)) 170 | 171 | (get-modeler [this] 172 | modeler) 173 | 174 | (query [this q] 175 | (query this q {})) 176 | 177 | (query [this q env] 178 | (query this q env [])) 179 | 180 | (query [this q env params] 181 | (let [ast (om-parser/query->ast q)] 182 | (->> (query-stores this env q params) 183 | (query-derived-attrs this env ast)))) 184 | 185 | (validate [this data] 186 | (let [config (:config this)] 187 | ((:validate config) this (validation this) data)))) 188 | 189 | (defn model 190 | [{:keys [schema] :as props} config modeler] 191 | {:pre [(or (nil? schema) (map? schema)) 192 | (satisfies? IModeler modeler)]} 193 | (Model. props config modeler)) 194 | 195 | ;;;; Model merging 196 | 197 | (defn merge-model-field 198 | [field1 field2] 199 | (cond 200 | (and (vector? field1) (vector? field2)) (concat field1 field2) 201 | (and (map? field1) (map? field2)) (merge field1 field2) 202 | (= field1 field2) field1)) 203 | 204 | (defn default-merge-model 205 | "Merges a model into a resulting model." 206 | [res model] 207 | (merge-with merge-model-field res model)) 208 | 209 | (defn merge-model-group 210 | [merge-model [name group]] 211 | (reduce merge-model {} group)) 212 | 213 | (defn merge-models 214 | "Merges all models with the same name." 215 | [merge-model models] 216 | (let [grouped (group-by :name models)] 217 | (mapv #(merge-model-group merge-model %) grouped))) 218 | 219 | ;;;; Modeler implementation 220 | 221 | (defn- models-for-store [models store] 222 | (into #{} 223 | (filter (fn [model] 224 | (some #{store} (stores model)))) 225 | models)) 226 | 227 | (defn- install-schemas! 228 | "Collects the schemas from all models" 229 | [modeler] 230 | (let [config (:config modeler)] 231 | ((:install-schemas config) (schemas modeler)))) 232 | 233 | (defrecord Modeler [config models] 234 | IModeler 235 | (models [this] 236 | @models) 237 | 238 | (get-model [this name] 239 | (first (filter #(= name (model-name %)) @models))) 240 | 241 | (schemas [this] 242 | (let [models @models] 243 | (let [stores (set (apply concat (map stores models)))] 244 | (letfn [(collect-step [res store] 245 | (let [models (models-for-store models store) 246 | schemas (map schema models) 247 | m (zipmap models schemas)] 248 | (update res store #(into {} (merge % m)))))] 249 | (reduce collect-step {} stores)))))) 250 | 251 | (defn- default-store-schema 252 | [schema] 253 | schema) 254 | 255 | (defn- default-validation 256 | [model] 257 | nil) 258 | 259 | (defn- default-model->attrs 260 | [model recursive?] 261 | []) 262 | 263 | (defn- default-model->joins 264 | [model] 265 | {}) 266 | 267 | (defn- default-validate 268 | [model validation data] 269 | nil) 270 | 271 | (defn- default-query-store 272 | [env query {:keys [inputs extra]}] 273 | nil) 274 | 275 | (defn- default-query-derived-attr 276 | [env attr query entity] 277 | nil) 278 | 279 | (defn- default-query-link 280 | [env link query] 281 | nil) 282 | 283 | (defn- default-transform [tspec raw-value] 284 | (tspec raw-value)) 285 | 286 | (defn- default-merge-store 287 | [store data store-data] 288 | (-> #{} 289 | (into data) 290 | (into store-data))) 291 | 292 | (defn- default-merge-derived-attr 293 | [model entity attr value] 294 | (let [attr-name (derived-attr-name model attr)] 295 | (assoc entity attr-name value))) 296 | 297 | (defn modeler 298 | [{:keys [models 299 | merge-model 300 | install-schemas 301 | model->attrs 302 | model->joins 303 | validate 304 | entity-id 305 | query-store 306 | query-derived-attr 307 | query-link 308 | merge-store 309 | merge-derived-attr 310 | transform] 311 | :or {models [] 312 | merge-model default-merge-model 313 | install-schemas (constantly nil) 314 | model->attrs default-model->attrs 315 | model->joins default-model->joins 316 | validate default-validate 317 | entity-id :db/id 318 | query-store default-query-store 319 | query-derived-attr default-query-derived-attr 320 | query-link default-query-link 321 | merge-store default-merge-store 322 | merge-derived-attr default-merge-derived-attr 323 | transform default-transform} 324 | :as config}] 325 | {:pre [(map? config)]} 326 | (let [config {:install-schemas install-schemas 327 | :entity-id entity-id 328 | :validate validate 329 | :model->attrs model->attrs 330 | :model->joins model->joins 331 | :query-store query-store 332 | :query-derived-attr query-derived-attr 333 | :query-link query-link 334 | :merge-store merge-store 335 | :merge-derived-attr merge-derived-attr 336 | :transform transform} 337 | model-specs (merge-models merge-model models) 338 | models (atom #{}) 339 | modeler (Modeler. config models)] 340 | (doseq [model-spec model-specs] 341 | (let [model' (model model-spec config modeler)] 342 | (swap! models conj model'))) 343 | (install-schemas! modeler) 344 | modeler)) 345 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | --------------------------------------------------------------------------------