├── .gitignore ├── .travis.yml ├── README.md ├── project.clj ├── shadow-cljs.edn ├── src └── clj │ └── qbits │ └── spex.cljc └── test └── qbits └── spex └── test └── core_test.cljc /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | /doc 6 | pom.xml* 7 | *.jar 8 | *.class 9 | *.asc 10 | .lein-deps-sum 11 | .lein-failures 12 | .lein-plugins 13 | /.shadow-cljs/ 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | 3 | lein: lein2 4 | 5 | jdk: 6 | - openjdk6 7 | - openjdk7 8 | - oraclejdk7 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spex 2 | [![cljdoc badge](https://cljdoc.xyz/badge/cc.qbits/spex)](https://cljdoc.xyz/d/cc.qbits/spex/CURRENT) 3 | 4 | Small utility/extension library for `clojure.spec`. 5 | 6 | Subject to changes/breakage. Use at own risk. 7 | 8 | At the moment it does only 3 things: 9 | 10 | assuming 11 | 12 | ``` clojure 13 | (require '[qbits.spex :as spex]) 14 | ``` 15 | * add `def-derived` which creates a keyword hierarchy behind the 16 | scenes (the hierarchy is internal/scoped to spex/ so no risk of 17 | cluttering the global one) 18 | 19 | ```clj 20 | (s/def ::foo string?) 21 | (spex/def-derived ::bar ::foo) 22 | ``` 23 | equivalent to: 24 | ```clj 25 | (s/def ::foo string?) 26 | (s/def ::bar ::foo) 27 | (spex/derive ::bar ::foo) 28 | ``` 29 | 30 | but that also works with maps, here foo will derive from ::baz and ::bar 31 | 32 | ```clj 33 | (spex/def-merged ::foo [::bar ::baz]) 34 | ``` 35 | equivalent to: 36 | ```clj 37 | (s/def ::foo (s/merge ::bar ::baz)) 38 | (spex/derive ::foo ::bar) 39 | (spex/derive ::foo ::baz) 40 | ``` 41 | 42 | So why do that? well you can then inspect the hierarchy, which can 43 | be handy: 44 | 45 | ``` clj 46 | (s/def-merged ::foo [::bar ::baz]) 47 | 48 | (spex/ancestors ::foo) => #{::bar ::baz} 49 | (spex/isa? ::foo ::bar) => true 50 | (spex/isa? ::foo ::baz) => true 51 | ``` 52 | 53 | * adds a metadata registry for registered specs, it currently supports 54 | variants of `vary-meta!`, `with-meta!`, `meta`, adds 55 | `unregister-meta!` and `with-doc`. 56 | 57 | you can then write code like: 58 | ```clj 59 | (-> (s/def ::foo string?) 60 | (spex/vary-meta! assoc :something :you-need)) 61 | 62 | ;; and retrieve the values with spex/meta 63 | (spex/meta ::foo) => {:something :you-need} 64 | ``` 65 | 66 | Since we have this hierarchy in place we can integrate this into 67 | metadata retrieval. Told you it could be useful :) 68 | 69 | ```clj 70 | (spex/def-derived ::bar ::foo) 71 | 72 | (spex/meta ::bar) => nil 73 | 74 | ;; remember we have meta data on :foo already, such that: 75 | (spex/meta ::foo) => {:something :you-need} 76 | 77 | ;; register meta at ::bar level 78 | (spex/vary-meta! ::bar assoc :another :key) 79 | 80 | ;; just the meta of ::bar 81 | (spex/meta ::bar) => {:another :key} 82 | 83 | ;; retrieve the meta for ::bar but also all its ancestors if you pass true to spex/meta 84 | (spex/meta ::bar true) => {:something :you-need, :another :key} 85 | ``` 86 | 87 | and `spex/with-doc` is just sugar on top of all this to add docstrings to specs 88 | 89 | ```clj 90 | (spex/with-doc ::foo "bla bla bla") 91 | 92 | (s/doc ::foo) => "bla bla bla" 93 | ``` 94 | 95 | All the functions that mutate the metadata of a spec return the spec 96 | key, that makes chaining easier, same goes for `spex/def-derived`: 97 | 98 | ```clojure 99 | (-> (s/def ::foo string?) 100 | (spex/vary-meta! assoc :something :you-need) 101 | (cond-> 102 | something? 103 | (spex/vary-meta! assoc :something-else :you-might-need))) 104 | ``` 105 | The internal hierarchy is queriable just like the global keyword hierarchy, 106 | you can use `spex/isa?` `spex/descendants` `spex/ancestors` 107 | `spex/parents` `spex/derive` `spex/underive`, which are just 108 | partially applied functions over the same functions in core with our 109 | own internal hierarchy `spex/spec-hierarchy`. 110 | 111 | 112 | ```clojure 113 | (s/def ::port (int-in-range? 1 65535)) 114 | (spex/def-derived ::redis-port ::port) 115 | 116 | (spex/isa? ::redis-port ::port) => true 117 | 118 | (spex/def-derived ::cassandra-port ::port) 119 | 120 | ;; list all things ::port 121 | (spex/descendants ::port) => #{::redis-port ::cassandra-port})) 122 | 123 | ``` 124 | 125 | This only works for aliases obviously. 126 | 127 | * adds a sugar to create namespaces within a ns. Ex: if you are in 128 | the user namespace `(spex/rel-ns 'foo.bar)` would create 129 | user.foo.bar 130 | 131 | ## Installation 132 | 133 | spex is [available on Clojars](https://clojars.org/cc.qbits/spex). 134 | 135 | [![Clojars Project](https://img.shields.io/clojars/v/cc.qbits/spex.svg)](https://clojars.org/cc.qbits/spex) 136 | 137 | ## License 138 | 139 | Copyright © 2016 [Max Penet](http://twitter.com/mpenet) 140 | 141 | Distributed under the 142 | [Eclipse Public License](http://www.eclipse.org/legal/epl-v10.html), 143 | the same as Clojure. 144 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject cc.qbits/spex "0.7.1" 2 | :description "Simple spex extensions, utils" 3 | :url "https://github.com/mpenet/spex" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.10.1"]] 7 | :profiles {:dev {:dependencies [[org.clojure/test.check "0.9.0"] 8 | [com.gfredericks/test.chuck "0.2.6"] 9 | [thheller/shadow-cljs "2.8.69"]]}} 10 | :source-paths ["src/clj"] 11 | :global-vars {*warn-on-reflection* true} 12 | :aliases {"shadow-cljs" ["run" "-m" "shadow.cljs.devtools.cli"] 13 | "test-cljs" ["shadow-cljs" "compile" "test"]}) 14 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:lein {:profile "+dev"} 2 | :builds {:test {:target :node-test 3 | :output-to "target/node-tests.js" 4 | :autorun true}}} 5 | -------------------------------------------------------------------------------- /src/clj/qbits/spex.cljc: -------------------------------------------------------------------------------- 1 | (ns qbits.spex 2 | #?(:cljs (:require-macros [qbits.spex])) 3 | (:refer-clojure 4 | :exclude [meta isa? parents ancestors derive descendants underive]) 5 | (:require 6 | [clojure.spec.alpha :as s])) 7 | 8 | (defmacro rel-ns 9 | "Creates a relative aliased namespace matching supplied symbol" 10 | [k] 11 | `(alias ~k (create-ns (symbol (str *ns* "." (str ~k)))))) 12 | 13 | ;; The following only works only for registered specs 14 | (s/def ::metadata-registry-val (s/map-of qualified-keyword? any?)) 15 | 16 | (defonce metadata-registry (atom {})) 17 | (defonce spec-hierarchy (atom (make-hierarchy))) 18 | 19 | (s/fdef derive 20 | :args (s/cat :tag qualified-keyword? 21 | :parent qualified-keyword?)) 22 | (defn derive 23 | "Like clojure.core/derive but scoped on our spec hierarchy" 24 | [tag parent] 25 | (swap! spec-hierarchy 26 | clojure.core/derive tag parent)) 27 | 28 | (s/fdef underive 29 | :args (s/cat :tag qualified-keyword? 30 | :parent qualified-keyword?)) 31 | (defn underive 32 | "Like clojure.core/underive but scoped on our spec hierarchy" 33 | [tag parent] 34 | (swap! spec-hierarchy 35 | clojure.core/underive tag parent)) 36 | 37 | (s/fdef isa? 38 | :args (s/cat :child qualified-keyword? 39 | :parent qualified-keyword?)) 40 | (defn isa? 41 | "Like clojure.core/isa? but scoped on our spec hierarchy" 42 | [child parent] 43 | (clojure.core/isa? @spec-hierarchy child parent)) 44 | 45 | (s/fdef parents 46 | :args (s/cat :tag qualified-keyword?)) 47 | (defn parents 48 | "Like clojure.core/parents but scoped on our spec hierarchy" 49 | [tag] 50 | (clojure.core/parents @spec-hierarchy tag)) 51 | 52 | (s/fdef ancestors 53 | :args (s/cat :tag qualified-keyword?)) 54 | (defn ancestors 55 | "Like clojure.core/ancestors but scoped on our spec hierarchy" 56 | [tag] 57 | (clojure.core/ancestors @spec-hierarchy tag)) 58 | 59 | (s/fdef descendants 60 | :args (s/cat :tag qualified-keyword?)) 61 | (defn descendants 62 | "Like clojure.core/descendants but scoped on our spec hierarchy" 63 | [tag] 64 | (clojure.core/descendants @spec-hierarchy tag)) 65 | 66 | (s/fdef vary-meta! 67 | :args (s/cat :k qualified-keyword? 68 | :f ifn? 69 | :args (s/* any?)) 70 | :ret qualified-keyword?) 71 | (defn vary-meta! 72 | "Like clojure.core/vary-meta but for registered specs, mutates the 73 | meta in place, return the keyword spec" 74 | [k f & args] 75 | (swap! metadata-registry 76 | #(update % k 77 | (fn [m] 78 | (apply f m args)))) 79 | k) 80 | 81 | (s/fdef with-meta! 82 | :args (s/cat :k qualified-keyword? 83 | :meta any?) 84 | :ret ::metadata-registry-val) 85 | (defn with-meta! 86 | "Like clojure.core/with-meta but for registered specs, mutates the 87 | meta in place, return the keyword spec" 88 | [k m] 89 | (swap! metadata-registry 90 | #(assoc % k m)) 91 | k) 92 | 93 | (s/fdef meta 94 | :args (s/cat :k qualified-keyword? 95 | :merge-with-ancestors (s/? boolean?)) 96 | :ret any?) 97 | (defn meta 98 | "Like clojure.core/meta but for registered specs. 99 | If merge-with-ancestors? is set to true it will merge with the 100 | metadata from all parents (top to bottom)" 101 | ([k merge-with-ancestors?] 102 | (if merge-with-ancestors? 103 | (let [m @metadata-registry] 104 | (reduce 105 | (fn [m' k] 106 | (merge m' (get m k))) 107 | {} 108 | (conj (vec (ancestors k)) k))) 109 | (get @metadata-registry k))) 110 | 111 | ([k] 112 | (meta k false))) 113 | 114 | (s/fdef unregister-meta! 115 | :args (s/cat :k qualified-keyword?) 116 | :ret qualified-keyword?) 117 | (defn unregister-meta! 118 | "Unregister meta data for a spec" 119 | [k] 120 | (swap! metadata-registry dissoc k) 121 | k) 122 | 123 | (s/fdef with-doc 124 | :args (s/cat :k qualified-keyword? 125 | :doc string?) 126 | :ret qualified-keyword?) 127 | (defn with-doc 128 | "Add doc metadata on a registered spec" 129 | [k doc] 130 | (vary-meta! k assoc :doc doc)) 131 | 132 | (s/fdef doc 133 | :args (s/cat :k qualified-keyword?) 134 | :ret (s/nilable string?)) 135 | (defn doc 136 | "Returns doc associated with spec" 137 | [k] 138 | (some-> (meta k) :doc)) 139 | 140 | (s/fdef def-derived 141 | :args (s/cat :k qualified-keyword? 142 | :parent qualified-keyword?)) 143 | (defmacro def-derived 144 | "2 arg arity will define a new spec such that (s/def ::k ::parent) and 145 | define a relationship between the 2 with spex/derive such 146 | that: (spec/isa? k parent) => true. 147 | 3 arg arity will define the same relationship but instead of 148 | creating a simple spec alias it will create a new spec such that 149 | (s/def ::k (s/merge ::parent [specs...]). 150 | Parents derivation only works between registered specs." 151 | ([k parent] 152 | `(do 153 | (s/def ~k ~parent) 154 | (derive ~k ~parent) 155 | ~k))) 156 | 157 | (s/fdef def-merged 158 | :args (s/cat :k qualified-keyword? 159 | :parents (s/coll-of qualified-keyword?))) 160 | (defmacro def-merged 161 | [k parents] 162 | `(do 163 | (s/def ~k (s/merge ~@parents)) 164 | ~@(for [p parents 165 | ;; we can only derive from registered specs 166 | :when (qualified-keyword? p)] 167 | `(derive ~k ~p)) 168 | ~k)) 169 | -------------------------------------------------------------------------------- /test/qbits/spex/test/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns qbits.spex.test.core-test 2 | (:require 3 | [clojure.test :refer [deftest is]] 4 | [qbits.spex :as sx] 5 | [clojure.spec.alpha :as s])) 6 | 7 | (s/def ::foo string?) 8 | 9 | (deftest test-meta 10 | (is (= (sx/meta ::foo) nil)) 11 | 12 | (sx/with-meta! ::foo {:bar :baz}) 13 | (is (= (sx/meta ::foo) {:bar :baz})) 14 | 15 | (sx/vary-meta! ::foo assoc :bak :prout) 16 | (is (= (sx/meta ::foo) {:bak :prout 17 | :bar :baz})) 18 | 19 | (sx/def-derived ::bar ::foo) 20 | 21 | (is (= (sx/meta ::bar) nil)) 22 | (is (= (sx/meta ::bar true) 23 | {:bak :prout 24 | :bar :baz})) 25 | 26 | (sx/vary-meta! ::bar assoc :1 :2) 27 | (is (= (sx/meta ::bar true) 28 | {:bak :prout 29 | :bar :baz 30 | :1 :2}))) 31 | 32 | (deftest test-hierachy 33 | (s/def ::bak string?) 34 | (is (sx/isa? ::bar ::foo)) 35 | (is (not (sx/isa? ::foo ::baz))) 36 | 37 | (sx/def-derived ::bak ::bar) 38 | (is (sx/isa? ::bak ::foo)) 39 | (is (= (sx/ancestors ::bak) #{::bar ::foo})) 40 | (is (= (sx/descendants ::foo) #{::bar ::bak})) 41 | (is (= (sx/parents ::foo) nil)) 42 | (is (= (sx/parents ::bar) #{::foo})) 43 | 44 | (sx/underive ::baz ::bar) 45 | (is (= (sx/ancestors ::baz) nil))) 46 | 47 | (deftest test-merged 48 | (s/def ::a (s/keys)) 49 | (s/def ::b (s/keys)) 50 | (sx/def-merged ::m [::a ::b]) 51 | (is (= #{::a ::b} (sx/ancestors ::m)))) 52 | --------------------------------------------------------------------------------