├── tests.edn ├── bin └── kaocha.sh ├── .gitignore ├── deps.edn ├── .circleci └── config.yml ├── src └── provisdom │ └── spectomic │ ├── specs.clj │ └── core.clj ├── CHANGELOG.md ├── README.md ├── test └── provisdom │ └── spectomic │ └── core_test.clj └── LICENSE /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 {} 2 | -------------------------------------------------------------------------------- /bin/kaocha.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clojure -M:test -m kaocha.runner "$@" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml.asc 5 | *.jar 6 | *.class 7 | /.lein-* 8 | /.nrepl-port 9 | .nrepl-history 10 | .hgignore 11 | .hg/ 12 | *.impl 13 | .idea 14 | .envrc 15 | project.clj 16 | *.iml 17 | .nrepl-history 18 | .nrepl-port 19 | out 20 | .cpcache 21 | extra 22 | pom.xml 23 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {} 3 | :aliases {:test 4 | {:extra-paths ["test"] 5 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"} 6 | lambdaisland/kaocha {:mvn/version "1.64.1010"} 7 | datascript/datascript {:mvn/version "1.3.12"} 8 | com.datomic/datomic-free {:mvn/version "0.9.5697"}}} 9 | :build {:deps {io.github.seancorfield/build-clj {:git/tag "v0.8.0" :git/sha "9bd8b8a"}} 10 | :ns-default build}}} 11 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | defaults: &defaults 4 | docker: 5 | - image: cimg/clojure:1.10.3 6 | 7 | jobs: 8 | test: 9 | <<: *defaults 10 | 11 | steps: 12 | - checkout 13 | 14 | - restore_cache: 15 | keys: 16 | - project-{{ checksum "deps.edn" }} 17 | - project 18 | 19 | - run: 20 | name: Run tests 21 | command: ./bin/kaocha.sh 22 | 23 | - save_cache: 24 | paths: 25 | - ~/.m2 26 | - ~/.gitlibs 27 | - ./.cpcache 28 | key: project-{{ checksum "deps.edn" }} 29 | 30 | workflows: 31 | version: 2 32 | ci-workflow: 33 | jobs: 34 | - test 35 | -------------------------------------------------------------------------------- /src/provisdom/spectomic/specs.clj: -------------------------------------------------------------------------------- 1 | (ns provisdom.spectomic.specs 2 | (:require 3 | [clojure.spec.alpha :as s] 4 | [clojure.spec.gen.alpha :as gen] 5 | [clojure.string :as str] 6 | [clojure.spec.alpha :as s])) 7 | 8 | 9 | (def datomic-value-types 10 | #{:db.type/string :db.type/boolean :db.type/long :db.type/bigint :db.type/float :db.type/double :db.type/bigdec 11 | :db.type/instant :db.type/uuid :db.type/uri :db.type/keyword :db.type/bytes :db.type/ref}) 12 | 13 | (s/def ::tempid 14 | (s/with-gen 15 | (s/or 16 | :dbid #(instance? (Class/forName "datomic.db.DbId") %) 17 | :string (s/and string? #(not (str/starts-with? % ":")))) 18 | (fn [] (gen/return ((resolve 'datomic.api/tempid) :db.part/db))))) 19 | 20 | (s/def :db/id 21 | (s/or 22 | :entity-id number? 23 | :lookup-ref (s/tuple keyword? (s/or :string string? 24 | :keyword keyword? 25 | :num number? 26 | :uuid uuid?)) 27 | :tempid ::tempid 28 | :ident keyword?)) 29 | 30 | (s/def :db/ident keyword?) 31 | (s/def :db/valueType datomic-value-types) 32 | (s/def :db/cardinality #{:db.cardinality/one :db.cardinality/many}) 33 | (s/def :db/doc string?) 34 | (s/def :db/unique #{:db.unique/value :db.unique/identity}) 35 | (s/def :db/index boolean?) 36 | (s/def :db/isComponent boolean?) 37 | (s/def :db/noHistory boolean?) 38 | (s/def :db/fulltext boolean?) 39 | 40 | (s/def ::datascript-optional-field-schema (s/keys :opt [:db/doc :db/unique :db/index :db/isComponent])) 41 | (s/def ::datascript-schema 42 | (s/map-of keyword? ::datascript-optional-field-schema)) 43 | 44 | 45 | (s/def ::datomic-optional-field-schema 46 | (s/merge 47 | ::datascript-optional-field-schema 48 | (s/keys :opt [:db/id :db/fulltext :db/noHistory]))) 49 | 50 | (s/def ::datomic-field-schema 51 | (s/coll-of 52 | (s/merge 53 | ::datomic-optional-field-schema 54 | (s/keys :req [:db/ident :db/valueType :db/cardinality])))) 55 | 56 | (s/def ::schema-entry (s/or :att keyword? :att-and-schema (s/tuple keyword? map?))) 57 | 58 | (s/def ::custom-type-resolver (s/fspec :args (s/cat :object any?))) 59 | (s/def ::schema-options (s/keys :opt-un [::custom-type-resolver])) -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.7.11] - 2019-12-09 4 | ### Added 5 | - Added `datascript-schema-from-datomic-schema` 6 | 7 | ## [0.7.10] - 2019-12-09 8 | ### Changed 9 | - Switched from Boot to deps.edn 10 | - Switching from Travis to CircleCI 11 | 12 | ## [0.7.9] - 2019-01-10 13 | ### Added 14 | - Improve error handling when an exception occurs while sampling. 15 | - Allow `:gen-resize` to be passed as an option to `datomic-schema`. 16 | 17 | ## [0.7.8] - 2019-01-04 18 | ### Fixed 19 | - If :db/valueType is explicitly passed in the extra schema attributes, we should 20 | force that value type and not throw any value type related exceptions. [#16](https://github.com/Provisdom/spectomic/pull/16) 21 | 22 | ## [0.7.7] - 2018-06-14 23 | ### Added 24 | - Handle `s/and` in the form parser. 25 | 26 | ## [0.7.6] - 2018-04-09 27 | ### Added 28 | - Call `spec->datomic-schema` within the `(clojure.spec.alpha/coll-of 29 | clojure.spec.alpha/every)` case in `find-type-via-form` to further increase 30 | performance. 31 | 32 | ## [0.7.5] - 2018-03-21 33 | ### Added 34 | - Call `spec->datomic-schema` within `find-type-via-form` to further increase 35 | performance. 36 | 37 | ## [0.7.4] - 2018-03-21 38 | ### Added 39 | - Decrease schema generation time for some collections and maps by setting the 40 | type based on the Spec form. 41 | 42 | ## [0.7.3] - 2018-02-12 43 | ### Fixed 44 | - Include `:db/isComponent` in DataScript schema. 45 | 46 | ## [0.7.2] - 2017-11-01 47 | ### Fixed 48 | - The `datascript-schema` function was incorrectly dropping the `:db/identity` values from maps. 49 | 50 | ## [0.7.1] - 2017-10-31 51 | ### Fixed 52 | - Compatibility with Clojure 1.9.0-beta4 by using `decimal?` instead of `bigdec?`. 53 | ### Changed 54 | - Updated to `[org.clojure/spec.alpha "0.1.143"]`. 55 | 56 | ## [0.7.0] - 2017-10-31 57 | ### Breaking 58 | - Removed `:db/valueType` from DataScript schema except when it is `:db.type/ref`. 59 | ### Added 60 | - Ensure generated schema can be transacted into Datomic and DataScript. 61 | - Datomic free and DataScript as test level dependencies. 62 | 63 | ## [0.6.1] - 2017-10-30 64 | ### Fixed 65 | - Leftover reader conditional causing tests to fail. 66 | - Move org.clojure/spec.alpha back to 0.1.123. 67 | 68 | ## [0.6.0] - 2017-10-30 69 | ### Breaking 70 | - Made `core.cljc` and `specs.cljc` `.clj` files instead of `.cljc`. 71 | 72 | ## [0.5.0] - 2017-08-30 73 | ### Breaking 74 | - Removed `datomic-schema` and `datascript-schema` macros. 75 | - Changed `datomic-schema*` and `datascript-schema*` to `datomic-schema` and `datascript-schema`, 76 | respectively. 77 | 78 | ## [0.4.1] - 2017-08-08 79 | ### Fixed 80 | - The specs for `datomic-schema*` and `datascript-schema*` are now correct for the two arity case. 81 | 82 | ## [0.4.0] - 2017-08-08 83 | ### Breaking 84 | - API now expects specs to be passed in a vector 85 | ### Added 86 | - Options map for API that can be passed a function to resolve custom types (resolves [#5](https://github.com/Provisdom/spectomic/issues/5)) 87 | 88 | ## [0.3.1] - 2017-08-08 89 | ### Added 90 | - Spec for schema entries [#6](https://github.com/Provisdom/spectomic/pull/6) 91 | - Explicit error when schema entry does not conform to spec. [#6](https://github.com/Provisdom/spectomic/pull/6) 92 | ### Changed 93 | - Internal code structure 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spectomic 2 | [![CircleCI](https://circleci.com/gh/Provisdom/spectomic.svg?style=svg)](https://circleci.com/gh/Provisdom/spectomic) 3 | 4 | Generate Datomic or Datascript schema from your Clojure(script) specs. 5 | 6 | ## Installation 7 | 8 | [](dependency) 9 | ```clojure 10 | [provisdom/spectomic "1.0.78"] ;; latest release 11 | ``` 12 | [](/dependency) 13 | 14 | ## Usage 15 | 16 | Let's take a look at a basic example. 17 | 18 | ```clojure 19 | (require '[clojure.spec :as s] 20 | '[provisdom.spectomic.core :as spectomic]) 21 | => nil 22 | 23 | (s/def ::string string?) 24 | => :boot.user/string 25 | 26 | (spectomic/datomic-schema [::string]) 27 | => [{:db/ident :boot.user/string 28 | :db/valueType :db.type/string 29 | :db/cardinality :db.cardinality/one}] 30 | ``` 31 | 32 | In this example we define the spec `::string` and then pass it to `datomic-schema` 33 | which returns a collection of schema matching the data our `::string` spec 34 | represents. Now let's look at a more complicated example. 35 | 36 | ```clojure 37 | (s/def :entity/id uuid?) 38 | => :entity/id 39 | 40 | (s/def :user/name string?) 41 | => :user/name 42 | 43 | (s/def :user/favorite-foods (s/coll-of string?)) 44 | => :user/favorite-foods 45 | 46 | (s/def :order/name string?) 47 | => :order/name 48 | 49 | (s/def :user/order (s/keys :req [:entity/id :order/name])) 50 | => :user/orders 51 | 52 | (s/def :user/orders (s/coll-of :user/order)) 53 | => :user/orders 54 | 55 | (s/def ::user (s/keys :req [:entity/id :user/name :user/favorite-foods :user/orders])) 56 | => :boot.user/user 57 | 58 | (spectomic/datomic-schema [[:entity/id {:db/unique :db.unique/identity 59 | :db/index true}] 60 | :user/name 61 | :user/favorite-foods 62 | :user/orders]) 63 | => [{:db/ident :entity/id 64 | :db/valueType :db.type/uuid 65 | :db/cardinality :db.cardinality/one 66 | :db/unique :db.unique/identity 67 | :db/index true} 68 | {:db/ident :user/name 69 | :db/valueType :db.type/string 70 | :db/cardinality :db.cardinality/one} 71 | {:db/ident :user/favorite-foods 72 | :db/valueType :db.type/string 73 | :db/cardinality :db.cardinality/many} 74 | {:db/ident :user/orders 75 | :db/valueType :db.type/ref 76 | :db/cardinality :db.cardinality/many}] 77 | ``` 78 | 79 | In this example we have a `::user` entity that is uniquely identified by an 80 | `:entity/id`. The user also has a collection of his or her favorite foods 81 | `:user/favorite-foods` and a collection of orders `:user/orders`. 82 | 83 | We need to let `datomic-schema` know that `:entity/id` is unique. We do this be 84 | providing `datomic-schema` with a tuple instead of just the spec keyword. The 85 | first element of the tuple is the spec and the second element is any extra schema 86 | fields to be added to the attribute. In this case we attach `:db/unique` and 87 | `:db/index`. You can see in the outputted schema that `:entity/id` does indeed 88 | have those extra fields. 89 | 90 | `:user/name` is not particularly interesting as it is similar to our basic example. 91 | 92 | `:user/favorite-foods` is represented as a collection of strings in our example. 93 | And as you can see in the returned schema, `:user/favorite-foods` is 94 | `:db.cardinality/many` and `:db.type/string`. 95 | 96 | `:user/orders` is a collection of maps of the for dictated by the spec `::orders`. 97 | And our returned schema is of type `:db.type/ref` and `:db.cardinality/many`. 98 | 99 | ### Resolving Custom Types 100 | Your code may use types that are not resolvable to Datomic types with the default 101 | type type resolver implementation. One option to this problem is to use the schema 102 | entry format you saw above with `:entity/id`. If you recall, we set the `:db/unique` 103 | property for `:entity/id` to `:db.unique/identity`. You can actually manually set 104 | the `:db/valueType` too. This could, however, become very repetitive. 105 | 106 | The second option is to pass a map with the `:custom-type-resolver` key set to a 107 | function that returns a Datomic type. If the default type resolver cannot resolve 108 | an object's type, your function will be called with the object passed to it as 109 | the only argument. Your function is expected to return a valid Datomic type, as 110 | defined [here](http://docs.datomic.com/schema.html#required-schema-attributes). 111 | 112 | ## Usage in Production 113 | 114 | This library provides two functions for generating schema from specs: `datomic-schema` 115 | and `datascript-schema`. Both function calls occur at **runtime**. This means 116 | that if you include this library as a runtime dependency, test.check will also be 117 | included. Often you don't want test.check as a runtime dependency so it is suggested 118 | that you include this dependency as `test` scope 119 | (e.g. `[provisdom/spectomic "x.x.x" :scope "test"]`). Including this library with 120 | `test` scope means that all your calls need to happen at compile time or elsewhere. 121 | Here are two examples of how this library can be used at compile time. 122 | 123 | ### Macro 124 | 125 | It is convenient to store the list of specs you want to convert into schema as a 126 | var. By doing so you are able to easily add new specs to the schema list at any 127 | time. We will use a `def`'ed var as an example. 128 | 129 | ```clojure 130 | (def schema-specs [[:entity/id {:db/unique :db.unique/identity :db/index true}] :user/name :user/favorite-foods]) 131 | ``` 132 | 133 | Now let's write a `def`-like macro that will generate our schema at compile time: 134 | 135 | ```clojure 136 | (defmacro defschema 137 | [name] 138 | `(def ~name (spectomic/datomic-schema schema-specs))) 139 | 140 | (defschema my-schema) 141 | ``` 142 | 143 | Now our schema is statically compiled and available in the var `my-schema`. 144 | 145 | ### Build time 146 | 147 | Sometimes you may want to save your schema into an actual EDN file for use in 148 | multiple places. This can be easily accomplished with a Boot task. 149 | 150 | ```clojure 151 | (require '[provisdom.spectomic.core :as spectomic]) 152 | 153 | (deftask generate-schema 154 | [s sym VAL sym "This symbol will be resolved and have its content passed to `datomic-schema`."] 155 | (spit "my-schema.edn" (spectomic/datomic-schema @(resolve sym)))) 156 | ``` 157 | 158 | This task is simple but does not adhere to Boot's design patterns. Here's a 159 | slightly more complex example that integrates well with other tasks. 160 | 161 | ```clojure 162 | (require '[clojure.java.io :as io]) 163 | 164 | (deftask generate-schema 165 | [s sym VAL sym "This symbol will be resolved and have its content passed to `datomic-schema`." 166 | o out-file VAL str "Name of the file for the schema to be outputted to. Defaults to schema.edn"] 167 | (let [specs-var (resolve sym)] 168 | (assert specs-var "The symbol provided cannot be resolved.") 169 | (with-pre-wrap fileset 170 | (let [out-file-name (or out-file "schema.edn") 171 | out-dir (tmp-dir!) 172 | out-file (io/file out-dir out-file-name)] 173 | (spit out-file (spectomic/datomic-schema @specs-var)) 174 | (commit! (add-resource fileset out-dir)))))) 175 | ``` 176 | 177 | ## Caveats 178 | 179 | ### Misleading Generators 180 | 181 | When writing your specs you need to be mindful of the generator that is used for 182 | the spec. One misleading predicate generator is `float?`. `float?`'s generator 183 | actually uses the same generator as `double?`, meaning it does not return a number 184 | with type `java.lang.Float`. This is problematic for our schema generator as it 185 | relies on your objects having the correct type that they represent. This is not 186 | a bug in Clojure spec however. If you look at how `float?` is defined you will 187 | see that `float?` returns true if the object is a `java.lang.Double` or a 188 | `java.lang.Float`. To combat this we can write our own `::float` spec like this: 189 | 190 | ```clojure 191 | (s/def ::float 192 | (s/with-gen 193 | #(instance? java.lang.Float %) 194 | #(gen/fmap float 195 | (s/gen (s/double-in :min Float/MIN_VALUE :max Float/MAX_VALUE :infinite? false :NaN? false))))) 196 | ``` 197 | 198 | ### Rare Edge Cases 199 | 200 | If you write a Spec that uses a generator that will return values of a different 201 | type very rarely then you will run into schema generation problems. Make sure 202 | your generators are returning consistent types. 203 | 204 | ## Implementation 205 | 206 | Rather than parsing every Spec form, we chose to implement this library using 207 | generators. Every Spec that is passed to `datomic-schema` or `datascript-schema` 208 | is sampled 100 times using test.check. The type of each sample is then matched 209 | with a Datomic type. Next we make sure that each sample is of the same type. If 210 | your generator is returning multiple types for a Spec then it's not clear how the 211 | schema should be generated so an error is thrown. If the type is a collection 212 | then we need to verify that for each sample, every element in the collection is 213 | of the same type. If that is true then we return a map with `:db.cardinality/many` 214 | and the `:db/valueType` set to the type of object the collection contains. 215 | 216 | Because generating samples for specs can end up taking a significant amount of 217 | time, we do some Spec form parsing up front to try and determine the Datomic 218 | type. If the Spec form is not handled then we fall back on generation. 219 | 220 | ## License 221 | 222 | Copyright © 2017 Provisdom 223 | 224 | Distributed under the Eclipse Public License either version 1.0 or (at 225 | your option) any later version. 226 | -------------------------------------------------------------------------------- /src/provisdom/spectomic/core.clj: -------------------------------------------------------------------------------- 1 | (ns provisdom.spectomic.core 2 | (:require 3 | [clojure.spec.alpha :as s] 4 | [clojure.spec.gen.alpha :as sgen] 5 | [provisdom.spectomic.specs :as spectomic]) 6 | (:import (clojure.lang ExceptionInfo))) 7 | 8 | ;; this could be a multimethod? 9 | (def ^:private class->datomic-type 10 | {java.lang.String :db.type/string 11 | java.lang.Boolean :db.type/boolean 12 | java.lang.Double :db.type/double 13 | java.lang.Long :db.type/long 14 | java.lang.Float :db.type/float 15 | java.util.Date :db.type/instant 16 | java.util.UUID :db.type/uuid 17 | java.math.BigDecimal :db.type/bigdec 18 | java.math.BigInteger :db.type/bigint 19 | java.net.URI :db.type/uri 20 | clojure.lang.Keyword :db.type/keyword}) 21 | 22 | (def datomic-types (conj (set (vals class->datomic-type)) :db.type/bytes :db.type/ref)) 23 | 24 | (defn datomic-type? 25 | [x] 26 | "Returns true if `x` is a datomic type." 27 | (some? (datomic-types x))) 28 | 29 | (defn- obj->datomic-type 30 | [obj custom-type-resolver] 31 | (let [t (type obj)] 32 | (cond 33 | (contains? class->datomic-type t) (class->datomic-type t) 34 | (map? obj) :db.type/ref 35 | (nil? obj) nil 36 | (= (Class/forName "[B") (.getClass obj)) :db.type/bytes 37 | :else (custom-type-resolver obj)))) 38 | 39 | (defn sample-types 40 | "Returns a set of all the datomic types `samples` contains." 41 | [samples custom-type-resolver] 42 | (into #{} 43 | (comp 44 | (map (fn [sample] 45 | (if (or (sequential? sample) (set? sample)) 46 | ::cardinality-many 47 | (obj->datomic-type sample custom-type-resolver)))) 48 | ;; we need to remove nils for the cases where a spec is nilable. 49 | (filter some?)) 50 | samples)) 51 | 52 | (defn spec-form 53 | [keyword-or-form] 54 | (if (keyword? keyword-or-form) 55 | (s/form keyword-or-form) 56 | keyword-or-form)) 57 | 58 | (defn find-type-via-generation 59 | "Returns Datomic schema for `spec`. Takes a map of the following options: 60 | :custom-type-resolver - A function that takes an object and returns the Datomic value type. 61 | :value-type - Force the value type for a spec to be this type. This is useful 62 | for cases where your spec generates multiple types and your code 63 | will work with multiple types (i.e. long and double)." 64 | [spec {:keys [custom-type-resolver value-type gen-resize] 65 | :or {gen-resize 10}}] 66 | (let [g (sgen/such-that (fn [s] 67 | ;; we need a sample that is not nil 68 | (and (some? s) 69 | ;; if the sample is a collection, then we need a collection that is not empty. 70 | ;; we cannot generate Datomic schema with an empty collection. 71 | (if (coll? s) 72 | (not-empty s) 73 | true))) 74 | (s/gen spec)) 75 | samples (binding [s/*recursion-limit* 1] 76 | (try 77 | (doall (sgen/sample ((resolve 'clojure.test.check.generators/resize) gen-resize g) 100)) 78 | (catch ExceptionInfo ex 79 | (throw (ex-info (.getMessage ex) (merge (ex-data ex) 80 | {:spec spec})))))) 81 | types (sample-types samples custom-type-resolver) 82 | cardinality-many? (= #{::cardinality-many} types)] 83 | ;; if we already know the value type, we don't need to do anything else 84 | (if value-type 85 | {:db/valueType value-type 86 | :db/cardinality (if cardinality-many? :db.cardinality/many :db.cardinality/one)} 87 | 88 | ;; Given we don't know the value type, this makes sure we are getting 89 | ;; consistent types from the generator. If types are inconsistent then schema 90 | ;; generation is unclear. 91 | (cond 92 | (> (count types) 1) (throw (ex-info "Spec resolves to multiple types." {:spec spec :types types})) 93 | (empty? types) (throw (ex-info "No matching Datomic types." {:spec spec})) 94 | :else 95 | (let [t (first types)] 96 | (cond 97 | cardinality-many? (let [collection-types (sample-types (mapcat identity samples) custom-type-resolver)] 98 | (cond 99 | (> (count collection-types) 1) 100 | (throw (ex-info "Spec collection contains multiple types." 101 | {:spec spec :types collection-types})) 102 | (= ::cardinality-many (first collection-types)) 103 | (throw (ex-info "Cannot create schema for a collection of collections." 104 | {:spec spec})) 105 | :else {:db/valueType (first collection-types) 106 | :db/cardinality :db.cardinality/many})) 107 | (datomic-type? t) {:db/valueType t 108 | :db/cardinality :db.cardinality/one} 109 | :else (throw (ex-info "Invalid Datomic type." {:spec spec :type t})))))))) 110 | 111 | (declare spec->datomic-schema) 112 | 113 | (defn find-type-via-form 114 | ([spec] (find-type-via-form spec nil)) 115 | ([spec custom-type-resolver] 116 | (let [form (spec-form spec)] 117 | (when (sequential? form) 118 | (case (first form) 119 | (clojure.spec.alpha/keys clojure.spec.alpha/merge) 120 | {:db/valueType :db.type/ref 121 | :db/cardinality :db.cardinality/one} 122 | 123 | (clojure.spec.alpha/coll-of 124 | clojure.spec.alpha/every) 125 | (let [inner-type (spec->datomic-schema (eval (second form)) {:custom-type-resolver custom-type-resolver})] 126 | (when (= :db.cardinality/one (:db/cardinality inner-type)) 127 | {:db/cardinality :db.cardinality/many 128 | :db/valueType (:db/valueType inner-type)})) 129 | 130 | clojure.spec.alpha/and 131 | (let [inner-forms (rest form) 132 | found-schemas (reduce (fn [schemas form] 133 | (if-let [schema (find-type-via-form form)] 134 | (conj schemas schema) 135 | schemas)) 136 | [] inner-forms)] 137 | ;; if we found multiple schemas this likely means that the Spec represents 138 | ;; multiple types. We'll let the find-type-via-generation throw the exception. 139 | (if (= 1 (count found-schemas)) 140 | (first found-schemas) 141 | nil)) 142 | 143 | clojure.spec.alpha/nilable 144 | (spec->datomic-schema (eval (second form))) 145 | 146 | nil))))) 147 | 148 | (defn- spec->datomic-schema 149 | "Returns Datomic schema for `spec`." 150 | ([spec] (spec->datomic-schema spec nil)) 151 | ([spec {:keys [custom-type-resolver] :as lookup-opts}] 152 | (if-let [t (find-type-via-form spec custom-type-resolver)] 153 | t 154 | (find-type-via-generation spec lookup-opts)))) 155 | 156 | (defn- spec-and-data 157 | [s] 158 | (let [c (s/conform ::spectomic/schema-entry s)] 159 | (if (= ::s/invalid c) 160 | (throw (ex-info "Invalid schema entry" {:data s})) 161 | (let [[b s] c] 162 | (condp = b 163 | :att [s {}] 164 | :att-and-schema s))))) 165 | 166 | (defn datomic-schema 167 | ([specs] (datomic-schema specs nil)) 168 | ([specs {:keys [custom-type-resolver] 169 | :or {custom-type-resolver type}}] 170 | (into [] 171 | (map (fn [s] 172 | (let [[spec extra-schema] (spec-and-data s) 173 | base-schema (if (and (:db/valueType extra-schema) 174 | (:db/cardinality extra-schema)) 175 | ;; if the user explicitly specifies the type and cardinality, 176 | ;; we can skip the call to spec->datomic-schema 177 | {} 178 | (spec->datomic-schema spec {:custom-type-resolver custom-type-resolver 179 | :value-type (:db/valueType extra-schema)}))] 180 | (merge {:db/ident spec} 181 | base-schema 182 | extra-schema)))) 183 | specs))) 184 | 185 | (s/fdef datomic-schema 186 | :args (s/cat :specs 187 | (s/coll-of 188 | (s/or :spec qualified-keyword? 189 | :tuple (s/tuple qualified-keyword? ::spectomic/datomic-optional-field-schema))) 190 | :opts (s/? (s/nilable ::spectomic/schema-options))) 191 | :ret ::spectomic/datomic-field-schema) 192 | 193 | (defn datascript-schema-from-datomic-schema 194 | [datomic-schema] 195 | (reduce (fn [ds-schema schema] 196 | (assoc ds-schema 197 | (:db/ident schema) 198 | (let [schema (select-keys schema [:db/cardinality :db/unique :db/valueType :db/isComponent])] 199 | ;; only include :db/valueType when it is a ref. 200 | (if (= :db.type/ref (:db/valueType schema)) 201 | schema 202 | (dissoc schema :db/valueType))))) 203 | {} datomic-schema)) 204 | 205 | (defn datascript-schema 206 | ([specs] (datascript-schema specs nil)) 207 | ([specs opts] 208 | (let [s (datomic-schema specs opts)] 209 | (datascript-schema-from-datomic-schema s)))) 210 | 211 | (s/fdef datascript-schema 212 | :args (s/cat :specs 213 | (s/coll-of 214 | (s/or :spec qualified-keyword? 215 | :tuple (s/tuple qualified-keyword? ::spectomic/datascript-optional-field-schema))) 216 | :opts (s/? (s/nilable ::spectomic/schema-options))) 217 | :ret ::spectomic/datascript-schema) -------------------------------------------------------------------------------- /test/provisdom/spectomic/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns provisdom.spectomic.core-test 2 | (:require 3 | [clojure.test :refer :all] 4 | [clojure.spec.alpha :as s] 5 | [clojure.spec.gen.alpha :as gen] 6 | [clojure.spec.test.alpha :as st] 7 | [datomic.api :as d] 8 | [datascript.core :as ds] 9 | [provisdom.spectomic.core :as spectomic])) 10 | 11 | (st/instrument) 12 | 13 | ;; basic Datomic types 14 | (s/def ::string string?) 15 | (s/def ::int int?) 16 | (s/def ::inst inst?) 17 | (s/def ::double double?) 18 | ;; the default generator for float? will generate Doubles instead of Floats so we need our own 19 | ;; generator and predicate function. 20 | (s/def ::float 21 | (s/with-gen 22 | #(instance? java.lang.Float %) 23 | #(gen/fmap float 24 | (s/gen (s/double-in :min Float/MIN_VALUE :max Float/MAX_VALUE :infinite? false :NaN? false))))) 25 | (s/def ::uuid uuid?) 26 | (s/def ::bigdec decimal?) 27 | (s/def ::bigint (s/with-gen 28 | #(instance? java.math.BigInteger %) 29 | (fn [] 30 | (gen/fmap #(BigInteger/valueOf %) 31 | (s/gen int?))))) 32 | (s/def ::uri uri?) 33 | (s/def ::keyword keyword?) 34 | (s/def ::bytes bytes?) 35 | 36 | (s/def ::nilable-int (s/nilable int?)) 37 | 38 | (s/def ::int-coll (s/coll-of ::int)) 39 | (s/def ::nilable-int-coll (s/coll-of ::nilable-int)) 40 | 41 | (s/def ::map (s/keys :req [::int ::string])) 42 | (s/def ::map-and (s/and map? ::map)) 43 | (s/def ::map-and-keys (s/and map? (s/keys :req [::int]))) 44 | (s/def ::map-and-two-types (s/and (s/keys :req [::int]) (s/coll-of int?))) 45 | (s/def ::map-coll-of-and (s/and ::nilable-map-coll sequential?)) 46 | (s/def ::map2 (s/keys :req [::keyword])) 47 | (s/def ::merged-map (s/merge ::map ::map2)) 48 | (s/def ::nilable-map (s/nilable ::map)) 49 | (s/def ::nilable-map-verbose (s/nilable (s/keys :req [::int ::string]))) 50 | (s/def ::nilable-map-coll (s/coll-of ::nilable-map)) 51 | (s/def ::nilable-map-coll-verbose (s/coll-of (s/nilable (s/keys :req [::int])))) 52 | 53 | ;; these specs will fail schema generation 54 | (s/def ::nil nil?) 55 | (s/def ::or (s/or :string string? :num int?)) 56 | (s/def ::or-coll (s/coll-of ::or)) 57 | (s/def ::coll-of-coll (s/coll-of (s/coll-of string?))) 58 | (defrecord MyObject []) 59 | (s/def ::myobject (s/with-gen #(instance? MyObject %) 60 | #(gen/return (MyObject.)))) 61 | 62 | (defn custom-resolver 63 | [v] 64 | (if (instance? java.time.LocalDate v) 65 | :db.type/instant 66 | (type v))) 67 | 68 | (s/def ::date (s/with-gen 69 | #(instance? java.time.LocalDate %) 70 | #(gen/return (java.time.LocalDate/ofEpochDay (rand-int 100000))))) 71 | 72 | (s/def ::date-coll (s/coll-of ::date)) 73 | 74 | (deftest find-type-via-form-test 75 | (let [ref-one {:db/valueType :db.type/ref 76 | :db/cardinality :db.cardinality/one} 77 | ref-many {:db/valueType :db.type/ref 78 | :db/cardinality :db.cardinality/many}] 79 | (is (= ref-one (spectomic/find-type-via-form ::map))) 80 | (is (= ref-one (spectomic/find-type-via-form ::merged-map))) 81 | (is (= ref-one (spectomic/find-type-via-form ::nilable-map))) 82 | (is (= ref-one (spectomic/find-type-via-form ::nilable-map-verbose))) 83 | (is (= ref-many (spectomic/find-type-via-form ::nilable-map-coll))) 84 | (is (= ref-many (spectomic/find-type-via-form ::nilable-map-coll-verbose))) 85 | (is (= ref-one (spectomic/find-type-via-form `(s/and coll? ::map map?)))) 86 | (is (= ref-one (spectomic/find-type-via-form ::map-and))) 87 | (is (= ref-one (spectomic/find-type-via-form ::map-and-keys))) 88 | (is (= ref-many (spectomic/find-type-via-form ::map-coll-of-and))) 89 | (is (nil? (spectomic/find-type-via-form ::keyword))) 90 | (is (nil? (spectomic/find-type-via-form ::coll-of-coll))) 91 | (is (nil? (spectomic/find-type-via-form (fn [])))) 92 | (is (nil? (spectomic/find-type-via-form ::map-and-two-types))))) 93 | 94 | (deftest find-type-via-generation-test 95 | (is (= {:db/valueType :db.type/ref 96 | :db/cardinality :db.cardinality/one} 97 | (spectomic/find-type-via-generation ::map nil)))) 98 | 99 | (deftest datomic-schema-test 100 | (testing "basic datomic types" 101 | (is (= [{:db/ident ::string 102 | :db/valueType :db.type/string 103 | :db/cardinality :db.cardinality/one} 104 | {:db/ident ::int 105 | :db/valueType :db.type/long 106 | :db/cardinality :db.cardinality/one} 107 | {:db/ident ::inst 108 | :db/valueType :db.type/instant 109 | :db/cardinality :db.cardinality/one} 110 | {:db/ident ::double 111 | :db/valueType :db.type/double 112 | :db/cardinality :db.cardinality/one} 113 | {:db/ident ::float 114 | :db/valueType :db.type/float 115 | :db/cardinality :db.cardinality/one} 116 | {:db/ident ::uuid 117 | :db/valueType :db.type/uuid 118 | :db/cardinality :db.cardinality/one} 119 | {:db/ident ::bigdec 120 | :db/valueType :db.type/bigdec 121 | :db/cardinality :db.cardinality/one} 122 | {:db/ident ::bigint 123 | :db/valueType :db.type/bigint 124 | :db/cardinality :db.cardinality/one} 125 | {:db/ident ::uri 126 | :db/valueType :db.type/uri 127 | :db/cardinality :db.cardinality/one} 128 | {:db/ident ::keyword 129 | :db/valueType :db.type/keyword 130 | :db/cardinality :db.cardinality/one} 131 | {:db/ident ::bytes 132 | :db/valueType :db.type/bytes 133 | :db/cardinality :db.cardinality/one}] 134 | (spectomic/datomic-schema [::string ::int ::inst ::double ::float ::uuid 135 | ::bigdec ::bigint ::uri ::keyword ::bytes])))) 136 | (testing ":db.type/ref" 137 | (is (= [{:db/ident ::map 138 | :db/valueType :db.type/ref 139 | :db/cardinality :db.cardinality/one} 140 | {:db/ident ::nilable-map 141 | :db/valueType :db.type/ref 142 | :db/cardinality :db.cardinality/one}] 143 | (spectomic/datomic-schema [::map ::nilable-map])))) 144 | 145 | (testing ":db.cardinality/many" 146 | (is (= [{:db/ident ::int-coll 147 | :db/valueType :db.type/long 148 | :db/cardinality :db.cardinality/many} 149 | {:db/ident ::nilable-int-coll 150 | :db/valueType :db.type/long 151 | :db/cardinality :db.cardinality/many}] 152 | (spectomic/datomic-schema [::int-coll ::nilable-int-coll])))) 153 | 154 | (testing "custom type resolvers" 155 | (is (= [{:db/ident ::date 156 | :db/valueType :db.type/instant 157 | :db/cardinality :db.cardinality/one} 158 | {:db/ident ::date-coll 159 | :db/valueType :db.type/instant 160 | :db/cardinality :db.cardinality/many}] 161 | (spectomic/datomic-schema [::date ::date-coll] 162 | {:custom-type-resolver custom-resolver})))) 163 | 164 | (testing "extra schema attrs" 165 | (is (= [{:db/ident ::int 166 | :db/valueType :db.type/long 167 | :db/cardinality :db.cardinality/one 168 | :db/index true 169 | :db/unique :db.unique/identity}] 170 | (spectomic/datomic-schema [[::int {:db/index true :db/unique :db.unique/identity}]])))) 171 | 172 | (testing "extra schema attrs still generates spec if only valueType is specified." 173 | (is (= [{:db/ident ::int 174 | :db/valueType :db.type/long 175 | :db/cardinality :db.cardinality/one}] 176 | (spectomic/datomic-schema [[::int {:db/valueType :db.type/long}]])))) 177 | 178 | (testing "extra schema attrs skips generation if both valueType and cardinality are passed." 179 | (is (= [{:db/ident ::int 180 | :db/valueType :db.type/long 181 | :db/cardinality :db.cardinality/one}] 182 | (spectomic/datomic-schema [[::int {:db/valueType :db.type/long 183 | :db/cardinality :db.cardinality/one}]])))) 184 | 185 | (are [spec] (thrown? clojure.lang.ExceptionInfo (spectomic/datomic-schema [spec])) 186 | ::nil ::or ::or-coll ::coll-of-coll ::myobject)) 187 | 188 | (deftest datascript-schema-test 189 | (are [schema specs] (= schema (spectomic/datascript-schema specs)) 190 | {::double {:db/cardinality :db.cardinality/one} 191 | ::inst {:db/cardinality :db.cardinality/one} 192 | ::int {:db/cardinality :db.cardinality/one} 193 | ::bigint {:db/cardinality :db.cardinality/one} 194 | ::float {:db/cardinality :db.cardinality/one} 195 | ::string {:db/cardinality :db.cardinality/one} 196 | ::keyword {:db/cardinality :db.cardinality/one} 197 | ::bigdec {:db/cardinality :db.cardinality/one} 198 | ::bytes {:db/cardinality :db.cardinality/one} 199 | ::uri {:db/cardinality :db.cardinality/one} 200 | ::uuid {:db/cardinality :db.cardinality/one} 201 | ::int-coll {:db/cardinality :db.cardinality/many} 202 | ::map {:db/cardinality :db.cardinality/one 203 | :db/valueType :db.type/ref}} 204 | [::string ::int ::inst ::double ::float ::uuid 205 | ::bigdec ::bigint ::uri ::keyword ::bytes ::int-coll 206 | ::map] 207 | {::int {:db/cardinality :db.cardinality/one 208 | :db/unique :db.unique/identity 209 | :db/isComponent true}} 210 | [[::int {:db/index true 211 | :db/unique :db.unique/identity 212 | :db/isComponent true}]])) 213 | 214 | (deftest datomic-schema-valueType-test 215 | (is (= [{:db/ident ::string 216 | :db/valueType :db.type/keyword 217 | :db/cardinality :db.cardinality/one}] 218 | (spectomic/datomic-schema 219 | [[::string {:db/valueType :db.type/keyword :db/cardinality :db.cardinality/one}]])))) 220 | 221 | (def test-schema-specs 222 | [::string ::int ::inst ::double ::float ::uuid 223 | ::bigdec ::bigint ::uri ::keyword ::bytes 224 | ::map ::nilable-map 225 | ::int-coll ::nilable-int-coll 226 | [::int {:db/index true :db/unique :db.unique/identity}]]) 227 | 228 | (deftest datomic-schema-transaction-test 229 | (let [db-uri "datomic:mem://test" 230 | _ (d/create-database db-uri) 231 | conn (d/connect db-uri) 232 | schema (spectomic/datomic-schema test-schema-specs)] 233 | (testing "able to transact Spectomic generated schema to Datomic" 234 | (is @(d/transact conn schema))))) 235 | 236 | (deftest datascript-schema-transaction-test 237 | (let [schema (spectomic/datascript-schema test-schema-specs)] 238 | (testing "able to transact Spectomic generated schema to DataScript" 239 | (is (ds/create-conn schema))))) 240 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 1.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF 5 | THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial code and 12 | documentation distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | 16 | i) changes to the Program, and 17 | 18 | ii) additions to the Program; 19 | 20 | where such changes and/or additions to the Program originate from and 21 | are distributed by that particular Contributor. A Contribution 22 | 'originates' from a Contributor if it was added to the Program by such 23 | Contributor itself or anyone acting on such Contributor's 24 | behalf. Contributions do not include additions to the Program which: 25 | (i) are separate modules of software distributed in conjunction with 26 | the Program under their own license agreement, and (ii) are not 27 | derivative works of the Program. 28 | 29 | "Contributor" means any person or entity that distributes the Program. 30 | 31 | "Licensed Patents" mean patent claims licensable by a Contributor 32 | which are necessarily infringed by the use or sale of its Contribution 33 | alone or when combined with the Program. 34 | 35 | "Program" means the Contributions distributed in accordance with this 36 | Agreement. 37 | 38 | "Recipient" means anyone who receives the Program under this 39 | Agreement, including all Contributors. 40 | 41 | 2. GRANT OF RIGHTS 42 | 43 | a) Subject to the terms of this Agreement, each Contributor hereby 44 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 45 | license to reproduce, prepare derivative works of, publicly display, 46 | publicly perform, distribute and sublicense the Contribution of such 47 | Contributor, if any, and such derivative works, in source code and 48 | object code form. 49 | 50 | b) Subject to the terms of this Agreement, each Contributor hereby 51 | grants Recipient a non-exclusive, worldwide, royalty-free patent 52 | license under Licensed Patents to make, use, sell, offer to sell, 53 | import and otherwise transfer the Contribution of such Contributor, if 54 | any, in source code and object code form. This patent license shall 55 | apply to the combination of the Contribution and the Program if, at 56 | the time the Contribution is added by the Contributor, such addition 57 | of the Contribution causes such combination to be covered by the 58 | Licensed Patents. The patent license shall not apply to any other 59 | combinations which include the Contribution. No hardware per se is 60 | licensed hereunder. 61 | 62 | c) Recipient understands that although each Contributor grants the 63 | licenses to its Contributions set forth herein, no assurances are 64 | provided by any Contributor that the Program does not infringe the 65 | patent or other intellectual property rights of any other entity. Each 66 | Contributor disclaims any liability to Recipient for claims brought by 67 | any other entity based on infringement of intellectual property rights 68 | or otherwise. As a condition to exercising the rights and licenses 69 | granted hereunder, each Recipient hereby assumes sole responsibility 70 | to secure any other intellectual property rights needed, if any. For 71 | example, if a third party patent license is required to allow 72 | Recipient to distribute the Program, it is Recipient's responsibility 73 | to acquire that license before distributing the Program. 74 | 75 | d) Each Contributor represents that to its knowledge it has sufficient 76 | copyright rights in its Contribution, if any, to grant the copyright 77 | license set forth in this Agreement. 78 | 79 | 3. REQUIREMENTS 80 | 81 | A Contributor may choose to distribute the Program in object code form 82 | under its own license agreement, provided that: 83 | 84 | a) it complies with the terms and conditions of this Agreement; and 85 | 86 | b) its license agreement: 87 | 88 | i) effectively disclaims on behalf of all Contributors all warranties 89 | and conditions, express and implied, including warranties or 90 | conditions of title and non-infringement, and implied warranties or 91 | conditions of merchantability and fitness for a particular purpose; 92 | 93 | ii) effectively excludes on behalf of all Contributors all liability 94 | for damages, including direct, indirect, special, incidental and 95 | consequential damages, such as lost profits; 96 | 97 | iii) states that any provisions which differ from this Agreement are 98 | offered by that Contributor alone and not by any other party; and 99 | 100 | iv) states that source code for the Program is available from such 101 | Contributor, and informs licensees how to obtain it in a reasonable 102 | manner on or through a medium customarily used for software exchange. 103 | 104 | When the Program is made available in source code form: 105 | 106 | a) it must be made available under this Agreement; and 107 | 108 | b) a copy of this Agreement must be included with each copy of the Program. 109 | 110 | Contributors may not remove or alter any copyright notices contained 111 | within the Program. 112 | 113 | Each Contributor must identify itself as the originator of its 114 | Contribution, if any, in a manner that reasonably allows subsequent 115 | Recipients to identify the originator of the Contribution. 116 | 117 | 4. COMMERCIAL DISTRIBUTION 118 | 119 | Commercial distributors of software may accept certain 120 | responsibilities with respect to end users, business partners and the 121 | like. While this license is intended to facilitate the commercial use 122 | of the Program, the Contributor who includes the Program in a 123 | commercial product offering should do so in a manner which does not 124 | create potential liability for other Contributors. Therefore, if a 125 | Contributor includes the Program in a commercial product offering, 126 | such Contributor ("Commercial Contributor") hereby agrees to defend 127 | and indemnify every other Contributor ("Indemnified Contributor") 128 | against any losses, damages and costs (collectively "Losses") arising 129 | from claims, lawsuits and other legal actions brought by a third party 130 | against the Indemnified Contributor to the extent caused by the acts 131 | or omissions of such Commercial Contributor in connection with its 132 | distribution of the Program in a commercial product offering. The 133 | obligations in this section do not apply to any claims or Losses 134 | relating to any actual or alleged intellectual property 135 | infringement. In order to qualify, an Indemnified Contributor must: a) 136 | promptly notify the Commercial Contributor in writing of such claim, 137 | and b) allow the Commercial Contributor tocontrol, and cooperate with 138 | the Commercial Contributor in, the defense and any related settlement 139 | negotiations. The Indemnified Contributor may participate in any such 140 | claim at its own expense. 141 | 142 | For example, a Contributor might include the Program in a commercial 143 | product offering, Product X. That Contributor is then a Commercial 144 | Contributor. If that Commercial Contributor then makes performance 145 | claims, or offers warranties related to Product X, those performance 146 | claims and warranties are such Commercial Contributor's responsibility 147 | alone. Under this section, the Commercial Contributor would have to 148 | defend claims against the other Contributors related to those 149 | performance claims and warranties, and if a court requires any other 150 | Contributor to pay any damages as a result, the Commercial Contributor 151 | must pay those damages. 152 | 153 | 5. NO WARRANTY 154 | 155 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 156 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 157 | KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY 158 | WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 159 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 160 | responsible for determining the appropriateness of using and 161 | distributing the Program and assumes all risks associated with its 162 | exercise of rights under this Agreement , including but not limited to 163 | the risks and costs of program errors, compliance with applicable 164 | laws, damage to or loss of data, programs or equipment, and 165 | unavailability or interruption of operations. 166 | 167 | 6. DISCLAIMER OF LIABILITY 168 | 169 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR 170 | ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 171 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 172 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 173 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 174 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 175 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 176 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 177 | 178 | 7. GENERAL 179 | 180 | If any provision of this Agreement is invalid or unenforceable under 181 | applicable law, it shall not affect the validity or enforceability of 182 | the remainder of the terms of this Agreement, and without further 183 | action by the parties hereto, such provision shall be reformed to the 184 | minimum extent necessary to make such provision valid and enforceable. 185 | 186 | If Recipient institutes patent litigation against any entity 187 | (including a cross-claim or counterclaim in a lawsuit) alleging that 188 | the Program itself (excluding combinations of the Program with other 189 | software or hardware) infringes such Recipient's patent(s), then such 190 | Recipient's rights granted under Section 2(b) shall terminate as of 191 | the date such litigation is filed. 192 | 193 | All Recipient's rights under this Agreement shall terminate if it 194 | fails to comply with any of the material terms or conditions of this 195 | Agreement and does not cure such failure in a reasonable period of 196 | time after becoming aware of such noncompliance. If all Recipient's 197 | rights under this Agreement terminate, Recipient agrees to cease use 198 | and distribution of the Program as soon as reasonably 199 | practicable. However, Recipient's obligations under this Agreement and 200 | any licenses granted by Recipient relating to the Program shall 201 | continue and survive. 202 | 203 | Everyone is permitted to copy and distribute copies of this Agreement, 204 | but in order to avoid inconsistency the Agreement is copyrighted and 205 | may only be modified in the following manner. The Agreement Steward 206 | reserves the right to publish new versions (including revisions) of 207 | this Agreement from time to time. No one other than the Agreement 208 | Steward has the right to modify this Agreement. The Eclipse Foundation 209 | is the initial Agreement Steward. The Eclipse Foundation may assign 210 | the responsibility to serve as the Agreement Steward to a suitable 211 | separate entity. Each new version of the Agreement will be given a 212 | distinguishing version number. The Program (including Contributions) 213 | may always be distributed subject to the version of the Agreement 214 | under which it was received. In addition, after a new version of the 215 | Agreement is published, Contributor may elect to distribute the 216 | Program (including its Contributions) under the new version. Except as 217 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives 218 | no rights or licenses to the intellectual property of any Contributor 219 | under this Agreement, whether expressly, by implication, estoppel or 220 | otherwise. All rights in the Program not expressly granted under this 221 | Agreement are reserved. 222 | 223 | This Agreement is governed by the laws of the State of Washington and 224 | the intellectual property laws of the United States of America. No 225 | party to this Agreement will bring a legal action under this Agreement 226 | more than one year after the cause of action arose. Each party waives 227 | its rights to a jury trial in any resulting litigation. 228 | --------------------------------------------------------------------------------