├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bb.edn ├── bin ├── push_docs_for_current_commit.sh └── release.sh ├── deps.edn ├── project.clj ├── resources └── clj-kondo.exports │ └── prismatic │ └── schema │ └── config.edn ├── src ├── clj │ └── schema │ │ ├── experimental │ │ ├── complete.clj │ │ └── generators.clj │ │ ├── macros.clj │ │ └── potemkin.clj └── cljc │ └── schema │ ├── coerce.cljc │ ├── core.cljc │ ├── experimental │ └── abstract_map.cljc │ ├── spec │ ├── collection.cljc │ ├── core.cljc │ ├── leaf.cljc │ └── variant.cljc │ ├── test.cljc │ └── utils.cljc └── test ├── bb └── schema │ └── bb_test_runner.clj ├── clj └── schema │ ├── experimental │ ├── complete_test.clj │ └── generators_test.clj │ ├── macros_test.clj │ └── test_macros.clj ├── cljc └── schema │ ├── coerce_test.cljc │ ├── core_test.cljc │ ├── experimental │ └── abstract_map_test.cljc │ ├── other_namespace.cljc │ ├── test_test.cljc │ └── utils_test.cljc └── cljs └── schema └── test_runner.cljs /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] This is a bug report (with instructions to reproduce) or other issue with the code 2 | (if this is a question or feature request, please **do not** open an issue and post on the [mailing list](https://groups.google.com/forum/#!forum/prismatic-plumbing) instead). -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | # monthly 10 | - cron: "0 0 1 * *" 11 | 12 | env: 13 | #bump to clear caches 14 | ACTION_CACHE_VERSION: 'v2' 15 | LEIN_VERSION: '2.9.8' 16 | 17 | jobs: 18 | setup: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | - uses: actions/cache@v2 24 | with: 25 | path: ~/.m2/repository 26 | key: ${{ env.ACTION_CACHE_VERSION }}-${{ runner.os }}-maven-${{ hashFiles('**/project.clj') }} 27 | restore-keys: | 28 | ${{ env.ACTION_CACHE_VERSION }}-${{ runner.os }}-maven- 29 | - name: Prepare java 30 | uses: actions/setup-java@v2 31 | with: 32 | distribution: 'temurin' 33 | java-version: 11 34 | - name: Install clojure tools 35 | uses: DeLaGuardo/setup-clojure@5.1 36 | with: 37 | lein: ${{ env.LEIN_VERSION }} 38 | - name: Warm deps cache 39 | run: lein all deps 40 | lint: 41 | needs: setup 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v2 46 | - uses: actions/cache@v2 47 | with: 48 | path: ~/.m2/repository 49 | key: ${{ env.ACTION_CACHE_VERSION }}-${{ runner.os }}-maven-${{ hashFiles('**/project.clj') }} 50 | restore-keys: | 51 | ${{ env.ACTION_CACHE_VERSION }}-${{ runner.os }}-maven- 52 | - name: Prepare java 53 | uses: actions/setup-java@v2 54 | with: 55 | distribution: 'temurin' 56 | java-version: 11 57 | - name: Install clojure tools 58 | uses: DeLaGuardo/setup-clojure@5.1 59 | with: 60 | lein: ${{ env.LEIN_VERSION }} 61 | - name: Run Eastwood 62 | run: lein eastwood 63 | test-jvm: 64 | needs: setup 65 | strategy: 66 | matrix: 67 | java: ['8', '11', '17', '18'] 68 | runs-on: ubuntu-latest 69 | steps: 70 | - name: Checkout 71 | uses: actions/checkout@v2 72 | - uses: actions/cache@v2 73 | with: 74 | path: ~/.m2/repository 75 | key: ${{ env.ACTION_CACHE_VERSION }}-${{ runner.os }}-maven-${{ hashFiles('**/project.clj') }} 76 | restore-keys: | 77 | ${{ env.ACTION_CACHE_VERSION }}-${{ runner.os }}-maven- 78 | - name: Prepare java 79 | uses: actions/setup-java@v2 80 | with: 81 | distribution: 'temurin' 82 | java-version: ${{ matrix.java }} 83 | - uses: actions/setup-node@v2 84 | with: 85 | node-version: '17.7.2' 86 | - name: Install clojure tools 87 | uses: DeLaGuardo/setup-clojure@5.1 88 | with: 89 | lein: ${{ env.LEIN_VERSION }} 90 | - name: Run JVM tests 91 | run: lein all test 92 | test-bb: 93 | needs: setup 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: Checkout 97 | uses: actions/checkout@v2 98 | - name: Prepare java 99 | uses: actions/setup-java@v2 100 | with: 101 | distribution: 'temurin' 102 | java-version: 17 103 | - name: Download babashka 104 | run: bash <(curl https://raw.githubusercontent.com/babashka/babashka/master/install) --dir . 105 | - name: Run Babashka tests 106 | run: ./bb test 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/** 2 | .lein** 3 | .repl/ 4 | out/ 5 | *~ 6 | pom.xml 7 | *.asc 8 | /doc/ 9 | .nrepl-port 10 | .cpcache/ 11 | .eastwood 12 | bb 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.4.1 (`2022-09-29`) 2 | * [#449](https://github.com/plumatic/schema/issues/449): Fix bad jsdoc 3 | 4 | ## 1.4.0 (`2022-08-16`) 5 | * Add `s/defprotocol` 6 | 7 | ## 1.3.5 (`2022-07-28`) 8 | * [#391](https://github.com/plumatic/schema/issues/391): Improve `s/defalias` error message when passed a schema that doesn't support metadata 9 | * [#442](https://github.com/plumatic/schema/pull/442): Add support for cross-platform JVM type hints (another attempt at fixing [#174](https://github.com/plumatic/schema/issues/174)) 10 | * [#434](https://github.com/plumatic/schema/pull/434): Fix [API doc generation](http://plumatic.github.io/schema) 11 | * Fix `view source` in API docs 12 | 13 | ## 1.3.1 - 1.3.4 (`2022-07-28`) 14 | * Prefer 1.3.4 (used to test the release process for schema) 15 | 16 | ## 1.3.0 (`2022-06-10`) 17 | * [Babashka support](https://github.com/plumatic/schema/pull/440) 18 | * CLJS: Add pretty `pr-str` for schemas 19 | 20 | ## 1.2.1 (`2022-04-28`) 21 | * Fix mutually recursive `s/letfn` bindings 22 | * Fix `s/fn` perf caveats in Clojure by avoiding wrappers 23 | * [Fix](https://github.com/plumatic/schema/pull/438) conflict with key named `constructor` in recent ClojureScript versions. 24 | 25 | ## 1.2.0 (`2021-11-03`) 26 | * **BREAKING** use `cljc` instead of `cljx`, which requires Clojure 1.7 or later. (#425) 27 | 28 | ## 1.1.12 (`2019-08-10`) 29 | * Fixes a "wrong number of arguments" warning in clojurescript (#418) 30 | * Fixes an issue with function name inference introduced in the previous 31 | release (#416) 32 | * Improved the names of built-in predicate schemas in clojurescript production 33 | environments (#417) 34 | 35 | ## 1.1.11 (`2019-06-12`) 36 | * No longer swallows fatal exceptions on the JVM ([#413](https://github.com/plumatic/schema/pull/413)) 37 | * Wildcard keys in map schemas are internally amended 38 | in a way that fixes [a bug in schema-generators](https://github.com/plumatic/schema-generators/issues/16) 39 | * Minor fixes for more recent ClojureScript versions 40 | 41 | ## 1.1.10 42 | * Fix warnings in newer versions of ClojureScript around `aget`, `aset`, and `uuid`. 43 | 44 | ## 1.1.9 45 | * Fix bug introduced in 1.1.8 ([#402](https://github.com/plumatic/schema/pull/402)). 46 | 47 | ## 1.1.8 48 | * Improved errors for multimethods. 49 | 50 | ## 1.1.7 51 | * Remove unnecessarily relative marker in `::schema.spec.collection/` keywords to fix compatibility with latest Clojure 1.9 alpha. 52 | 53 | ## 1.1.6 54 | * Add exclusions to workaround `->MapEntry` warnings with latest cljs. 55 | 56 | ## 1.1.5 57 | * Add float type to JVM coercers. 58 | 59 | ## 1.1.4 60 | * Highlights schema validation errors 61 | * Fix an issue with `isa?` and the global hierarchy 62 | * Fix an issue with coercion and map entries 63 | 64 | ## 1.1.3 65 | * Make fn validation customizable by addition of `fn-validator` function 66 | 67 | ## 1.1.2 68 | * Exclude `clojure.core/Inst` to avoid warnings in Clojure 1.9 alphas 69 | 70 | ## 1.1.1 71 | * Fix (at least some) AOT issues around PSimpleCell/SimpleVCell, by replacing with atom/AtomicReference. 72 | 73 | ## 1.1.0 74 | * **Deprecate* schema.experimental.generators and schema.experimental.complete. Find them in their new home in the separate schema-generators project. 75 | * **BREAKING** change the internal details of collection specs (should only be an issue for custom collection schemas that don't rely on helpers `one-element` or `all-elements`). 76 | * Fix generation for sequence schemas containing a non-trailing `s/optional` element. 77 | 78 | ## 1.0.6 79 | * Install a pprint method that uses the explain, in addition to an ordinary print-method. Should fix large prints and stack overflows while pprinting schemas, or with plugins such as `pretty`. 80 | 81 | ## 1.0.5 82 | * Fix completion through non-map collections 83 | 84 | ## 1.0.4 85 | * Attempt to resolve issues with AOT compilation by moving typehint on `use-fn-validation` var to callsites. 86 | * Update generators, bump minimum version of test.check to 0.9.0. Generators will only work under Clojure 1.7.0+. 87 | * Better performance for anonymous schematized functions, via lazy checker creation 88 | 89 | ## 1.0.3 90 | * Fix warning about overriding `atom` under Clojure 1.7 91 | * Fix behavior of `constrained` with some schemas (e.g. maps). 92 | 93 | ## 1.0.2 94 | * Extend keyword `enum` coercion to keyword `eq` coercion 95 | * Add `s/atom` schema for atoms 96 | * Add `coercer!` which throws on error 97 | * Add leaf generators for UUIDs 98 | * Make `s/defn` compatible with `with-test` 99 | * Add `constrained` schema for postconditions (replaces `(both x (s/pred ...))`) 100 | 101 | ## 1.0.1 102 | * Catch and report exceptions in guards the same as preconditions, rather than allowing them to propagate out. 103 | 104 | ## 1.0.0 105 | * New schema backend, which is faster, simpler, and more declarative, enabling more applications and simplifying tooling. Users of built-in schema types should experience very little or no breakage, but tooling or custom schema types will need to be updated. As a concrete example of an application that's enabled, schema now experimentally supports test-check style generation from schemas, as well as completion of partial inputs. 106 | * **BREAKING** Changes to the core Schema protocol will break existing third-party schema tooling and schema types. 107 | * **BREAKING** Records coerced to an ordinary (non-record) map schema are now converted to maps, rather than retaining their record type. 108 | * **Deprecate** `s/either` in favor of `s/cond-pre`, `s/conditional`, or `schema.experimental.abstract-map-schema`. As of this release, `either` no longer works with coercion. 109 | * **Deprecate** `s/both` in favor of improved `s/conditional`. 110 | * **Deprecate** `schema.core/defrecord+`; moved to new `schema.potemkin` namespace. 111 | * `s/pred` can more intelligently guess the predicate name 112 | * `record` schemas can now coerce values to corresponding record types. 113 | * New experimental `abstract-map-schema` that models super/subclasses as maps. 114 | * Improved explains explains for leaf schemas, especially in Clojurescript. 115 | 116 | ## 0.4.4 117 | * Fix ClojureScript warnings about `map->Record` constructors being redefined. 118 | * Add queue schemas 119 | * Configurable maximum length for values in error messages 120 | * Fix potential memory leaks after many redefinitions of `s/defn` or `s/defrecord`. 121 | 122 | ## 0.4.3 123 | * Fix longstanding AOT compilation issue when used with Clojure 1.7.0-RC1 and later. 124 | 125 | ## 0.4.2 126 | * Add recursive schema support for ClojureScript 127 | * Add ns metadata to defschema 128 | 129 | ## 0.4.1 130 | * Fix some harmless warnings when using Schema with the latest version of ClojureScript (due to the addition of positional constructors for `deftype`). 131 | 132 | ## 0.4.0 133 | * **BREAKING** Remove support for old `^{:schema ..}` style annotations. `:- schema` is the preferred way, but metadata-style schemas are still allowed for valid Clojure typehints. 134 | * **BREAKING** Remove support for bare `:- Protocol` annotations (use `:- (s/protocol Protocol)` instead). 135 | * **BREAKING** Remove deprecated macros (`defn`, `defrecord`, etc) from schema.macros. The identical versions in schema.core remain. 136 | * **BREAKING** Remove potemkin as a dependency, and the `*use-potemkin*` flag. To get the old behavior of potemkin defrecords, you can still bring your own potemkin and use `schema.core/defrecord+` in place of `schema.core/defrecord`. 137 | 138 | ## 0.3.7 139 | * Add coercion handler for s/Uuid from string input 140 | 141 | ## 0.3.6 142 | * Support java.util.List instances as valid data for sequence schemas 143 | 144 | ## 0.3.5 145 | * Make primitive schemas work better in some cases under partial AOT compilation 146 | 147 | ## 0.3.3 148 | * Fix bug in `defschema` which clobbered metadata, breaking `s/protocol` in Clojure in 0.3.2. 149 | 150 | ## 0.3.2 151 | * Fix `s/protocol` in Clojure (didn't work properly with extends created later) 152 | * Fix ClojureScript (Closure) warning about reference to global RegExp object. 153 | * Add `set-compile-fn-validation!` function to turn off emission of validation globally, and turn off emission of validation code for non- ^:always-validate functions when *assert* is false. 154 | 155 | ## 0.3.1 156 | * Fix Clojurescript compilation warnings/errors from accidental references to `clojure.data/diff` and `class` inside error messages. 157 | 158 | ## 0.3.0 159 | * **BREAKING** increase minimum clojurescript version 2120 to support :include-macros 160 | * **Deprecate** direct use of `schema.macros` in client code -- prefer canonical versions in `schema.core` 161 | in both Clojure and ClojureScript, using `:include-macros true` in cljs. 162 | * **Deprecate** old `^{:s schema}` syntax for providing schemas. 163 | * **Deprecate** `*use-potemkin*` flag and behavior to default to potemkin s/defrecords in Clojure; 164 | in future releases, you will have to provide your own potemkin and explicitly opt-in to this behavior. 165 | * (Hopefully) fix issues with AOT compilation, by removing dependence on potemkin/import-vars. 166 | * Add `isa` schema for Clojure hierarchies. 167 | * Preserve the types of maps (including Records) when coercing with map schemas. 168 | * Smarter code generation in s/defrecord to avoid dead code warnings 169 | * Fix printed form of s/Str in ClojureScript 170 | * Make some internal fns public to simplify third-part schema extensions 171 | * Walking records with map schemas preserves the record type 172 | * Proper explain for s/Str 173 | 174 | ## 0.2.6 175 | * Memoize walker computation, providing much faster checker compilation for graph-structured schemas 176 | 177 | ## 0.2.5 178 | * Add `normalized-defn-args` helper fn for defining `s/defn`-like macros. 179 | * Map schemas correctly validate against struct-maps 180 | 181 | ## 0.2.4 182 | * Fixed an issue that could cause ClojureScript compilation to fail 183 | * Generalize `s/recursive` to work on artibrary refs 184 | * Add `s/Symbol` as a cross-platfor primitive 185 | 186 | ## 0.2.3 187 | * Improved explains for primitives & primitive arrays 188 | * More robust double coercions 189 | * Fix cljs warning about extending js/Function 190 | * Import schema.macros/defmulti in schema.core 191 | 192 | ## 0.2.2 193 | * Add validated `s/def`. 194 | * Add validated `s/defmethod`. 195 | * Add `Bool` coercions. 196 | 197 | ## 0.2.1 198 | * Add `Bool` to cross-platform primitives 199 | * Fix several minor bugs 200 | * Replace cljs-test with headless clojurescript.test. 201 | 202 | ## 0.2.0 203 | * **breaking change:** Cross-platform leaves String and Number are now Str and Num (the former caused warnings and broke AOT). 204 | * Replaced core Schema protocol method `check` with `walker`, for increased speed and versatility 205 | * Support for schema-driven transformations/coercion 206 | * Schemas for primitive arrays (`longs`, etc) 207 | * Schematized `letfn` 208 | 209 | ## 0.1.10 210 | * Remove non-dev dependency on cljx 211 | 212 | ## 0.1.9 213 | * Support for pre/postcondition maps in `s/defn` 214 | * Support for recursive schemas in Clojure 215 | * Fixes for sm/defn and sm/defrecord with cljs advanced compilation 216 | 217 | ## 0.1.8 218 | * Works with advanced compilation in cljs (at least sometimes) 219 | 220 | ## 0.1.7 221 | * More small bugfixes 222 | * Better validation error messages in cljs 223 | 224 | ## 0.1.6 225 | * Minor bugfixes (thanks various contributors) 226 | * Extend schema protocol to regex (thanks [AlexBaranosky](https://github.com/AlexBaranosky)). 227 | * Add `:never-validate` meta option 228 | 229 | ## 0.1.5 230 | * Fix regression in primitive handling introduced in 0.1.4 231 | 232 | ## 0.1.4 233 | * Added Regex, Inst, and Uuid as primitive schema types (thanks [jwhitlark](https://github.com/jwhitlark)) 234 | * Add annotated arglists to functions defined with `s/defn` (thanks [danielneal](https://github.com/danielneal)) 235 | * Add `set-fn-validation!` to schema.core, to globally turn validation on or off. 236 | * Add `:always-validate` metadata on fn/defn name to unconditionally use validation. 237 | 238 | ## 0.1.3 239 | * Fix compatibility with Clojurescript 1889 (removal of format) 240 | 241 | ## 0.1.2 242 | * Validate returns the value on success 243 | * Sequence schemas only match sequential? things, to match map and set 244 | * Implementation of `defschema` puts name in metadata, rather than generating named schema 245 | * Improved error messages and stack traces for `s/defn` 246 | 247 | ## 0.1.1 248 | * Bugfix: with-fn-validation persisting after Exception 249 | 250 | ## 0.1.0 251 | * Initial release 252 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions to Schema are very welcome. 4 | 5 | Please file bug reports on [GitHub](https://github.com/plumatic/schema/issues). 6 | 7 | For questions, feature requests, or discussion, please post on the Plumbing [mailing list](https://groups.google.com/forum/#!forum/prismatic-plumbing) for now. 8 | 9 | Contributions are preferred as GitHub pull requests on topic branches. If you want to discuss a potential change before coding it up, please post on the mailing list. 10 | 11 | Schema is relatively well-tested, on both Clojure and ClojureScript. Before submitting a pull request, we ask that you: 12 | 13 | * please try to follow the conventions in the existing code, including standard Emacs indentation, no trailing whitespace, and a max width of 95 columns 14 | * rebase your feature branch on the latest master branch 15 | * ensure any new code is well-tested, and if possible, any issue fixed is covered by one or more new tests 16 | * check that all of the tests pass **in both Clojure and ClojureScript** 17 | 18 | To run the Clojure and ClojureScript tests, run `lein test`. You must have phantomjs installed for the ClojureScript tests to run. 19 | 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Source code distributed under the 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 to control, 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 New York 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 | 229 | 230 | 231 | Images distributed under the Creative Commons Attribution + ShareAlike 232 | License version 3.0: 233 | 234 | THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS 235 | CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS 236 | PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE 237 | WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS 238 | PROHIBITED. 239 | 240 | BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND 241 | AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS 242 | LICENSE MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU 243 | THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH 244 | TERMS AND CONDITIONS. 245 | 246 | 1. Definitions 247 | 248 | "Adaptation" means a work based upon the Work, or upon the Work 249 | and other pre-existing works, such as a translation, adaptation, 250 | derivative work, arrangement of music or other alterations of a 251 | literary or artistic work, or phonogram or performance and 252 | includes cinematographic adaptations or any other form in which 253 | the Work may be recast, transformed, or adapted including in any 254 | form recognizably derived from the original, except that a work 255 | that constitutes a Collection will not be considered an Adaptation 256 | for the purpose of this License. For the avoidance of doubt, where 257 | the Work is a musical work, performance or phonogram, the 258 | synchronization of the Work in timed-relation with a moving image 259 | ("synching") will be considered an Adaptation for the purpose of 260 | this License. 261 | 262 | "Collection" means a collection of literary or artistic works, 263 | such as encyclopedias and anthologies, or performances, phonograms 264 | or broadcasts, or other works or subject matter other than works 265 | listed in Section 1(f) below, which, by reason of the selection 266 | and arrangement of their contents, constitute intellectual 267 | creations, in which the Work is included in its entirety in 268 | unmodified form along with one or more other contributions, each 269 | constituting separate and independent works in themselves, which 270 | together are assembled into a collective whole. A work that 271 | constitutes a Collection will not be considered an Adaptation (as 272 | defined below) for the purposes of this License. 273 | 274 | "Creative Commons Compatible License" means a license that is 275 | listed at http://creativecommons.org/compatiblelicenses that has 276 | been approved by Creative Commons as being essentially equivalent 277 | to this License, including, at a minimum, because that license: 278 | (i) contains terms that have the same purpose, meaning and effect 279 | as the License Elements of this License; and, (ii) explicitly 280 | permits the relicensing of adaptations of works made available 281 | under that license under this License or a Creative Commons 282 | jurisdiction license with the same License Elements as this 283 | License. 284 | 285 | "Distribute" means to make available to the public the original 286 | and copies of the Work or Adaptation, as appropriate, through sale 287 | or other transfer of ownership. 288 | 289 | "License Elements" means the following high-level license 290 | attributes as selected by Licensor and indicated in the title of 291 | this License: Attribution, ShareAlike. 292 | 293 | "Licensor" means the individual, individuals, entity or entities 294 | that offer(s) the Work under the terms of this License. 295 | 296 | "Original Author" means, in the case of a literary or artistic 297 | work, the individual, individuals, entity or entities who created 298 | the Work or if no individual or entity can be identified, the 299 | publisher; and in addition (i) in the case of a performance the 300 | actors, singers, musicians, dancers, and other persons who act, 301 | sing, deliver, declaim, play in, interpret or otherwise perform 302 | literary or artistic works or expressions of folklore; (ii) in the 303 | case of a phonogram the producer being the person or legal entity 304 | who first fixes the sounds of a performance or other sounds; and, 305 | (iii) in the case of broadcasts, the organization that transmits 306 | the broadcast. 307 | 308 | "Work" means the literary and/or artistic work offered under the 309 | terms of this License including without limitation any production 310 | in the literary, scientific and artistic domain, whatever may be 311 | the mode or form of its expression including digital form, such as 312 | a book, pamphlet and other writing; a lecture, address, sermon or 313 | other work of the same nature; a dramatic or dramatico-musical 314 | work; a choreographic work or entertainment in dumb show; a 315 | musical composition with or without words; a cinematographic work 316 | to which are assimilated works expressed by a process analogous to 317 | cinematography; a work of drawing, painting, architecture, 318 | sculpture, engraving or lithography; a photographic work to which 319 | are assimilated works expressed by a process analogous to 320 | photography; a work of applied art; an illustration, map, plan, 321 | sketch or three-dimensional work relative to geography, 322 | topography, architecture or science; a performance; a broadcast; a 323 | phonogram; a compilation of data to the extent it is protected as 324 | a copyrightable work; or a work performed by a variety or circus 325 | performer to the extent it is not otherwise considered a literary 326 | or artistic work. 327 | 328 | "You" means an individual or entity exercising rights under this 329 | License who has not previously violated the terms of this License 330 | with respect to the Work, or who has received express permission 331 | from the Licensor to exercise rights under this License despite a 332 | previous violation. 333 | 334 | "Publicly Perform" means to perform public recitations of the Work 335 | and to communicate to the public those public recitations, by any 336 | means or process, including by wire or wireless means or public 337 | digital performances; to make available to the public Works in 338 | such a way that members of the public may access these Works from 339 | a place and at a place individually chosen by them; to perform the 340 | Work to the public by any means or process and the communication 341 | to the public of the performances of the Work, including by public 342 | digital performance; to broadcast and rebroadcast the Work by any 343 | means including signs, sounds or images. 344 | 345 | "Reproduce" means to make copies of the Work by any means 346 | including without limitation by sound or visual recordings and the 347 | right of fixation and reproducing fixations of the Work, including 348 | storage of a protected performance or phonogram in digital form or 349 | other electronic medium. 350 | 351 | 2. Fair Dealing Rights. Nothing in this License is intended to reduce, 352 | limit, or restrict any uses free from copyright or rights arising from 353 | limitations or exceptions that are provided for in connection with the 354 | copyright protection under copyright law or other applicable laws. 355 | 356 | 3. License Grant. Subject to the terms and conditions of this License, 357 | Licensor hereby grants You a worldwide, royalty-free, non-exclusive, 358 | perpetual (for the duration of the applicable copyright) license to 359 | exercise the rights in the Work as stated below: 360 | 361 | to Reproduce the Work, to incorporate the Work into one or more 362 | Collections, and to Reproduce the Work as incorporated in the 363 | Collections; 364 | 365 | to create and Reproduce Adaptations provided that any such 366 | Adaptation, including any translation in any medium, takes 367 | reasonable steps to clearly label, demarcate or otherwise identify 368 | that changes were made to the original Work. For example, a 369 | translation could be marked "The original work was translated from 370 | English to Spanish," or a modification could indicate "The 371 | original work has been modified."; 372 | 373 | to Distribute and Publicly Perform the Work including as 374 | incorporated in Collections; and, 375 | 376 | to Distribute and Publicly Perform Adaptations. 377 | 378 | For the avoidance of doubt: 379 | 380 | Non-waivable Compulsory License Schemes. In those 381 | jurisdictions in which the right to collect royalties through 382 | any statutory or compulsory licensing scheme cannot be waived, 383 | the Licensor reserves the exclusive right to collect such 384 | royalties for any exercise by You of the rights granted under 385 | this License; 386 | 387 | Waivable Compulsory License Schemes. In those jurisdictions in 388 | which the right to collect royalties through any statutory or 389 | compulsory licensing scheme can be waived, the Licensor waives 390 | the exclusive right to collect such royalties for any exercise 391 | by You of the rights granted under this License; and, 392 | 393 | Voluntary License Schemes. The Licensor waives the right to 394 | collect royalties, whether individually or, in the event that 395 | the Licensor is a member of a collecting society that 396 | administers voluntary licensing schemes, via that society, 397 | from any exercise by You of the rights granted under this 398 | License. 399 | 400 | The above rights may be exercised in all media and formats whether now 401 | known or hereafter devised. The above rights include the right to make 402 | such modifications as are technically necessary to exercise the rights 403 | in other media and formats. Subject to Section 8(f), all rights not 404 | expressly granted by Licensor are hereby reserved. 405 | 406 | 4. Restrictions. The license granted in Section 3 above is expressly 407 | made subject to and limited by the following restrictions: 408 | 409 | You may Distribute or Publicly Perform the Work only under the 410 | terms of this License. You must include a copy of, or the Uniform 411 | Resource Identifier (URI) for, this License with every copy of the 412 | Work You Distribute or Publicly Perform. You may not offer or 413 | impose any terms on the Work that restrict the terms of this 414 | License or the ability of the recipient of the Work to exercise 415 | the rights granted to that recipient under the terms of the 416 | License. You may not sublicense the Work. You must keep intact all 417 | notices that refer to this License and to the disclaimer of 418 | warranties with every copy of the Work You Distribute or Publicly 419 | Perform. When You Distribute or Publicly Perform the Work, You may 420 | not impose any effective technological measures on the Work that 421 | restrict the ability of a recipient of the Work from You to 422 | exercise the rights granted to that recipient under the terms of 423 | the License. This Section 4(a) applies to the Work as incorporated 424 | in a Collection, but this does not require the Collection apart 425 | from the Work itself to be made subject to the terms of this 426 | License. If You create a Collection, upon notice from any Licensor 427 | You must, to the extent practicable, remove from the Collection 428 | any credit as required by Section 4(c), as requested. If You 429 | create an Adaptation, upon notice from any Licensor You must, to 430 | the extent practicable, remove from the Adaptation any credit as 431 | required by Section 4(c), as requested. 432 | 433 | You may Distribute or Publicly Perform an Adaptation only under 434 | the terms of: (i) this License; (ii) a later version of this 435 | License with the same License Elements as this License; (iii) a 436 | Creative Commons jurisdiction license (either this or a later 437 | license version) that contains the same License Elements as this 438 | License (e.g., Attribution-ShareAlike 3.0 US)); (iv) a Creative 439 | Commons Compatible License. If you license the Adaptation under 440 | one of the licenses mentioned in (iv), you must comply with the 441 | terms of that license. If you license the Adaptation under the 442 | terms of any of the licenses mentioned in (i), (ii) or (iii) (the 443 | "Applicable License"), you must comply with the terms of the 444 | Applicable License generally and the following provisions: (I) You 445 | must include a copy of, or the URI for, the Applicable License 446 | with every copy of each Adaptation You Distribute or Publicly 447 | Perform; (II) You may not offer or impose any terms on the 448 | Adaptation that restrict the terms of the Applicable License or 449 | the ability of the recipient of the Adaptation to exercise the 450 | rights granted to that recipient under the terms of the Applicable 451 | License; (III) You must keep intact all notices that refer to the 452 | Applicable License and to the disclaimer of warranties with every 453 | copy of the Work as included in the Adaptation You Distribute or 454 | Publicly Perform; (IV) when You Distribute or Publicly Perform the 455 | Adaptation, You may not impose any effective technological 456 | measures on the Adaptation that restrict the ability of a 457 | recipient of the Adaptation from You to exercise the rights 458 | granted to that recipient under the terms of the Applicable 459 | License. This Section 4(b) applies to the Adaptation as 460 | incorporated in a Collection, but this does not require the 461 | Collection apart from the Adaptation itself to be made subject to 462 | the terms of the Applicable License. 463 | 464 | If You Distribute, or Publicly Perform the Work or any Adaptations 465 | or Collections, You must, unless a request has been made pursuant 466 | to Section 4(a), keep intact all copyright notices for the Work 467 | and provide, reasonable to the medium or means You are utilizing: 468 | (i) the name of the Original Author (or pseudonym, if applicable) 469 | if supplied, and/or if the Original Author and/or Licensor 470 | designate another party or parties (e.g., a sponsor institute, 471 | publishing entity, journal) for attribution ("Attribution 472 | Parties") in Licensor's copyright notice, terms of service or by 473 | other reasonable means, the name of such party or parties; (ii) 474 | the title of the Work if supplied; (iii) to the extent reasonably 475 | practicable, the URI, if any, that Licensor specifies to be 476 | associated with the Work, unless such URI does not refer to the 477 | copyright notice or licensing information for the Work; and (iv) , 478 | consistent with Ssection 3(b), in the case of an Adaptation, a 479 | credit identifying the use of the Work in the Adaptation (e.g., 480 | "French translation of the Work by Original Author," or 481 | "Screenplay based on original Work by Original Author"). The 482 | credit required by this Section 4(c) may be implemented in any 483 | reasonable manner; provided, however, that in the case of a 484 | Adaptation or Collection, at a minimum such credit will appear, if 485 | a credit for all contributing authors of the Adaptation or 486 | Collection appears, then as part of these credits and in a manner 487 | at least as prominent as the credits for the other contributing 488 | authors. For the avoidance of doubt, You may only use the credit 489 | required by this Section for the purpose of attribution in the 490 | manner set out above and, by exercising Your rights under this 491 | License, You may not implicitly or explicitly assert or imply any 492 | connection with, sponsorship or endorsement by the Original 493 | Author, Licensor and/or Attribution Parties, as appropriate, of 494 | You or Your use of the Work, without the separate, express prior 495 | written permission of the Original Author, Licensor and/or 496 | Attribution Parties. 497 | 498 | Except as otherwise agreed in writing by the Licensor or as may be 499 | otherwise permitted by applicable law, if You Reproduce, 500 | Distribute or Publicly Perform the Work either by itself or as 501 | part of any Adaptations or Collections, You must not distort, 502 | mutilate, modify or take other derogatory action in relation to 503 | the Work which would be prejudicial to the Original Author's honor 504 | or reputation. Licensor agrees that in those jurisdictions 505 | (e.g. Japan), in which any exercise of the right granted in 506 | Section 3(b) of this License (the right to make Adaptations) would 507 | be deemed to be a distortion, mutilation, modification or other 508 | derogatory action prejudicial to the Original Author's honor and 509 | reputation, the Licensor will waive or not assert, as appropriate, 510 | this Section, to the fullest extent permitted by the applicable 511 | national law, to enable You to reasonably exercise Your right 512 | under Section 3(b) of this License (right to make Adaptations) but 513 | not otherwise. 514 | 515 | 5. Representations, Warranties and Disclaimer 516 | 517 | UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, 518 | LICENSOR OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR 519 | WARRANTIES OF ANY KIND CONCERNING THE WORK, EXPRESS, IMPLIED, 520 | STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, WARRANTIES OF 521 | TITLE, MERCHANTIBILITY, FITNESS FOR A PARTICULAR PURPOSE, 522 | NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, 523 | OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT 524 | DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED 525 | WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. 526 | 527 | 6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY 528 | APPLICABLE LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY 529 | LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR 530 | EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, 531 | EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 532 | 533 | 7. Termination 534 | 535 | This License and the rights granted hereunder will terminate 536 | automatically upon any breach by You of the terms of this 537 | License. Individuals or entities who have received Adaptations or 538 | Collections from You under this License, however, will not have 539 | their licenses terminated provided such individuals or entities 540 | remain in full compliance with those licenses. Sections 1, 2, 5, 541 | 6, 7, and 8 will survive any termination of this License. 542 | 543 | Subject to the above terms and conditions, the license granted 544 | here is perpetual (for the duration of the applicable copyright in 545 | the Work). Notwithstanding the above, Licensor reserves the right 546 | to release the Work under different license terms or to stop 547 | distributing the Work at any time; provided, however that any such 548 | election will not serve to withdraw this License (or any other 549 | license that has been, or is required to be, granted under the 550 | terms of this License), and this License will continue in full 551 | force and effect unless terminated as stated above. 552 | 553 | 8. Miscellaneous 554 | 555 | Each time You Distribute or Publicly Perform the Work or a 556 | Collection, the Licensor offers to the recipient a license to the 557 | Work on the same terms and conditions as the license granted to 558 | You under this License. 559 | 560 | Each time You Distribute or Publicly Perform an Adaptation, 561 | Licensor offers to the recipient a license to the original Work on 562 | the same terms and conditions as the license granted to You under 563 | this License. 564 | 565 | If any provision of this License is invalid or unenforceable under 566 | applicable law, it shall not affect the validity or enforceability 567 | of the remainder of the terms of this License, and without further 568 | action by the parties to this agreement, such provision shall be 569 | reformed to the minimum extent necessary to make such provision 570 | valid and enforceable. 571 | 572 | No term or provision of this License shall be deemed waived and no 573 | breach consented to unless such waiver or consent shall be in 574 | writing and signed by the party to be charged with such waiver or 575 | consent. 576 | 577 | This License constitutes the entire agreement between the parties 578 | with respect to the Work licensed here. There are no 579 | understandings, agreements or representations with respect to the 580 | Work not specified here. Licensor shall not be bound by any 581 | additional provisions that may appear in any communication from 582 | You. This License may not be modified without the mutual written 583 | agreement of the Licensor and You. 584 | 585 | The rights granted under, and the subject matter referenced, in 586 | this License were drafted utilizing the terminology of the Berne 587 | Convention for the Protection of Literary and Artistic Works (as 588 | amended on September 28, 1979), the Rome Convention of 1961, the 589 | WIPO Copyright Treaty of 1996, the WIPO Performances and 590 | Phonograms Treaty of 1996 and the Universal Copyright Convention 591 | (as revised on July 24, 1971). These rights and subject matter 592 | take effect in the relevant jurisdiction in which the License 593 | terms are sought to be enforced according to the corresponding 594 | provisions of the implementation of those treaty provisions in the 595 | applicable national law. If the standard suite of rights granted 596 | under applicable copyright law includes additional rights not 597 | granted under this License, such additional rights are deemed to 598 | be included in the License; this License is not intended to 599 | restrict the license of any rights under applicable law. 600 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | A Clojure(Script) library for declarative data description and validation. 4 | 5 | [![Clojars Project](https://clojars.org/prismatic/schema/latest-version.svg)](https://clojars.org/prismatic/schema) 6 | 7 | [API docs](https://plumatic.github.io/schema). 8 | 9 | -- 10 | 11 | One of the difficulties with bringing Clojure into a team is the overhead of understanding the kind of data (e.g., list of strings, nested map from long to string to double) that a function expects and returns. While a full-blown type system is one solution to this problem, we present a lighter weight solution: schemas. (For more details on why we built Schema, check out [this post](https://plumatic.github.io/schema-for-clojurescript-data-shape-declaration-and-validation).) 12 | 13 | Schema is a rich language for describing data shapes, with a variety of features: 14 | 15 | - Data validation, with descriptive error messages of failures (targeted at programmers) 16 | - Annotation of function arguments and return values, with optional runtime validation 17 | - Schema-driven data **coercion**, which can automatically, succinctly, and safely convert complex data types (see the Coercion section below) 18 | - Other 19 | - Schema is also built into our [`plumbing`](https://github.com/plumatic/plumbing) and [`fnhouse`](https://github.com/plumatic/fnhouse) libraries, which illustrate how we build services and APIs easily and safely with Schema 20 | - Schema also supports experimental `clojure.test.check` data **generation** from Schemas, as well as **completion** of partial datums, features we've found very useful when writing tests as part of the [`schema-generators`](https://github.com/plumatic/schema-generators) library 21 | 22 | ## Meet Schema 23 | 24 | A Schema is a Clojure(Script) data structure describing a data shape, which can be used to document and validate functions and data. 25 | 26 | ```clojure 27 | (ns schema-examples 28 | (:require [schema.core :as s 29 | :include-macros true ;; cljs only 30 | ])) 31 | 32 | (s/defschema Data 33 | "A schema for a nested data type" 34 | {:a {:b s/Str 35 | :c s/Int} 36 | :d [{:e s/Keyword 37 | :f [s/Num]}]}) 38 | 39 | (s/validate 40 | Data 41 | {:a {:b "abc" 42 | :c 123} 43 | :d [{:e :bc 44 | :f [12.2 13 100]} 45 | {:e :bc 46 | :f [-1]}]}) 47 | ;; Success! 48 | 49 | (s/validate 50 | Data 51 | {:a {:b 123 52 | :c "ABC"}}) 53 | ;; Exception -- Value does not match schema: 54 | ;; {:a {:b (not (instance? java.lang.String 123)), 55 | ;; :c (not (integer? "ABC"))}, 56 | ;; :d missing-required-key} 57 | ``` 58 | 59 | The simplest schemas describe leaf values like Keywords, Numbers, and instances of Classes (on the JVM) and prototypes (in ClojureScript): 60 | 61 | ```clojure 62 | ;; s/Any, s/Bool, s/Num, s/Keyword, s/Symbol, s/Int, and s/Str are cross-platform schemas. 63 | 64 | (s/validate s/Num 42) 65 | ;; 42 66 | (s/validate s/Num "42") 67 | ;; RuntimeException: Value does not match schema: (not (instance java.lang.Number "42")) 68 | 69 | (s/validate s/Keyword :whoa) 70 | ;; :whoa 71 | (s/validate s/Keyword 123) 72 | ;; RuntimeException: Value does not match schema: (not (keyword? 123)) 73 | 74 | ;; On the JVM, you can use classes for instance? checks 75 | (s/validate java.lang.String "schema") 76 | 77 | ;; On JS, you can use prototype functions 78 | (s/validate Element (js/document.getElementById "some-div-id")) 79 | ``` 80 | 81 | From these simple building blocks, we can build up more complex schemas that look like the data they describe. Taking the examples above: 82 | 83 | ```clojure 84 | ;; list of strings 85 | (s/validate [s/Str] ["a" "b" "c"]) 86 | 87 | ;; nested map from long to String to double 88 | (s/validate {long {String double}} {1 {"2" 3.0 "4" 5.0}}) 89 | ``` 90 | 91 | Since schemas are just data, you can also `def` them and reuse and compose them as you would expect: 92 | 93 | ```clojure 94 | (def StringList [s/Str]) 95 | (def StringScores {String double}) 96 | (def StringScoreMap {long StringScores}) 97 | ``` 98 | 99 | However, we encourage you to use `s/defschema` for this purpose to improve error messages: 100 | 101 | ```clojure 102 | (s/defschema StringList [s/Str]) 103 | (s/defschema StringScores {String double}) 104 | (s/defschema StringScoreMap {long StringScores}) 105 | ``` 106 | 107 | What about when things go bad? Schema's `s/check` and `s/validate` provide meaningful errors that look like the bad parts of your data, and are (hopefully) easy to understand. 108 | 109 | ```clojure 110 | (s/validate StringList ["a" :b "c"]) 111 | ;; RuntimeException: Value does not match schema: 112 | ;; [nil (not (instance? java.lang.String :b)) nil] 113 | 114 | (s/validate StringScoreMap {1 {"2" 3.0 "3" [5.0]} 4.0 {}}) 115 | ;; RuntimeException: Value does not match schema: 116 | ;; {1 {"3" (not (instance? java.lang.Double [5.0]))}, 117 | ;; (not (instance? java.lang.Long 4.0)) invalid-key} 118 | 119 | ``` 120 | 121 | See the [More examples](#more-examples) section below for more examples and explanation, or the [custom Schemas types](https://github.com/plumatic/schema/wiki/Defining-New-Schema-Types-1.0) page for details on how Schema works under the hood. 122 | 123 | 124 | ## Beyond type hints 125 | 126 | If you've done much Clojure, you've probably seen code with documentation like this: 127 | 128 | ```clojure 129 | (defprotocol TimestampOffsetter 130 | (offset-timestamp [this offset] "adds integer offset to stamped object and returns the result")) 131 | 132 | (defrecord StampedNames 133 | [^Long date 134 | names] ;; a list of Strings 135 | TimestampOffsetter 136 | (offset [this offset] (+ date offset))) 137 | 138 | (defn ^StampedNames stamped-names 139 | "names is a list of Strings" 140 | [names] 141 | (StampedNames. (str (System/currentTimeMillis)) names)) 142 | 143 | (def ^StampedNames example-stamped-names 144 | (stamped-names (map (fn [first-name] ;; takes and returns a string 145 | (str first-name " Smith")) 146 | ["Bob" "Jane"]))) 147 | ``` 148 | 149 | Clojure's type hints make great documentation, but they fall short for complex types, often leading to ad-hoc descriptions of data in comments and doc-strings. This is better than nothing, but these ad hoc descriptions are often imprecise, hard to read, and prone to bit-rot. 150 | 151 | Schema provides macros `s/defprotocol`, `s/defrecord`, `s/defn`, `s/def`, and `s/fn` that help bridge this gap. These macros are just like their `clojure.core` counterparts, except they support arbitrary schemas as type hints on fields, arguments, and return values. This is a graceful extension of Clojure's type hinting system, because every type hint is a valid Schema, and Schemas that represent valid type hints are automatically passed through to Clojure. 152 | 153 | ```clojure 154 | (s/defprotocol TimestampOffsetter 155 | (offset-timestamp :- s/Int [this offset :- s/Int])) 156 | 157 | (s/defrecord StampedNames 158 | [date :- Long 159 | names :- [s/Str]] 160 | TimestampOffsetter 161 | (offset [this offset] (+ date offset))) 162 | 163 | (s/defn stamped-names :- StampedNames 164 | [names :- [s/Str]] 165 | (StampedNames. (str (System/currentTimeMillis)) names)) 166 | 167 | (s/def example-stamped-names :- StampedNames 168 | (stamped-names (map (s/fn :- s/Str [first-name :- s/Str] 169 | (str first-name " Smith")) 170 | ["Bob" "Jane"]))) 171 | ``` 172 | 173 | Here, `x :- y` means that `x` must satisfy schema `y`, replacing and extending the more familiar metadata hints such as `^y x`. 174 | 175 | As you can see, these type hints are precise, easy to read, and shorter than the comments they replace. Moreover, they produce Schemas that are *data*, and can be inspected, manipulated, and used for validation on-demand (did you spot the bug in `stamped-names`?) 176 | 177 | ```clojure 178 | ;; You can inspect the schemas of the record and function 179 | 180 | (s/explain StampedNames) 181 | ==> (record user.StampedNames {:date java.lang.Long, :names [java.lang.String]}) 182 | 183 | (s/explain (s/fn-schema stamped-names)) 184 | ==> (=> (record user.StampedNames {:date java.lang.Long, :names [java.lang.String]}) 185 | [java.lang.String]) 186 | 187 | ;; And you can turn on validation to catch bugs in your functions and schemas 188 | (s/with-fn-validation 189 | (stamped-names ["bob"])) 190 | ==> RuntimeException: Output of stamped-names does not match schema: 191 | {:date (not (instance? java.lang.Long "1378267311501"))} 192 | 193 | ;; Oops, I guess we should remove that `str` from `stamped-names`. 194 | ``` 195 | 196 | ## Schemas in practice 197 | 198 | We've already seen how we can build up Schemas via composition, attach them to functions, and use them to validate data. What does this look like in practice? 199 | 200 | First, we ensure that all data types that will be shared across namespaces (or heavily used within namespaces) have Schemas, either by `def`ing them or using `s/defrecord`. This allows us to compactly and precisely refer to this data type in more complex data types, or when documenting function arguments and return values. 201 | 202 | This documentation is probably the most important benefit of Schema, which is why we've optimized Schemas for easy readability and reuse -- and sometimes, this is all you need. Schemas are purely descriptive, not prescriptive, so unlike a type system they should never get in your way, or constrain the types of functions you can write. 203 | 204 | After documentation, the next-most important benefit is validation. Thus far, we've found four key use cases for validation. First, you can globally turn on function validation within a given test namespace by adding this line: 205 | 206 | ```clojure 207 | (use-fixtures :once schema.test/validate-schemas) 208 | ``` 209 | 210 | As long as your tests cover all call boundaries, this means you should catch any 'type-like' bugs in your code at test time. 211 | 212 | Second, it may be handy to enable schema validation during development. To enable it, you can either type this into the repl or put it in your `user.clj`: 213 | 214 | ```clojure 215 | (s/set-fn-validation! true) 216 | ``` 217 | 218 | To disable it again, call the same function, but with `false` as parameter instead. 219 | 220 | Third, we manually call `s/validate` to check any data we read and write over the wire or to persistent storage, ensuring that we catch and debug bad data before it strays too far from its source. If you need maximal performance, you can avoid the schema processing overhead on each call by create a validator once with `s/validator` and calling the resulting function on each datum you want to validate (`s/defn` does this under the hood). Analogously, `s/check` and `s/checker` are similar, but *return* the error (or nil for success) rather than throwing exceptions on bad data. 221 | 222 | Alternatively, you can force validation for key functions (without the need for `with-fn-validation`): 223 | 224 | ```clojure 225 | (s/defn ^:always-validate stamped-names ...) 226 | ``` 227 | 228 | Thus, each time you invoke `stamped-names`, Schema will perform validation. 229 | 230 | To reduce generated code size, you can use the `*assert*` flag and `set-compile-fn-validation!` functions to control when validation code is generated ([details](https://github.com/plumatic/schema/blob/master/src/clj/schema/macros.clj#L181)). 231 | 232 | Schema will attempt to reduce the verbosity of its output by restricting the size of values that fail validation to 19 characters. If a value exceeds this, it will be replaced by the name of its class. You can adjust this size limitation by calling `set-max-value-length!`. 233 | 234 | Finally, we use validation with coercion for API inputs and outputs. See the coercion section below for details. 235 | 236 | ## More examples 237 | 238 | The source code in [schema/core.cljc](https://github.com/plumatic/schema/blob/master/src/cljc/schema/core.cljc) provides a wealth of extra tools for defining schemas, which are described in docstrings. The file [schema/core_test.cljc](https://github.com/plumatic/schema/blob/master/test/cljc/schema/core_test.cljc) demonstrates a variety of sample schemas and many examples of passing & failing clojure data. We'll just touch on a few more examples here, and refer the reader to the code for more details and examples (for now). 239 | 240 | ### Map schemas 241 | 242 | In addition to uniform maps (like `String` to `Double`), map schemas can also capture maps with specific key requirements: 243 | 244 | ```clojure 245 | (s/defschema FooBar {(s/required-key :foo) s/Str (s/required-key :bar) s/Keyword}) 246 | 247 | (s/validate FooBar {:foo "f" :bar :b}) 248 | ;; {:foo "f" :bar :b} 249 | 250 | (s/validate FooBar {:foo :f}) 251 | ;; RuntimeException: Value does not match schema: 252 | ;; {:foo (not (instance? java.lang.String :f)), 253 | ;; :bar missing-required-key} 254 | ``` 255 | 256 | For the special case of keywords, you can omit the `required-key`, like `{:foo s/Str :bar s/Keyword}`. You can also provide specific optional keys, and combine specific keys with generic schemas for the remaining key-value mappings: 257 | 258 | ```clojure 259 | 260 | (s/defschema FancyMap 261 | "If foo is present, it must map to a Keyword. Any number of additional 262 | String-String mappings are allowed as well." 263 | {(s/optional-key :foo) s/Keyword 264 | s/Str s/Str}) 265 | 266 | (s/validate FancyMap {"a" "b"}) 267 | 268 | (s/validate FancyMap {:foo :f "c" "d" "e" "f"}) 269 | ``` 270 | 271 | ### Sequence schemas 272 | 273 | Unlike most schemas, sequence schemas are implicitly nilable: 274 | 275 | ```clojure 276 | (s/validate [s/Any] nil) 277 | ;=> nil 278 | ``` 279 | 280 | You can also write sequence schemas that expect particular values in specific positions 281 | using some regex-like schemas. `s/one` is a named entry (like a singleton `cat` in clojure.spec), 282 | `s/optional` is an optional entry (like `?` in regular expressions), and 283 | a trailing schema describes the rest of the sequence (like `*` in regular expressions). 284 | 285 | ```clojure 286 | (s/defschema FancySeq 287 | "A sequence that starts with a String, followed by an optional Keyword, 288 | followed by any number of Numbers." 289 | [(s/one s/Str "s") 290 | (s/optional s/Keyword "k") 291 | s/Num]) 292 | 293 | (s/validate FancySeq ["test"]) 294 | (s/validate FancySeq ["test" :k]) 295 | (s/validate FancySeq ["test" :k 1 2 3]) 296 | ;; all ok 297 | 298 | (s/validate FancySeq [1 :k 2 3 "4"]) 299 | ;; RuntimeException: Value does not match schema: 300 | ;; [(named (not (instance? java.lang.String 1)) "s") 301 | ;; nil nil nil 302 | ;; (not (instance? java.lang.Number "4"))] 303 | ``` 304 | 305 | ### Set schemas 306 | 307 | A homogeneous set of values is specified by a singleton set. A set of strings is `#{s/Str}`. 308 | 309 | Use `s/conditional` to add additional constraints: 310 | 311 | ```clojure 312 | (s/defn NonEmptySet [s] 313 | (s/conditional 314 | (every-pred set? seq) #{s})) 315 | 316 | (s/validate (NonEmptySet s/Str) #{}) 317 | ;; Fail 318 | (s/validate (NonEmptySet s/Str) #{"a"}) 319 | ;; Ok 320 | ``` 321 | 322 | ### Other schema types 323 | 324 | [`schema.core`](https://github.com/plumatic/schema/blob/master/src/cljc/schema/core.cljc) provides many more utilities for building schemas, including `s/maybe`, `s/eq`, `s/enum`, `s/pred`, `s/conditional`, `s/cond-pre`, `s/constrained`, and more. Here are a few of our favorites: 325 | 326 | ```clojure 327 | ;; anything 328 | (s/validate s/Any "woohoo!") 329 | (s/validate s/Any 'go-nuts) 330 | (s/validate s/Any 42.0) 331 | (s/validate [s/Any] ["woohoo!" 'go-nuts 42.0]) 332 | 333 | ;; maybe (nilable) 334 | (s/validate (s/maybe s/Keyword) :a) 335 | (s/validate (s/maybe s/Keyword) nil) 336 | 337 | ;; eq and enum 338 | (s/validate (s/eq :a) :a) 339 | (s/validate (s/enum :a :b :c) :a) 340 | 341 | ;; pred 342 | (s/validate (s/pred odd?) 1) 343 | 344 | ;; conditional (i.e. variant or option) 345 | (s/defschema StringListOrKeywordMap (s/conditional map? {s/Keyword s/Keyword} :else [String])) 346 | (s/validate StringListOrKeywordMap ["A" "B" "C"]) 347 | ;; => ["A" "B" "C"] 348 | (s/validate StringListOrKeywordMap {:foo :bar}) 349 | ;; => {:foo :bar} 350 | (s/validate StringListOrKeywordMap [:foo]) 351 | ;; RuntimeException: Value does not match schema: [(not (instance? java.lang.String :foo))] 352 | 353 | ;; if (shorthand for conditional) 354 | (s/defschema StringListOrKeywordMap (s/if map? {s/Keyword s/Keyword} [String])) 355 | 356 | ;; cond-pre (experimental), also shorthand for conditional, allows you to skip the 357 | ;; predicate when the options are superficially different by doing a greedy match 358 | ;; on the preconditions of the options. 359 | (s/defschema StringListOrKeywordMap (s/cond-pre {s/Keyword s/Keyword} [String])) 360 | ;; but don't do this -- this will never validate `{:b :x}` because the first schema 361 | ;; will be chosen based on the `map?` precondition (use `if` or `abstract-map-schema` instead): 362 | (s/defschema BadSchema (s/cond-pre {:a s/Keyword} {:b s/Keyword})) 363 | 364 | ;; conditional can also be used to apply extra validation to a single type, 365 | ;; but constrained is often more desirable since it applies the validation 366 | ;; as a *postcondition*, which typically provides better error messages 367 | ;; and works better with coercion 368 | (s/defschema OddLong (s/constrained long odd?)) 369 | (s/validate OddLong 1) 370 | ;; 1 371 | (s/validate OddLong 2) 372 | ;; RuntimeException: Value does not match schema: (not (odd? 2)) 373 | (s/validate OddLong (int 3)) 374 | ;; RuntimeException: Value does not match schema: (not (instance? java.lang.Long 3)) 375 | 376 | ;; recursive 377 | (s/defschema Tree {:value s/Int :children [(s/recursive #'Tree)]}) 378 | (s/validate Tree {:value 0, :children [{:value 1, :children []}]}) 379 | 380 | ;; abstract-map (experimental) models "abstract classes" and "subclasses" with maps. 381 | (require '[schema.experimental.abstract-map :as abstract-map]) 382 | (s/defschema Animal 383 | (abstract-map/abstract-map-schema 384 | :type 385 | {:name s/Str})) 386 | (abstract-map/extend-schema Cat Animal [:cat] {:claws? s/Bool}) 387 | (abstract-map/extend-schema Dog Animal [:dog] {:barks? s/Bool}) 388 | (s/validate Cat {:type :cat :name "melvin" :claws? true}) 389 | (s/validate Animal {:type :cat :name "melvin" :claws? true}) 390 | (s/validate Animal {:type :dog :name "roofer" :barks? true}) 391 | (s/validate Animal {:type :cat :name "confused kitty" :barks? true}) 392 | ;; RuntimeException: 393 | ;; Value does not match schema: {:claws? missing-required-key, :barks? disallowed-key} 394 | ``` 395 | 396 | You can also define schemas for [recursive data types](https://github.com/plumatic/schema/wiki/Recursive-Schemas), or create [your own custom schemas types](https://github.com/plumatic/schema/wiki/Defining-New-Schema-Types-1.0). 397 | 398 | ## Transformations and Coercion 399 | 400 | Schema also supports schema-driven data transformations, with *coercion* being the main application fleshed out thus far. Coercion is like validation, except a schema-dependent transformation can be applied to the input data before validation. 401 | 402 | An example application of coercion is converting parsed JSON (e.g., from an HTTP post request) to a domain object with a richer set of types (e.g., Keywords). 403 | 404 | ```clojure 405 | (s/defschema CommentRequest 406 | {(s/optional-key :parent-comment-id) long 407 | :text String 408 | :share-services [(s/enum :twitter :facebook :google)]}) 409 | 410 | (def parse-comment-request 411 | (coerce/coercer CommentRequest coerce/json-coercion-matcher)) 412 | 413 | (= (parse-comment-request 414 | {:parent-comment-id (int 2128123123) 415 | :text "This is awesome!" 416 | :share-services ["twitter" "facebook"]}) 417 | {:parent-comment-id 2128123123 418 | :text "This is awesome!" 419 | :share-services [:twitter :facebook]}) 420 | ;; ==> true 421 | ``` 422 | 423 | Here, `json-coercion-matcher` provides some useful defaults for coercing from JSON, such as: 424 | 425 | - Numbers should be coerced to the expected type, if this can be done without losing precision. 426 | - When a Keyword is expected, a String can be coerced to the correct type by calling keyword 427 | 428 | There's nothing special about `json-coercion-matcher` though; it's just as easy to [make your own schema-specific transformations](https://github.com/plumatic/schema/wiki/Writing-Custom-Transformations) to do even more. 429 | 430 | For more details, see [this blog post](https://plumatic.github.io//schema-0-2-0-back-with-clojurescript-data-coercion). 431 | 432 | ## For the Future 433 | 434 | Longer-term, we have lots more in store for Schema. Just a couple of the crazy ideas we have brewing are: 435 | - Automatically generate API client libraries based on API schemas 436 | - Compile to `core.typed` annotations for more typey goodness, if that's your thing 437 | 438 | ## Community 439 | 440 | Please feel free to join the Plumbing [mailing list](https://groups.google.com/forum/#!forum/prismatic-plumbing) to ask questions or discuss how you're using Schema. 441 | 442 | We welcome contributions in the form of bug reports and pull requests; please see `CONTRIBUTING.md` in the repo root for guidelines. Libraries that extend `schema` with new functionality are great too; here are a few that we know of: 443 | 444 | - https://github.com/metosin/schema-tools has lots of useful utilities for working with schemas 445 | - https://github.com/cddr/integrity includes a variety of extensions, including helpers for producing error messages suitable for end-users. 446 | - https://github.com/gfredericks/schema-bijections has support for bijections, which are like a precise, two-way version of coercion, created for use with JSON APIs. 447 | - https://github.com/outpace/schema-transit couples Schema to Cognitect's Transit library 448 | - https://github.com/plumatic/schema-generators provides out-of-the box generation and partial datum completion from Schemas. 449 | - https://github.com/KitApps/schema-refined provides `constrained` and `conditional` on steroids to make your schemas as precise as it's possible using set of flexible and composable predicates 450 | - https://github.com/vodori/schema-conformer provides a more advanced coercion matcher with optional transformations like removing extra map keys, initializing default values, and converting between vectors and sets. 451 | - https://github.com/vodori/schema-forms provides a converter from Schema to JSON Schema. 452 | 453 | If you make something new, please feel free to PR to add it here! 454 | 455 | ## Supported Clojure versions 456 | 457 | Schema is currently supported on Clojure 1.8 onwards, [Babashka](https://github.com/babashka/babashka) 0.8.156 onwards, and the latest version of ClojureScript. 458 | 459 | ## License 460 | 461 | Distributed under the Eclipse Public License, the same as Clojure. 462 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/clj" "src/cljc" "test/clj" "test/cljc" "test/bb"] 2 | ;:deps {org.clojure/test.check {:mvn/version "1.1.1"}} 3 | :tasks 4 | {:requires ([babashka.fs :as fs] 5 | [babashka.process :as p :refer [process]] 6 | [babashka.wait :as wait]) 7 | nrepl (let [port (with-open [sock (java.net.ServerSocket. 0)] (.getLocalPort sock)) 8 | proc (process (str "bb nrepl-server " port) {:inherit true})] 9 | (wait/wait-for-port "localhost" port) 10 | (spit ".nrepl-port" port) 11 | (fs/delete-on-exit ".nrepl-port") 12 | (deref proc)) 13 | test (require 'schema.bb-test-runner)}} 14 | -------------------------------------------------------------------------------- /bin/push_docs_for_current_commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Script to generate docs and push to github pages. 5 | # https://github.com/weavejester/codox/wiki/Deploying-to-GitHub-Pages 6 | lein doc 7 | git fetch origin 8 | git checkout gh-pages # To be sure you're on the right branch 9 | git reset --hard gh-pages 10 | git pull origin gh-pages 11 | # gh-pages is currently deployed from root (not `doc` folder) 12 | rm *.html 13 | rm -fr js css doc 14 | mv target/doc/* . 15 | git add . 16 | git commit --allow-empty -am "new documentation push." 17 | git push -u origin gh-pages 18 | git checkout - 19 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | lein release 4 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src/clj" "src/cljc"] 2 | :aliases {:test {:extra-paths ["test/clj" "test/cljc" "test/cljs"]}}} 3 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject prismatic/schema "1.4.2-SNAPSHOT" 2 | :description "Clojure(Script) library for declarative data description and validation" 3 | :url "http://github.com/plumatic/schema" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.8.0"] 8 | [org.clojure/clojurescript "1.10.520"] 9 | [org.clojure/tools.nrepl "0.2.5"] 10 | [org.clojure/test.check "1.1.1"] 11 | [potemkin "0.4.1"]] 12 | :eastwood {:exclude-namespaces [] 13 | :exclude-linters [:def-in-def :local-shadows-var :constant-test :suspicious-expression :deprecations 14 | :unused-meta-on-macro :wrong-tag :unused-ret-vals]} 15 | :plugins [[lein-codox "0.10.8"] 16 | [lein-cljsbuild "1.1.7"] 17 | [lein-doo "0.1.10"] 18 | [lein-pprint "1.3.2"] 19 | [lein-shell "0.5.0"] 20 | [jonase/eastwood "1.2.3"]]} 21 | :1.9 {:dependencies [[org.clojure/clojure "1.9.0"] [org.clojure/clojurescript "1.10.520"]]} 22 | :1.10 {:dependencies [[org.clojure/clojure "1.10.3"] [org.clojure/clojurescript "1.10.879"]]} 23 | :1.11 {:dependencies [[org.clojure/clojure "1.11.1"] [org.clojure/clojurescript "1.11.4"]]} 24 | :1.12 {:dependencies [[org.clojure/clojure "1.12.0-master-SNAPSHOT"] [org.clojure/clojurescript "1.11.4"]] 25 | :repositories [["sonatype-oss-public" {:url "https://oss.sonatype.org/content/groups/public"}]]}} 26 | 27 | :aliases {"all" ["with-profile" "+dev:+1.9:+1.10:+1.11:+1.12"] 28 | "deploy" ["do" "clean," "deploy" "clojars"] 29 | "test" ["do" "clean," "test," "doo" "node" "test" "once"] 30 | "doc" ["codox"]} 31 | 32 | :jar-exclusions [#"\.swp|\.swo|\.DS_Store"] 33 | 34 | :source-paths ["src/clj" "src/cljc"] 35 | 36 | :test-paths ["test/clj" "test/cljc" "test/cljs"] 37 | 38 | :cljsbuild {:builds 39 | [{:id "dev" 40 | :source-paths ["src/clj" "src/cljc"] 41 | :compiler {:output-to "target/main.js" 42 | :optimizations :whitespace 43 | :pretty-print true}} 44 | {:id "test" 45 | :source-paths ["src/clj" "src/cljc" 46 | "test/clj" "test/cljc" "test/cljs"] 47 | :compiler {:output-to "target/unit-test.js" 48 | :main schema.test-runner 49 | :target :nodejs 50 | :pretty-print true}} 51 | {:id "test-no-assert" 52 | :source-paths ["src/clj" "src/cljc" 53 | "test/clj" "test/cljc" "test/cljs"] 54 | :assert false 55 | :compiler {:output-to "target/unit-test.js" 56 | :main schema.test-runner 57 | :target :nodejs 58 | :pretty-print true}}]} 59 | 60 | :codox {:source-uri "https://github.com/plumatic/schema/blob/{git-commit}/{filepath}#L{line}"} 61 | 62 | :release-tasks [["vcs" "assert-committed"] 63 | ["change" "version" "leiningen.release/bump-version" "release"] 64 | ["vcs" "commit"] 65 | ["vcs" "tag"] 66 | ["deploy"] 67 | ["shell" "./bin/push_docs_for_current_commit.sh"] 68 | ["change" "version" "leiningen.release/bump-version"] 69 | ["vcs" "commit"] 70 | ["vcs" "push"] 71 | ["shell" "git" "push" "origin" "master" "--tags"]] 72 | 73 | :signing {:gpg-key "66E0BF75"}) 74 | -------------------------------------------------------------------------------- /resources/clj-kondo.exports/prismatic/schema/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {schema.test/deftest clojure.test/deftest}} 2 | -------------------------------------------------------------------------------- /src/clj/schema/experimental/complete.clj: -------------------------------------------------------------------------------- 1 | (ns schema.experimental.complete 2 | "Deprecated---please migrate to https://github.com/plumatic/schema-generators 3 | 4 | (Extremely) experimental support for 'completing' partial datums to match 5 | a schema. To use it, you must provide your own test.check dependency." 6 | {:deprecated "1.1.0" 7 | :superceded-by "schema-generators.complete"} 8 | (:require 9 | [clojure.test.check.generators :as check-generators] 10 | [schema.spec.core :as spec] 11 | schema.spec.collection 12 | schema.spec.leaf 13 | schema.spec.variant 14 | [schema.coerce :as coerce] 15 | [schema.core :as s] 16 | [schema.macros :as macros] 17 | [schema.utils :as utils] 18 | [schema.experimental.generators :as generators])) 19 | 20 | (def +missing+ ::missing) 21 | 22 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 23 | ;;; Private helpers 24 | 25 | (defprotocol Completer 26 | (completer* [spec s sub-checker generator-opts] 27 | "A function applied to a datum as part of coercion to complete missing fields.")) 28 | 29 | (defn sample [g] 30 | (check-generators/generate g 10)) 31 | 32 | (extend-protocol Completer 33 | schema.spec.variant.VariantSpec 34 | (completer* [spec s sub-checker generator-opts] 35 | (let [g (apply generators/generator s generator-opts)] 36 | (if (and (class? s) (isa? s clojure.lang.IRecord) (utils/class-schema s)) 37 | (fn record-completer [x] 38 | (sub-checker (into (sample g) x))) 39 | (fn variant-completer [x] 40 | (if (= +missing+ x) 41 | (sample g) 42 | (sub-checker x)))))) 43 | 44 | schema.spec.collection.CollectionSpec 45 | (completer* [spec s sub-checker generator-opts] 46 | (if (instance? clojure.lang.APersistentMap s) ;; todo: pluggable 47 | (let [g (apply generators/generator s generator-opts)] 48 | (fn map-completer [x] 49 | (if (= +missing+ x) 50 | (sample g) 51 | ;; for now, just do required keys when user provides input. 52 | (let [ks (distinct (concat (keys x) 53 | (->> s 54 | keys 55 | (filter s/required-key?) 56 | (map s/explicit-schema-key))))] 57 | (sub-checker 58 | (into {} (for [k ks] [k (get x k +missing+)]))))))) 59 | (let [g (apply generators/generator s generator-opts)] 60 | (fn coll-completer [x] 61 | (if (= +missing+ x) 62 | (sample g) 63 | (sub-checker x)))))) 64 | 65 | schema.spec.leaf.LeafSpec 66 | (completer* [spec s sub-checker generator-opts] 67 | (let [g (apply generators/generator s generator-opts)] 68 | (fn leaf-completer [x] 69 | (if (= +missing+ x) 70 | (sample g) 71 | x))))) 72 | 73 | 74 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 75 | ;;; Public 76 | 77 | (s/defn completer 78 | "Produce a function that simultaneously coerces, completes, and validates a datum." 79 | ([schema] (completer schema {})) 80 | ([schema coercion-matcher] (completer schema coercion-matcher {})) 81 | ([schema coercion-matcher leaf-generators] 82 | (completer schema coercion-matcher leaf-generators {})) 83 | ([schema 84 | coercion-matcher :- coerce/CoercionMatcher 85 | leaf-generators :- generators/LeafGenerators 86 | wrappers :- generators/GeneratorWrappers] 87 | (spec/run-checker 88 | (fn [s params] 89 | (let [c (spec/checker (s/spec s) params) 90 | coercer (or (coercion-matcher s) identity) 91 | completr (completer* (s/spec s) s c [leaf-generators wrappers])] 92 | (fn [x] 93 | (macros/try-catchall 94 | (let [v (coercer x)] 95 | (if (utils/error? v) 96 | v 97 | (completr v))) 98 | (catch t (macros/validation-error s x t)))))) 99 | true 100 | schema))) 101 | 102 | (defn complete 103 | "Fill in partial-datum to make it validate schema." 104 | [partial-datum & completer-args] 105 | ((apply completer completer-args) partial-datum)) 106 | -------------------------------------------------------------------------------- /src/clj/schema/experimental/generators.clj: -------------------------------------------------------------------------------- 1 | (ns schema.experimental.generators 2 | "Deprecated---please migrate to https://github.com/plumatic/schema-generators 3 | 4 | (Very) experimental support for compiling schemas to test.check generators. 5 | To use it, you must provide your own test.check dependency. 6 | 7 | TODO: add cljs support." 8 | {:deprecated "1.1.0" 9 | :superceded-by "schema-generators.generators"} 10 | (:require 11 | [clojure.test.check.generators :as generators] 12 | [schema.spec.core :as spec] 13 | schema.spec.collection 14 | schema.spec.leaf 15 | schema.spec.variant 16 | [schema.core :as s] 17 | [schema.macros :as macros])) 18 | 19 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 20 | ;;; Private helpers for composite schemas 21 | 22 | (defn g-by [f & args] 23 | (generators/fmap 24 | (partial apply f) 25 | (apply generators/tuple args))) 26 | 27 | (defn g-apply-by [f args] 28 | (generators/fmap f (apply generators/tuple args))) 29 | 30 | (defn- sub-generator 31 | [{:keys [schema]} 32 | {:keys [subschema-generator ^java.util.Map cache] :as params}] 33 | (spec/with-cache cache schema 34 | (fn [d] (#'generators/make-gen (fn [r s] (generators/call-gen @d r (quot s 2))))) 35 | (fn [] (subschema-generator schema params)))) 36 | 37 | 38 | ;; Helpers for collections 39 | 40 | (declare elements-generator) 41 | 42 | (defn element-generator [e params] 43 | (if (vector? e) 44 | (case (first e) 45 | :schema.spec.collection/optional 46 | (generators/one-of 47 | [(generators/return nil) 48 | (elements-generator (next e) params)]) 49 | 50 | :schema.spec.collection/remaining 51 | (do (macros/assert! (= 2 (count e)) "remaining can have only one schema.") 52 | (generators/vector (sub-generator (second e) params)))) 53 | (generators/fmap vector (sub-generator e params)))) 54 | 55 | (defn elements-generator [elts params] 56 | (->> elts 57 | (map #(element-generator % params)) 58 | (apply generators/tuple) 59 | (generators/fmap (partial apply concat)))) 60 | 61 | (defprotocol CompositeGenerator 62 | (composite-generator [s params])) 63 | 64 | (extend-protocol CompositeGenerator 65 | schema.spec.variant.VariantSpec 66 | (composite-generator [s params] 67 | (generators/such-that 68 | (fn [x] 69 | (let [pre (.-pre ^schema.spec.variant.VariantSpec s) 70 | post (.-post ^schema.spec.variant.VariantSpec s)] 71 | (not 72 | (or (pre x) 73 | (and post (post x)))))) 74 | (generators/one-of 75 | (for [o (macros/safe-get s :options)] 76 | (if-let [g (:guard o)] 77 | (generators/such-that g (sub-generator o params)) 78 | (sub-generator o params)))))) 79 | 80 | ;; TODO: this does not currently capture proper semantics of maps with 81 | ;; both specific keys and key schemas that can override them. 82 | schema.spec.collection.CollectionSpec 83 | (composite-generator [s params] 84 | (generators/such-that 85 | (complement (.-pre ^schema.spec.collection.CollectionSpec s)) 86 | (generators/fmap (:konstructor s) (elements-generator (:elements s) params)))) 87 | 88 | schema.spec.leaf.LeafSpec 89 | (composite-generator [s params] 90 | (macros/assert! false "You must provide a leaf generator for %s" s))) 91 | 92 | 93 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 94 | ;;; Public 95 | 96 | (def Schema 97 | "A Schema for Schemas" 98 | (s/protocol s/Schema)) 99 | 100 | (def Generator 101 | "A test.check generator" 102 | s/Any) 103 | 104 | (def LeafGenerators 105 | "A mapping from schemas to generating functions that should be used." 106 | (s/=> (s/maybe Generator) Schema)) 107 | 108 | (def +primitive-generators+ 109 | {Double generators/double 110 | ;; using unchecked-float here will unfortunately generate a lot of 111 | ;; infinities, since lots of doubles are out of the float range 112 | Float (generators/fmap unchecked-float generators/double) 113 | Long generators/large-integer 114 | Integer (generators/fmap unchecked-int generators/large-integer) 115 | Short (generators/fmap unchecked-short generators/large-integer) 116 | Character (generators/fmap unchecked-char generators/large-integer) 117 | Byte (generators/fmap unchecked-byte generators/large-integer) 118 | Boolean generators/boolean}) 119 | 120 | (def +simple-leaf-generators+ 121 | (merge 122 | +primitive-generators+ 123 | {s/Str generators/string-ascii 124 | s/Bool generators/boolean 125 | s/Num (generators/one-of [generators/large-integer generators/double]) 126 | s/Int (generators/one-of 127 | [generators/large-integer 128 | (generators/fmap unchecked-int generators/large-integer) 129 | (generators/fmap bigint generators/large-integer)]) 130 | s/Keyword generators/keyword 131 | clojure.lang.Keyword generators/keyword 132 | s/Symbol (generators/fmap (comp symbol name) generators/keyword) 133 | Object generators/any 134 | s/Any generators/any 135 | s/Uuid generators/uuid 136 | s/Inst (generators/fmap (fn [^long ms] (java.util.Date. ms)) generators/int)} 137 | (into {} 138 | (for [[f ctor c] [[doubles double-array Double] 139 | [floats float-array Float] 140 | [longs long-array Long] 141 | [ints int-array Integer] 142 | [shorts short-array Short] 143 | [chars char-array Character] 144 | [bytes byte-array Byte] 145 | [booleans boolean-array Boolean]]] 146 | [f (generators/fmap ctor (generators/vector (macros/safe-get +primitive-generators+ c)))])))) 147 | 148 | (defn eq-generators [s] 149 | (when (instance? schema.core.EqSchema s) 150 | (generators/return (.-v ^schema.core.EqSchema s)))) 151 | 152 | (defn enum-generators [s] 153 | (when (instance? schema.core.EnumSchema s) 154 | (let [vs (vec (.-vs ^schema.core.EnumSchema s))] 155 | (generators/fmap #(nth vs %) (generators/choose 0 (dec (count vs))))))) 156 | 157 | 158 | (defn default-leaf-generators 159 | [leaf-generators] 160 | (some-fn 161 | leaf-generators 162 | +simple-leaf-generators+ 163 | eq-generators 164 | enum-generators)) 165 | 166 | (defn always [x] (generators/return x)) 167 | 168 | (def GeneratorWrappers 169 | "A mapping from schemas to wrappers that should be used around the default 170 | generators." 171 | (s/=> (s/maybe (s/=> Generator Generator)) 172 | Schema)) 173 | 174 | (defn such-that 175 | "Helper wrapper that filters to values that match predicate." 176 | [f] 177 | (partial generators/such-that f)) 178 | 179 | (defn fmap 180 | "Helper wrapper that maps f over all values." 181 | [f] 182 | (partial generators/fmap f)) 183 | 184 | (defn merged 185 | "Helper wrapper that merges some keys into a schema" 186 | [m] 187 | (fmap #(merge % m))) 188 | 189 | (s/defn generator :- Generator 190 | "Produce a test.check generator for schema. 191 | 192 | leaf-generators must return generators for all leaf schemas, and can also return 193 | generators for non-leaf schemas to override default generation logic. 194 | 195 | constraints is an optional mapping from schema to wrappers for the default generators, 196 | which can impose constraints, fix certain values, etc." 197 | ([schema] 198 | (generator schema {})) 199 | ([schema leaf-generators] 200 | (generator schema leaf-generators {})) 201 | ([schema :- Schema 202 | leaf-generators :- LeafGenerators 203 | wrappers :- GeneratorWrappers] 204 | (let [leaf-generators (default-leaf-generators leaf-generators) 205 | gen (fn [s params] 206 | ((or (wrappers s) identity) 207 | (or (leaf-generators s) 208 | (composite-generator (s/spec s) params))))] 209 | (generators/fmap 210 | (s/validator schema) 211 | (gen schema {:subschema-generator gen :cache (java.util.IdentityHashMap.)}))))) 212 | 213 | (s/defn sample :- [s/Any] 214 | "Sample k elements from generator." 215 | [k & generator-args] 216 | (generators/sample (apply generator generator-args) k)) 217 | 218 | (s/defn generate 219 | "Sample a single element of low to moderate size." 220 | [& generator-args] 221 | (generators/generate (apply generator generator-args) 10)) 222 | -------------------------------------------------------------------------------- /src/clj/schema/macros.clj: -------------------------------------------------------------------------------- 1 | (ns schema.macros 2 | "Macros and macro helpers used in schema.core." 3 | (:refer-clojure :exclude [simple-symbol?]) 4 | (:require 5 | [clojure.string :as str] 6 | [schema.utils :as utils])) 7 | 8 | ;; can remove this once we drop Clojure 1.8 support 9 | (defn- simple-symbol? [x] 10 | (and (symbol? x) 11 | (not (namespace x)))) 12 | 13 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 14 | ;;; Helpers used in schema.core. 15 | 16 | (defn cljs-env? 17 | "Take the &env from a macro, and tell whether we are expanding into cljs." 18 | [env] 19 | (boolean (:ns env))) 20 | 21 | (defmacro if-cljs 22 | "Return then if we are generating cljs code and else for Clojure code. 23 | https://groups.google.com/d/msg/clojurescript/iBY5HaQda4A/w1lAQi9_AwsJ" 24 | [then else] 25 | (if (cljs-env? &env) then else)) 26 | 27 | (def bb? (boolean (System/getProperty "babashka.version"))) 28 | 29 | (defmacro if-bb 30 | [then else] 31 | (if bb? then else)) 32 | 33 | (defmacro try-catchall 34 | "A cross-platform variant of try-catch that catches all* exceptions. 35 | Does not (yet) support finally, and does not need or want an exception class. 36 | 37 | *On the JVM certain fatal exceptions are not caught." 38 | [& body] 39 | (let [try-body (butlast body) 40 | [catch sym & catch-body :as catch-form] (last body)] 41 | (assert (= catch 'catch)) 42 | (assert (symbol? sym)) 43 | `(if-cljs 44 | (try ~@try-body (~'catch js/Object ~sym ~@catch-body)) 45 | (try 46 | ~@try-body 47 | ;; this whitelist is shamelessly copied from scala 48 | ;; https://github.com/scala/scala/blob/2.13.x/src/library/scala/util/control/NonFatal.scala#L42 49 | (~'catch VirtualMachineError e# (throw e#)) 50 | (~'catch ThreadDeath e# (throw e#)) 51 | (~'catch InterruptedException e# (throw e#)) 52 | (~'catch LinkageError e# (throw e#)) 53 | (~'catch Throwable ~sym ~@catch-body))))) 54 | 55 | (defmacro error! 56 | "Generate a cross-platform exception appropriate to the macroexpansion context" 57 | ([s] 58 | `(if-cljs 59 | (throw (js/Error. ~s)) 60 | (throw (RuntimeException. ~(with-meta s `{:tag java.lang.String}))))) 61 | ([s m] 62 | (let [m (merge {:type :schema.core/error} m)] 63 | `(if-cljs 64 | (throw (ex-info ~s ~m)) 65 | (throw (clojure.lang.ExceptionInfo. ~(with-meta s `{:tag java.lang.String}) ~m)))))) 66 | 67 | (defmacro safe-get 68 | "Like get but throw an exception if not found. A macro for historical reasons (to 69 | work around cljx function placement restrictions)." 70 | [m k] 71 | `(let [m# ~m k# ~k] 72 | (if-let [pair# (find m# k#)] 73 | (val pair#) 74 | (error! (utils/format* "Key %s not found in %s" k# m#))))) 75 | 76 | (defmacro assert! 77 | "Like assert, but throws a RuntimeException (in Clojure) and takes args to format." 78 | [form & format-args] 79 | `(when-not ~form 80 | (error! (utils/format* ~@format-args)))) 81 | 82 | (defmacro validation-error [schema value expectation & [fail-explanation]] 83 | `(schema.utils/error 84 | (utils/make-ValidationError ~schema ~value (delay ~expectation) ~fail-explanation))) 85 | 86 | (defmacro defrecord-schema 87 | "Like defrecord for schema primitives, and also registers cross-platform print methods." 88 | [n & args] 89 | `(do (clojure.core/defrecord ~n ~@args) 90 | (if-cljs (extend-protocol cljs.core/IPrintWithWriter 91 | ~n 92 | (~'-pr-writer [s# w# _#] 93 | (cljs.core/-write w# (schema.core/explain s#)))) 94 | ;; bb doesn't support multimethods extended via protocols yet 95 | (if-bb (schema.core/register-schema-print-as-explain ~n) 96 | ;; relies on (register-schema-print-as-explain schema.core.Schema) 97 | nil)) 98 | ~n)) 99 | 100 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 101 | ;;; Helpers for processing and normalizing element/argument schemas in s/defrecord and s/(de)fn 102 | 103 | (defn maybe-split-first [pred s] 104 | (if (pred (first s)) 105 | [(first s) (next s)] 106 | [nil s])) 107 | 108 | (def primitive-sym? '#{float double boolean byte char short int long 109 | floats doubles booleans bytes chars shorts ints longs objects}) 110 | 111 | (defn resolve-tag 112 | "Given a Symbol, attempt to return a valid Clojure tag else nil. 113 | 114 | Symbols not contained in `primitive-sym?` will be resolved. Symbols 115 | resolved to Vars have their values checked in an attempt to provide 116 | type hints when possible. 117 | 118 | A valid tag is a primitive, Class, or Var containing a Class." 119 | [env tag] 120 | (when (symbol? tag) 121 | (let [resolved (delay (resolve env tag))] 122 | (cond 123 | (or (primitive-sym? tag) (class? @resolved)) 124 | tag 125 | 126 | (var? @resolved) 127 | (let [v (var-get @resolved)] 128 | (when (class? v) 129 | (symbol (.getName ^Class v)))))))) 130 | 131 | (defn normalized-metadata 132 | "Take an object with optional metadata, which may include a :tag, 133 | plus an optional explicit schema, and normalize the 134 | object to have a valid Clojure :tag plus a :schema field." 135 | [env imeta explicit-schema] 136 | (let [{:keys [tag s s? schema]} (meta imeta)] 137 | (assert! (not (or s s?)) "^{:s schema} style schemas are no longer supported.") 138 | (assert! (< (count (remove nil? [schema explicit-schema])) 2) 139 | "Expected single schema, got meta %s, explicit %s" (meta imeta) explicit-schema) 140 | (let [schema (or explicit-schema schema tag `schema.core/Any)] 141 | (with-meta imeta 142 | (-> (or (meta imeta) {}) 143 | (dissoc :tag) 144 | (utils/assoc-when :schema schema 145 | :tag (resolve-tag env (or tag schema)))))))) 146 | 147 | (defn extract-schema-form 148 | "Pull out the schema stored on a thing. Public only because of its use in a public macro." 149 | [symbol] 150 | (let [s (:schema (meta symbol))] 151 | (assert! s "%s is missing a schema" symbol) 152 | s)) 153 | 154 | (defn extract-arrow-schematized-element 155 | "Take a nonempty seq, which may start like [a ...] or [a :- schema ...], and return 156 | a list of [first-element-with-schema-attached rest-elements]" 157 | [env s] 158 | (assert (seq s)) 159 | (let [[f & more] s] 160 | (if (= :- (first more)) 161 | [(normalized-metadata env f (second more)) (drop 2 more)] 162 | [(normalized-metadata env f nil) more]))) 163 | 164 | (defn process-arrow-schematized-args 165 | "Take an arg vector, in which each argument is followed by an optional :- schema, 166 | and transform into an ordinary arg vector where the schemas are metadata on the args." 167 | [env args] 168 | (loop [in args out []] 169 | (if (empty? in) 170 | out 171 | (let [[arg more] (extract-arrow-schematized-element env in)] 172 | (recur more (conj out arg)))))) 173 | 174 | 175 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 176 | ;;; Helpers for schematized fn/defn 177 | 178 | (defn split-rest-arg [env bind] 179 | (let [[pre-& [_ rest-arg :as post-&]] (split-with #(not= % '&) bind)] 180 | (if (seq post-&) 181 | (do (assert! (= (count post-&) 2) "& must be followed by a single binding" (vec post-&)) 182 | (assert! (or (symbol? rest-arg) 183 | (and (vector? rest-arg) 184 | (not-any? #{'&} rest-arg))) 185 | "Bad & binding form: currently only bare symbols and vectors supported" (vec post-&)) 186 | 187 | [(vec pre-&) 188 | (if (vector? rest-arg) 189 | (with-meta (process-arrow-schematized-args env rest-arg) (meta rest-arg)) 190 | rest-arg)]) 191 | [bind nil]))) 192 | 193 | (defn single-arg-schema-form [rest? [index arg]] 194 | `(~(if rest? `schema.core/optional `schema.core/one) 195 | ~(extract-schema-form arg) 196 | ~(if (symbol? arg) 197 | `'~arg 198 | `'~(symbol (str (if rest? "rest" "arg") index))))) 199 | 200 | (defn simple-arglist-schema-form [rest? regular-args] 201 | (mapv (partial single-arg-schema-form rest?) (map-indexed vector regular-args))) 202 | 203 | (defn rest-arg-schema-form [arg] 204 | (let [s (extract-schema-form arg)] 205 | (if (= s `schema.core/Any) 206 | (if (vector? arg) 207 | (simple-arglist-schema-form true arg) 208 | [`schema.core/Any]) 209 | (do (assert! (vector? s) "Expected seq schema for rest args, got %s" s) 210 | s)))) 211 | 212 | (defn input-schema-form [regular-args rest-arg] 213 | (let [base (simple-arglist-schema-form false regular-args)] 214 | (if rest-arg 215 | (vec (concat base (rest-arg-schema-form rest-arg))) 216 | base))) 217 | 218 | (defn apply-prepost-conditions 219 | "Replicate pre/postcondition logic from clojure.core/fn." 220 | [body] 221 | (let [[conds body] (maybe-split-first #(and (map? %) (next body)) body)] 222 | (concat (map (fn [c] `(assert ~c)) (:pre conds)) 223 | (if-let [post (:post conds)] 224 | `((let [~'% (do ~@body)] 225 | ~@(map (fn [c] `(assert ~c)) post) 226 | ~'%)) 227 | body)))) 228 | 229 | (def ^:dynamic *compile-fn-validation* (atom true)) 230 | 231 | (defn compile-fn-validation? 232 | "Returns true if validation should be included at compile time, otherwise false. 233 | Validation is elided for any of the following cases: 234 | * function has :never-validate metadata 235 | * *compile-fn-validation* is false 236 | * *assert* is false AND function is not :always-validate" 237 | [env fn-name] 238 | (let [fn-meta (meta fn-name)] 239 | (and 240 | @*compile-fn-validation* 241 | (not (:never-validate fn-meta)) 242 | (or (:always-validate fn-meta) 243 | *assert*)))) 244 | 245 | (defn process-fn-arity 246 | "Process a single (bind & body) form, producing an output tag, schema-form, 247 | and arity-form which has asserts for validation purposes added that are 248 | executed when turned on, and have very low overhead otherwise. 249 | tag? is a prospective tag for the fn symbol based on the output schema. 250 | schema-bindings are bindings to lift eval outwards, so we don't build the schema 251 | every time we do the validation. 252 | 253 | :ufv-sym should name a local binding bound to `schema.utils/use-fn-validation`. 254 | 255 | 5-args arity is deprecated." 256 | ([env fn-name output-schema-sym bind-meta arity-form] 257 | (process-fn-arity {:env env :fn-name fn-name :output-schema-sym output-schema-sym 258 | :bind-meta bind-meta :arity-form arity-form :ufv-sym 'ufv__})) 259 | ([{[bind & body] :arity-form :keys [env fn-name output-schema-sym bind-meta ufv-sym]}] 260 | (assert! (vector? bind) "Got non-vector binding form %s" bind) 261 | (when-let [bad-meta (seq (filter (or (meta bind) {}) [:tag :s? :s :schema]))] 262 | (throw (RuntimeException. (str "Meta not supported on bindings, put on fn name" (vec bad-meta))))) 263 | (let [original-arglist bind 264 | bind (with-meta (process-arrow-schematized-args env bind) bind-meta) 265 | [regular-args rest-arg] (split-rest-arg env bind) 266 | input-schema-sym (gensym "input-schema") 267 | input-checker-sym (gensym "input-checker") 268 | output-checker-sym (gensym "output-checker") 269 | compile-validation (compile-fn-validation? env fn-name)] 270 | {:schema-binding [input-schema-sym (input-schema-form regular-args rest-arg)] 271 | :more-bindings (when compile-validation 272 | [input-checker-sym `(delay (schema.core/checker ~input-schema-sym)) 273 | output-checker-sym `(delay (schema.core/checker ~output-schema-sym))]) 274 | :arglist bind 275 | :raw-arglist original-arglist 276 | :arity-form (if compile-validation 277 | (let [bind-syms (vec (repeatedly (count regular-args) gensym)) 278 | rest-sym (when rest-arg (gensym "rest")) 279 | metad-bind-syms (with-meta (mapv #(with-meta %1 (meta %2)) bind-syms bind) bind-meta)] 280 | (list 281 | (if rest-arg 282 | (into metad-bind-syms ['& rest-sym]) 283 | metad-bind-syms) 284 | `(let [validate# ~(if (:always-validate (meta fn-name)) 285 | `true 286 | `(if-cljs (deref ~ufv-sym) 287 | (if-bb (deref ~ufv-sym) (.get ~ufv-sym))))] 288 | (when validate# 289 | (let [args# ~(if rest-arg 290 | `(list* ~@bind-syms ~rest-sym) 291 | bind-syms)] 292 | (if schema.core/fn-validator 293 | (schema.core/fn-validator :input 294 | '~fn-name 295 | ~input-schema-sym 296 | @~input-checker-sym 297 | args#) 298 | (when-let [error# (@~input-checker-sym args#)] 299 | (error! (utils/format* "Input to %s does not match schema: \n\n\t \033[0;33m %s \033[0m \n\n" 300 | '~fn-name (pr-str error#)) 301 | {:schema ~input-schema-sym :value args# :error error#}))))) 302 | (let [o# (loop ~(into (vec (interleave (map #(with-meta % {}) bind) bind-syms)) 303 | (when rest-arg [rest-arg rest-sym])) 304 | ~@(apply-prepost-conditions body))] 305 | (when validate# 306 | (if schema.core/fn-validator 307 | (schema.core/fn-validator :output 308 | '~fn-name 309 | ~output-schema-sym 310 | @~output-checker-sym 311 | o#) 312 | (when-let [error# (@~output-checker-sym o#)] 313 | (error! (utils/format* "Output of %s does not match schema: \n\n\t \033[0;33m %s \033[0m \n\n" 314 | '~fn-name (pr-str error#)) 315 | {:schema ~output-schema-sym :value o# :error error#})))) 316 | o#)))) 317 | (cons (into regular-args (when rest-arg ['& rest-arg])) 318 | body))}))) 319 | 320 | (defn process-fn- 321 | "Process the fn args into a final tag proposal, schema form, schema bindings, and fn form" 322 | [env name fn-body] 323 | (let [compile-validation (compile-fn-validation? env name) 324 | output-schema (extract-schema-form name) 325 | output-schema-sym (gensym "output-schema") 326 | bind-meta (or (when-let [t (:tag (meta name))] 327 | (when (primitive-sym? t) 328 | {:tag t})) 329 | {}) 330 | ufv-sym (gensym "ufv") 331 | processed-arities (map #(process-fn-arity {:env env :fn-name name :output-schema-sym output-schema-sym 332 | :bind-meta bind-meta :arity-form % :ufv-sym ufv-sym}) 333 | (if (vector? (first fn-body)) 334 | [fn-body] 335 | fn-body)) 336 | schema-bindings (map :schema-binding processed-arities) 337 | fn-forms (map :arity-form processed-arities)] 338 | {:outer-bindings (vec (concat 339 | (when compile-validation 340 | `[~(with-meta ufv-sym {:tag 'java.util.concurrent.atomic.AtomicReference}) schema.utils/use-fn-validation]) 341 | [output-schema-sym output-schema] 342 | (apply concat schema-bindings) 343 | (mapcat :more-bindings processed-arities))) 344 | :arglists (map :arglist processed-arities) 345 | :raw-arglists (map :raw-arglist processed-arities) 346 | :schema-form (if (= 1 (count processed-arities)) 347 | `(schema.core/->FnSchema ~output-schema-sym ~[(ffirst schema-bindings)]) 348 | `(schema.core/make-fn-schema ~output-schema-sym ~(mapv first schema-bindings))) 349 | :fn-body fn-forms})) 350 | 351 | (defn parse-arity-spec 352 | "Helper for schema.core/=>*." 353 | [spec] 354 | (assert! (vector? spec) "An arity spec must be a vector") 355 | (let [[init more] ((juxt take-while drop-while) #(not= '& %) spec) 356 | fixed (mapv (fn [i s] `(schema.core/one ~s '~(symbol (str "arg" i)))) (range) init)] 357 | (if (empty? more) 358 | fixed 359 | (do (assert! (and (= (count more) 2) (vector? (second more))) 360 | "An arity with & must be followed by a single sequence schema") 361 | (into fixed (second more)))))) 362 | 363 | (defn emit-defrecord 364 | [defrecord-constructor-sym env name field-schema & more-args] 365 | (let [[extra-key-schema? more-args] (maybe-split-first map? more-args) 366 | [extra-validator-fn? more-args] (maybe-split-first (complement symbol?) more-args) 367 | field-schema (process-arrow-schematized-args env field-schema)] 368 | `(do 369 | (let [bad-keys# (seq (filter #(schema.core/required-key? %) 370 | (keys ~extra-key-schema?)))] 371 | (assert! (not bad-keys#) "extra-key-schema? can not contain required keys: %s" 372 | (vec bad-keys#))) 373 | ~(when extra-validator-fn? 374 | `(assert! (fn? ~extra-validator-fn?) "Extra-validator-fn? not a fn: %s" 375 | (type ~extra-validator-fn?))) 376 | (~defrecord-constructor-sym ~name ~field-schema ~@more-args) 377 | (utils/declare-class-schema! 378 | ~name 379 | (utils/assoc-when 380 | (schema.core/record 381 | ~name 382 | (merge ~(into {} 383 | (for [k field-schema] 384 | [(keyword (clojure.core/name k)) 385 | (do (assert! (symbol? k) 386 | "Non-symbol in record binding form: %s" k) 387 | (extract-schema-form k))])) 388 | ~extra-key-schema?) 389 | ~(symbol (str 'map-> name))) 390 | :extra-validator-fn ~extra-validator-fn?)) 391 | ~(let [map-sym (gensym "m")] 392 | `(if-cljs 393 | nil 394 | (defn ~(symbol (str 'map-> name)) 395 | ~(str "Factory function for class " name ", taking a map of keywords to field values, but not much\n" 396 | " slower than ->x like the clojure.core version.\n" 397 | " (performance is fixed in Clojure 1.7, so this should eventually be removed.)") 398 | [~map-sym] 399 | (let [base# (new ~(symbol (str name)) 400 | ~@(map (fn [s] `(get ~map-sym ~(keyword s))) field-schema)) 401 | remaining# (dissoc ~map-sym ~@(map keyword field-schema))] 402 | (if (seq remaining#) 403 | (merge base# remaining#) 404 | base#))))) 405 | ~(let [map-sym (gensym "m")] 406 | `(defn ~(symbol (str 'strict-map-> name)) 407 | ~(str "Factory function for class " name ", taking a map of keywords to field values. All" 408 | " keys are required, and no extra keys are allowed. Even faster than map->") 409 | [~map-sym & [drop-extra-keys?#]] 410 | (when-not (or drop-extra-keys?# (= (count ~map-sym) ~(count field-schema))) 411 | (error! (utils/format* "Wrong number of keys: expected %s, got %s" 412 | (sort ~(mapv keyword field-schema)) (sort (keys ~map-sym))))) 413 | (new ~(symbol (str name)) 414 | ~@(map (fn [s] `(safe-get ~map-sym ~(keyword s))) field-schema))))))) 415 | 416 | (if-bb nil 417 | (defn -instrument-protocol-method 418 | "Given a protocol Var pvar, its method method-var and instrument-method, 419 | instrument the protocol method." 420 | [pvar ;:- Var 421 | method-var ;:- (Var InnerMth) 422 | instrument-method #_:- #_(s/=>* OuterMth 423 | [InnerMth 424 | (named (=> Any OuterMth InnerMth) 425 | 'sync!)])] 426 | (let [;; propagate method cache to inner method. 427 | ;; explanation: all functions in Clojure have special support for protocol methods 428 | ;; via the __methodImplCache field: https://github.com/clojure/clojure/search?q=methodimplcache&type=. 429 | ;; this mutable field is used inside each protocol method's implementation via (fn this [..] (.__methodImplCache this)) 430 | ;; and also mutated from the "outside" via (set! .__methodImplCache protocol-method). 431 | ;; since we wrap protocol methods, we need to preserve these two features (settable from outside, readable from inside). 432 | sync! (fn [^clojure.lang.AFunction outer-mth 433 | ^clojure.lang.AFunction inner-mth] 434 | (when-not (identical? (.__methodImplCache outer-mth) 435 | (.__methodImplCache inner-mth)) 436 | ;; lock to prevent outdated outer caches from overwriting newer inner caches 437 | (locking inner-mth 438 | (set! (.__methodImplCache inner-mth) 439 | ;; vv WARNING: must be calculated within protected area 440 | (.__methodImplCache outer-mth) 441 | ;; ^^ WARNING: must be calculated within protected area 442 | )))) 443 | ^clojure.lang.AFunction inner-mth @method-var 444 | ^clojure.lang.AFunction outer-mth (instrument-method inner-mth sync!) 445 | ;; populate outer cache so we can use outer-mth as the protocol method without needing 446 | ;; to call -reset-methods. 447 | _ (set! (.__methodImplCache outer-mth) 448 | (.__methodImplCache inner-mth)) 449 | method-builder (fn [cache] 450 | (set! (.__methodImplCache outer-mth) cache) 451 | (sync! outer-mth inner-mth) 452 | ;; preempt future fix for CLJ-1796--have a canonical method 453 | ;; representation for the duration of the protocol, matching 454 | ;; CLJS semantics. 455 | outer-mth) 456 | this-nsym (ns-name *ns*)] 457 | ;; instrument method builder 458 | (alter-var-root pvar assoc-in [:method-builders method-var] method-builder) 459 | ;; defeat Compiler.java inlining capabilities so we can always enforce schemas 460 | (alter-meta! method-var assoc :inline (fn [& args] 461 | `((do ~(symbol (name this-nsym) (str (.sym ^clojure.lang.Var method-var)))) 462 | ~@args))) 463 | ;; instrument the actual method 464 | (alter-var-root method-var (fn [_] outer-mth))))) 465 | 466 | (defn parse-defprotocol-sig [env pname name+sig+doc] 467 | (let [[doc name+sig] (let [lst (last name+sig+doc)] 468 | (if (string? lst) 469 | [lst (butlast name+sig+doc)] 470 | [nil name+sig+doc])) 471 | [method-name sig] (maybe-split-first simple-symbol? name+sig) 472 | _ (assert! (simple-symbol? method-name) "Missing method name %s" (pr-str method-name)) 473 | [output-schema sig] (let [fst (first sig)] 474 | (if (= :- fst) 475 | (let [nxt (next sig)] 476 | (assert! nxt "Missing schema after :- in %s" method-name) 477 | [(first nxt) (next nxt)]) 478 | [`schema.core/Any sig])) 479 | _ (assert (seq sig)) 480 | binds (mapv #(process-arrow-schematized-args env %) 481 | sig) 482 | cljs? (cljs-env? env)] 483 | {:sig (->> (concat (cons method-name binds) (when doc [doc])) 484 | ;; work around https://clojure.atlassian.net/browse/CLJS-3211 485 | (apply list)) 486 | :method-name method-name 487 | :schema-form `(schema.core/=>* ~output-schema ~@(map #(mapv (comp :schema meta) %) binds)) 488 | :instrument-method (let [outer-mth-meta (-> (or (meta method-name) {}) 489 | (dissoc :always-validate :never-validate) 490 | (into 491 | (cond 492 | (-> method-name meta :never-validate) {:never-validate true} 493 | (-> method-name meta :always-validate) {:always-validate true} 494 | (-> pname meta :never-validate) {:never-validate true} 495 | (-> pname meta :always-validate) {:always-validate true})) 496 | not-empty) 497 | inner-mth (gensym) 498 | gen-binder (fn [gs bind] 499 | (vec (mapcat #(list %1 :- (-> %2 meta :schema)) gs bind))) 500 | gen-bind-syms (fn [bind] 501 | (mapv (fn [s] 502 | (if (symbol? s) 503 | (gensym (str (name s) "__")) 504 | (gensym))) 505 | bind))] 506 | (cond 507 | ;; instrumentation not possible babashka yet 508 | bb? nil 509 | 510 | cljs? 511 | (let [cljs-nsym (-> env :ns :name) 512 | ->arity-sym #(symbol (str cljs-nsym "." method-name ".cljs$core$IFn$_invoke$arity$" %)) 513 | arities (into {} 514 | (map (fn [bind] 515 | [(count bind) (gensym)]) 516 | binds))] 517 | `(let ~(vec (mapcat (fn [[i g]] 518 | [g (if (= 1 (count arities)) 519 | ;; just one arity, wrap method-name 520 | method-name 521 | ;; multiple arites, wrap each arity individually. don't save/call old method-name 522 | ;; as it will dispatch right back to the wrapper's arities in an infinite loop. 523 | (->arity-sym i))]) 524 | arities)) 525 | ;; use defn instead of set! to completely hide the $arity$ methods of the underlying protocol 526 | ;; in case the cljs compiler attempts inlining. 527 | (schema.core/defn ~(with-meta method-name 528 | (assoc outer-mth-meta 529 | :protocol (symbol (name cljs-nsym) (name pname)) 530 | :doc doc)) 531 | :- ~output-schema 532 | ~@(map (fn [bind] 533 | (let [arity (count bind) 534 | gs (gen-bind-syms bind) 535 | inner-mth (get arities arity) 536 | _ (assert inner-mth)] 537 | (list (gen-binder gs bind) 538 | (cons inner-mth gs)))) 539 | binds)))) 540 | :else 541 | (let [outer-mth (gensym (str method-name "__")) 542 | sync! (gensym)] 543 | `(-instrument-protocol-method 544 | (var ~pname) 545 | (var ~method-name) 546 | ;; a function that wraps a protocol method in a schema check with a 547 | ;; cache synchronization point 548 | (fn [~inner-mth ~sync!] 549 | (schema.core/fn ~(with-meta outer-mth outer-mth-meta) 550 | :- ~output-schema 551 | ~@(map (fn [bind] 552 | (let [gs (gen-bind-syms bind)] 553 | (list (gen-binder gs bind) 554 | (list sync! outer-mth inner-mth) 555 | (cons inner-mth gs)))) 556 | binds)))))))})) 557 | 558 | (defn process-defprotocol [env name+opts+sigs] 559 | (let [[pname opts+sigs] (maybe-split-first simple-symbol? name+opts+sigs) 560 | _ (assert! (simple-symbol? pname) "Missing protocol name: %s" (pr-str pname)) 561 | [doc opts+sigs] (maybe-split-first string? opts+sigs) 562 | [opts sigs] (loop [preamble [] 563 | [fst :as opts+sigs] opts+sigs] 564 | (if (keyword? fst) 565 | (let [nxt (next opts+sigs)] 566 | (assert! nxt "Uneven args to defprotocol %s" pname) 567 | (recur (conj preamble fst (first nxt)) 568 | (next nxt))) 569 | [preamble opts+sigs]))] 570 | {:pname pname 571 | :opts opts 572 | :doc doc 573 | :parsed-sigs (mapv (partial parse-defprotocol-sig env pname) sigs)})) 574 | 575 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 576 | ;;; Public: helpers for schematized functions 577 | 578 | (defn normalized-defn-args 579 | "Helper for defining defn-like macros with schemas. Env is &env 580 | from the macro body. Reads optional docstring, return type and 581 | attribute-map and normalizes them into the metadata of the name, 582 | returning the normalized arglist. Based on 583 | clojure.tools.macro/name-with-attributes." 584 | [env macro-args] 585 | (let [[name macro-args] (extract-arrow-schematized-element env macro-args) 586 | [maybe-docstring macro-args] (maybe-split-first string? macro-args) 587 | [maybe-attr-map macro-args] (maybe-split-first map? macro-args)] 588 | (cons (vary-meta name merge 589 | (or maybe-attr-map {}) 590 | (when maybe-docstring {:doc maybe-docstring})) 591 | macro-args))) 592 | 593 | (defn set-compile-fn-validation! 594 | "Globally turn on or off function validation from being compiled into s/fn and s/defn. 595 | Enabled by default. 596 | See (doc compile-fn-validation?) for all conditions which control fn validation compilation" 597 | [on?] 598 | (reset! *compile-fn-validation* on?)) 599 | -------------------------------------------------------------------------------- /src/clj/schema/potemkin.clj: -------------------------------------------------------------------------------- 1 | (ns schema.potemkin 2 | "Features that require an explicit potemkin dependency to be provided by the consumer." 3 | (:require [schema.macros :as macros] 4 | [potemkin])) 5 | 6 | (defmacro defrecord+ 7 | "Like defrecord, but emits a record using potemkin/defrecord+. You must provide 8 | your own dependency on potemkin to use this." 9 | {:arglists '([name field-schema extra-key-schema? extra-validator-fn? & opts+specs])} 10 | [name field-schema & more-args] 11 | (apply macros/emit-defrecord 'potemkin/defrecord+ &env name field-schema more-args)) 12 | -------------------------------------------------------------------------------- /src/cljc/schema/coerce.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.coerce 2 | "Extension of schema for input coercion (coercing an input to match a schema)" 3 | (:require 4 | #?(:cljs [cljs.reader :as reader]) 5 | #?(:clj [clojure.edn :as edn]) 6 | #?(:clj [schema.macros :as macros]) 7 | #?(:clj [schema.core :as s] 8 | :cljs [schema.core :as s :include-macros true]) 9 | [schema.spec.core :as spec] 10 | [schema.utils :as utils] 11 | [clojure.string :as str]) 12 | #?(:cljs (:require-macros [schema.macros :as macros]))) 13 | 14 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 15 | ;;; Generic input coercion 16 | 17 | (def Schema 18 | "A Schema for Schemas" 19 | (s/protocol s/Schema)) 20 | 21 | (def CoercionMatcher 22 | "A function from schema to coercion function, or nil if no special coercion is needed. 23 | The returned function is applied to the corresponding data before validation (or walking/ 24 | coercion of its sub-schemas, if applicable)" 25 | (s/=> (s/maybe (s/=> s/Any s/Any)) Schema)) 26 | 27 | (s/defn coercer 28 | "Produce a function that simultaneously coerces and validates a datum. Returns 29 | a coerced value, or a schema.utils.ErrorContainer describing the error." 30 | [schema coercion-matcher :- CoercionMatcher] 31 | (spec/run-checker 32 | (fn [s params] 33 | (let [c (spec/checker (s/spec s) params)] 34 | (if-let [coercer (coercion-matcher s)] 35 | (fn [x] 36 | (macros/try-catchall 37 | (let [v (coercer x)] 38 | (if (utils/error? v) 39 | v 40 | (c v))) 41 | (catch t (macros/validation-error s x t)))) 42 | c))) 43 | true 44 | schema)) 45 | 46 | (s/defn coercer! 47 | "Like `coercer`, but is guaranteed to return a value that satisfies schema (or throw)." 48 | [schema coercion-matcher :- CoercionMatcher] 49 | (let [c (coercer schema coercion-matcher)] 50 | (fn [value] 51 | (let [coerced (c value)] 52 | (when-let [error (utils/error-val coerced)] 53 | (macros/error! (utils/format* "Value cannot be coerced to match schema: %s" (pr-str error)) 54 | {:schema schema :value value :error error})) 55 | coerced)))) 56 | 57 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 58 | ;;; Coercion helpers 59 | 60 | (s/defn first-matcher :- CoercionMatcher 61 | "A matcher that takes the first match from matchers." 62 | [matchers :- [CoercionMatcher]] 63 | (fn [schema] (first (keep #(% schema) matchers)))) 64 | 65 | (defn string->keyword [s] 66 | (if (string? s) (keyword s) s)) 67 | 68 | (defn string->boolean 69 | "returns true for strings that are equal, ignoring case, to the string 'true' 70 | (following java.lang.Boolean/parseBoolean semantics)" 71 | [s] 72 | (if (string? s) (= "true" (str/lower-case s)) s)) 73 | 74 | (defn keyword-enum-matcher [schema] 75 | (when (or (and (instance? #?(:clj schema.core.EnumSchema :cljs s/EnumSchema) schema) 76 | (every? keyword? (.-vs ^schema.core.EnumSchema schema))) 77 | (and (instance? #?(:clj schema.core.EqSchema :cljs s/EqSchema) schema) 78 | (keyword? (.-v ^schema.core.EqSchema schema)))) 79 | string->keyword)) 80 | 81 | (defn set-matcher [schema] 82 | (if (instance? #?(:clj clojure.lang.APersistentSet :cljs cljs.core.PersistentHashSet) schema) 83 | (fn [x] (if (sequential? x) (set x) x)))) 84 | 85 | (defn safe 86 | "Take a single-arg function f, and return a single-arg function that acts as identity 87 | if f throws an exception, and like f otherwise. Useful because coercers are not explicitly 88 | guarded for exceptions, and failing to coerce will generally produce a more useful error 89 | in this case." 90 | [f] 91 | (fn [x] (macros/try-catchall (f x) (catch e x)))) 92 | 93 | #?(:clj (def safe-long-cast 94 | "Coerce x to a long if this can be done without losing precision, otherwise return x." 95 | (safe 96 | (fn [x] 97 | (let [l (long x)] 98 | (if (== l x) 99 | l 100 | x)))))) 101 | 102 | (def string->uuid 103 | "Returns instance of UUID if input is a string. 104 | Note: in CLJS, this does not guarantee a specific UUID string representation, 105 | similar to #uuid reader" 106 | #?(:clj 107 | (safe #(java.util.UUID/fromString ^String %)) 108 | :cljs 109 | #(if (string? %) (uuid %) %))) 110 | 111 | 112 | (def ^:no-doc +json-coercions+ 113 | (merge 114 | {s/Keyword string->keyword 115 | s/Bool string->boolean 116 | s/Uuid string->uuid} 117 | #?(:clj {clojure.lang.Keyword string->keyword 118 | s/Int safe-long-cast 119 | Long safe-long-cast 120 | Double (safe double) 121 | Float (safe float) 122 | Boolean string->boolean}))) 123 | 124 | (defn json-coercion-matcher 125 | "A matcher that coerces keywords and keyword eq/enums from strings, and longs and doubles 126 | from numbers on the JVM (without losing precision)" 127 | [schema] 128 | (or (+json-coercions+ schema) 129 | (keyword-enum-matcher schema) 130 | (set-matcher schema))) 131 | 132 | (def edn-read-string 133 | "Reads one object from a string. Returns nil when string is nil or empty" 134 | #?(:clj edn/read-string :cljs reader/read-string)) 135 | 136 | (def ^:no-doc +string-coercions+ 137 | (merge 138 | +json-coercions+ 139 | {s/Num (safe edn-read-string) 140 | s/Int (safe edn-read-string)} 141 | #?(:clj {s/Int (safe #(safe-long-cast (edn-read-string %))) 142 | Long (safe #(safe-long-cast (edn-read-string %))) 143 | Double (safe #(Double/parseDouble %))}))) 144 | 145 | (defn string-coercion-matcher 146 | "A matcher that coerces keywords, keyword eq/enums, s/Num and s/Int, 147 | and long and doubles (JVM only) from strings." 148 | [schema] 149 | (or (+string-coercions+ schema) 150 | (keyword-enum-matcher schema) 151 | (set-matcher schema))) 152 | -------------------------------------------------------------------------------- /src/cljc/schema/experimental/abstract_map.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.experimental.abstract-map 2 | "Schemas representing abstract classes and subclasses" 3 | (:require 4 | [clojure.string :as str] 5 | #?(:clj [schema.core :as s] 6 | :cljs [schema.core :as s :include-macros true]) 7 | [schema.spec.core :as spec] 8 | [schema.spec.variant :as variant])) 9 | 10 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 11 | ;;; Private: helpers 12 | 13 | (defprotocol PExtensibleSchema 14 | (extend-schema! [this extension schema-name dispatch-values])) 15 | 16 | ;; a "subclass" 17 | (defrecord SchemaExtension [schema-name base-schema extended-schema explain-value] 18 | s/Schema 19 | (spec [this] 20 | (variant/variant-spec spec/+no-precondition+ [{:schema extended-schema}])) 21 | (explain [this] 22 | (list 'extend-schema 23 | schema-name 24 | (s/schema-name base-schema) 25 | (s/explain explain-value)))) 26 | 27 | ;; an "abstract class" 28 | (defrecord AbstractSchema [sub-schemas dispatch-key schema open?] 29 | s/Schema 30 | (spec [this] 31 | (variant/variant-spec 32 | spec/+no-precondition+ 33 | (concat 34 | (for [[k s] @sub-schemas] 35 | {:guard #(= (keyword (dispatch-key %)) (keyword k)) 36 | :schema s}) 37 | (when open? 38 | [{:schema (assoc schema dispatch-key s/Keyword s/Any s/Any)}])) 39 | (fn [v] (list (set (keys @sub-schemas)) (list dispatch-key v))))) 40 | (explain [this] 41 | (list 'abstract-map-schema dispatch-key (s/explain schema) (set (keys @sub-schemas)))) 42 | 43 | PExtensibleSchema 44 | (extend-schema! [this extension schema-name dispatch-values] 45 | (let [sub-schema (assoc (merge schema extension) 46 | dispatch-key (apply s/enum dispatch-values)) 47 | ext-schema (s/schema-with-name 48 | (SchemaExtension. schema-name this sub-schema extension) 49 | (name schema-name))] 50 | (swap! sub-schemas merge (into {} (for [k dispatch-values] [k ext-schema]))) 51 | ext-schema))) 52 | 53 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 54 | ;;; Public 55 | 56 | (s/defn abstract-map-schema 57 | "A schema representing an 'abstract class' map that must match at least one concrete 58 | subtype (indicated by the value of dispatch-key, a keyword). Add subtypes by calling 59 | `extend-schema`." 60 | [dispatch-key :- s/Keyword schema :- (s/pred map?)] 61 | (AbstractSchema. (atom {}) dispatch-key schema false)) 62 | 63 | (s/defn open-abstract-map-schema 64 | "Like abstract-map-schema, but allows unknown types to validate (for, e.g. forward 65 | compatibility)." 66 | [dispatch-key :- s/Keyword schema :- (s/pred map?)] 67 | (AbstractSchema. (atom {}) dispatch-key schema true)) 68 | 69 | #?(:clj 70 | (defmacro extend-schema 71 | [schema-name extensible-schema dispatch-values extension] 72 | `(def ~schema-name 73 | (extend-schema! ~extensible-schema ~extension '~schema-name ~dispatch-values)))) 74 | 75 | (defn sub-schemas [abstract-schema] 76 | @(.-sub-schemas ^AbstractSchema abstract-schema)) 77 | -------------------------------------------------------------------------------- /src/cljc/schema/spec/collection.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.spec.collection 2 | "A collection spec represents a collection of elements, 3 | each of which is itself schematized." 4 | (:require 5 | #?(:clj [schema.macros :as macros]) 6 | [schema.utils :as utils] 7 | [schema.spec.core :as spec]) 8 | #?(:cljs (:require-macros [schema.macros :as macros]))) 9 | 10 | 11 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 12 | ;;; Collection Specs 13 | 14 | (declare sequence-transformer) 15 | 16 | (defn- element-transformer [e params then] 17 | (if (vector? e) 18 | (case (first e) 19 | ::optional 20 | (sequence-transformer (next e) params then) 21 | 22 | ::remaining 23 | (let [_ (macros/assert! (= 2 (count e)) "remaining can have only one schema.") 24 | c (spec/sub-checker (second e) params)] 25 | #?(:clj (fn [^java.util.List res x] 26 | (doseq [i x] 27 | (.add res (c i))) 28 | (then res nil)) 29 | :cljs (fn [res x] 30 | (swap! res into (map c x)) 31 | (then res nil))))) 32 | 33 | (let [parser (:parser e) 34 | c (spec/sub-checker e params)] 35 | #?(:clj (fn [^java.util.List res x] 36 | (then res (parser (fn [t] (.add res (if (utils/error? t) t (c t)))) x))) 37 | :cljs (fn [res x] 38 | (then res (parser (fn [t] (swap! res conj (if (utils/error? t) t (c t)))) x))))))) 39 | 40 | (defn- sequence-transformer [elts params then] 41 | (macros/assert! (not-any? #(and (vector? %) (= (first %) ::remaining)) (butlast elts)) 42 | "Remaining schemas must be in tail position.") 43 | (reduce 44 | (fn [f e] 45 | (element-transformer e params f)) 46 | then 47 | (reverse elts))) 48 | 49 | #?(:clj ;; for performance 50 | (defn- has-error? [^java.util.List l] 51 | (let [it (.iterator l)] 52 | (loop [] 53 | (if (.hasNext it) 54 | (if (utils/error? (.next it)) 55 | true 56 | (recur)) 57 | false)))) 58 | 59 | :cljs 60 | (defn- has-error? [l] 61 | (some utils/error? l))) 62 | 63 | (defn subschemas [elt] 64 | (if (map? elt) 65 | [(:schema elt)] 66 | (do (assert (vector? elt)) 67 | (assert (#{::remaining ::optional} (first elt))) 68 | (mapcat subschemas (next elt))))) 69 | 70 | (defrecord CollectionSpec [pre konstructor elements on-error] 71 | spec/CoreSpec 72 | (subschemas [this] (mapcat subschemas elements)) 73 | (checker [this params] 74 | (let [konstructor (if (:return-walked? params) konstructor (fn [_] nil)) 75 | t (sequence-transformer elements params (fn [_ x] x))] 76 | (fn [x] 77 | (or (pre x) 78 | (let [res #?(:clj (java.util.ArrayList.) :cljs (atom [])) 79 | remaining (t res x) 80 | res #?(:clj res :cljs @res)] 81 | (if (or (seq remaining) (has-error? res)) 82 | (utils/error (on-error x res remaining)) 83 | (konstructor res)))))))) 84 | 85 | 86 | (defn collection-spec 87 | "A collection represents a collection of elements, each of which is itself 88 | schematized. At the top level, the collection has a precondition 89 | (presumably on the overall type), a constructor for the collection from a 90 | sequence of items, an element spec, and a function that constructs a 91 | descriptive error on failure. 92 | 93 | The element spec is a nested list structure, in which the leaf elements each 94 | provide an element schema, parser (allowing for efficient processing of structured 95 | collections), and optional error wrapper. Each item in the list can be a leaf 96 | element or an `optional` nested element spec (see below). In addition, the final 97 | element can be a `remaining` schema (see below). 98 | 99 | Note that the `optional` carries no semantics with respect to validation; 100 | the user must ensure that the parser enforces the desired semantics, which 101 | should match the structure of the spec for proper generation." 102 | [pre ;- spec/Precondition 103 | konstructor ;- (s/=> s/Any [(s/named s/Any 'checked-value)]) 104 | elements ;- [(s/cond-pre 105 | ;; {:schema (s/protocol Schema) 106 | ;; :parser (s/=> s/Any (s/=> s/Any s/Any) s/Any) ; takes [item-fn coll], calls item-fn on matching items, returns remaining. 107 | ;; (s/optional-key :error-wrap) (s/pred fn?)} 108 | ;; [(s/one ::optional) (s/recursive Elements)]] 109 | ;; where the last element can optionally be a [::remaining schema] 110 | on-error ;- (=> s/Any (s/named s/Any 'value) [(s/named s/Any 'checked-element)] [(s/named s/Any 'unmatched-element)]) 111 | ] 112 | (->CollectionSpec pre konstructor elements on-error)) 113 | 114 | 115 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 116 | ;;; Helpers for creating 'elements' 117 | 118 | (defn remaining 119 | "All remaining elements must match schema s" 120 | [s] 121 | [::remaining s]) 122 | 123 | (defn optional 124 | "If any more elements are present, they must match the elements in 'ss'" 125 | [& ss] 126 | (vec (cons ::optional ss))) 127 | 128 | (defn all-elements [schema] 129 | (remaining 130 | {:schema schema 131 | :parser (fn [coll] (macros/error! (str "should never be not called")))})) 132 | 133 | (defn one-element [required? schema parser] 134 | (let [base {:schema schema :parser parser}] 135 | (if required? 136 | base 137 | (optional base)))) 138 | 139 | (defn optional-tail [schema parser more] 140 | (into (optional {:schema schema :parser parser}) more)) 141 | -------------------------------------------------------------------------------- /src/cljc/schema/spec/core.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.spec.core 2 | "Protocol and preliminaries for Schema 'specs', which are a common language 3 | for schemas to use to express their structure." 4 | (:require 5 | #?(:clj [schema.macros :as macros]) 6 | [schema.utils :as utils]) 7 | #?(:cljs (:require-macros [schema.macros :as macros]))) 8 | 9 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 10 | ;;; Core spec protocol 11 | 12 | (defprotocol CoreSpec 13 | "Specs are a common language for Schemas to express their structure. 14 | These two use-cases aren't privileged, just the two that are considered core 15 | to being a Spec." 16 | (subschemas [this] 17 | "List all subschemas") 18 | (checker [this params] 19 | "Create a function that takes [data], and either returns a walked version of data 20 | (by default, usually just data), or a utils/ErrorContainer containing value that looks 21 | like the 'bad' parts of data with ValidationErrors at the leaves describing the failures. 22 | 23 | params is a map specifying: 24 | - :subschema-checker - a function for checking subschemas 25 | - :returned-walked? - a boolean specifying whether to return a walked version of the data 26 | (otherwise, nil is returned which increases performance) 27 | - :cache - a map structure from schema to checker, which speeds up checker creation 28 | when the same subschema appears multiple times, and also facilitates handling 29 | recursive schemas.")) 30 | 31 | 32 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 33 | ;;; Preconditions 34 | 35 | ;; A Precondition is a function of a value that returns a 36 | ;; ValidationError if the value does not satisfy the precondition, 37 | ;; and otherwise returns nil. 38 | ;; e.g., (s/defschema Precondition (s/=> (s/maybe schema.utils.ValidationError) s/Any)) 39 | ;; as such, a precondition is essentially a very simple checker. 40 | 41 | (def +no-precondition+ (fn [_] nil)) 42 | 43 | (defn precondition 44 | "Helper for making preconditions. 45 | Takes a schema, predicate p, and error function err-f. 46 | If the datum passes the predicate, returns nil. 47 | Otherwise, returns a validation error with description (err-f datum-description), 48 | where datum-description is a (short) printable stand-in for the datum." 49 | [s p err-f] 50 | (fn [x] 51 | (when-let [reason (macros/try-catchall (when-not (p x) 'not) (catch e# 'throws?))] 52 | (macros/validation-error s x (err-f (utils/value-name x)) reason)))) 53 | 54 | #?(:clj 55 | (defmacro simple-precondition 56 | "A simple precondition where f-sym names a predicate (e.g. (simple-precondition s map?))" 57 | [s f-sym] 58 | `(precondition ~s ~f-sym #(list (quote ~f-sym) %)))) 59 | 60 | 61 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 62 | ;;; Helpers 63 | 64 | (defn run-checker 65 | "A helper to start a checking run, by setting the appropriate params. 66 | For examples, see schema.core/checker or schema.coerce/coercer." 67 | [f return-walked? s] 68 | (f 69 | s 70 | {:subschema-checker f 71 | :return-walked? return-walked? 72 | :cache #?(:clj (java.util.IdentityHashMap.) :cljs (atom {}))})) 73 | 74 | (defn with-cache [cache cache-key wrap-recursive-delay result-fn] 75 | (if-let [w #?(:clj (.get ^java.util.Map cache cache-key) 76 | :cljs (@cache cache-key))] 77 | (if (= ::in-progress w) ;; recursive 78 | (wrap-recursive-delay (delay #?(:clj (.get ^java.util.Map cache cache-key) 79 | :cljs (@cache cache-key)))) 80 | w) 81 | (do #?(:clj (.put ^java.util.Map cache cache-key ::in-progress) 82 | :cljs (swap! cache assoc cache-key ::in-progress)) 83 | (let [res (result-fn)] 84 | #?(:clj (.put ^java.util.Map cache cache-key res) 85 | :cljs (swap! cache assoc cache-key res)) 86 | res)))) 87 | 88 | (defn sub-checker 89 | "Should be called recursively on each subschema in the 'checker' method of a spec. 90 | Handles caching and error wrapping behavior." 91 | [{:keys [schema error-wrap]} 92 | {:keys [subschema-checker cache] :as params}] 93 | (let [sub (with-cache cache schema 94 | (fn [d] (fn [x] (@d x))) 95 | (fn [] (subschema-checker schema params)))] 96 | (if error-wrap 97 | (fn [x] 98 | (let [res (sub x)] 99 | (if-let [e (utils/error-val res)] 100 | (utils/error (error-wrap res)) 101 | res))) 102 | sub))) 103 | -------------------------------------------------------------------------------- /src/cljc/schema/spec/leaf.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.spec.leaf 2 | (:require 3 | [schema.spec.core :as spec])) 4 | 5 | 6 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 7 | ;;; Leaf Specs 8 | 9 | (defrecord LeafSpec [pre] 10 | spec/CoreSpec 11 | (subschemas [this] nil) 12 | (checker [this params] 13 | (fn [x] (or (pre x) x)))) 14 | 15 | (defn leaf-spec 16 | "A leaf spec represents an atomic datum that is checked completely 17 | with a single precondition, and is otherwise a black box to Schema." 18 | [pre ;- spec/Precondition 19 | ] 20 | (->LeafSpec pre)) 21 | -------------------------------------------------------------------------------- /src/cljc/schema/spec/variant.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.spec.variant 2 | (:require 3 | #?(:clj [schema.macros :as macros]) 4 | [schema.utils :as utils] 5 | [schema.spec.core :as spec]) 6 | #?(:cljs (:require-macros [schema.macros :as macros]))) 7 | 8 | 9 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 10 | ;;; Variant Specs 11 | 12 | (defn- option-step [o params else] 13 | (let [g (:guard o) 14 | c (spec/sub-checker o params) 15 | step (if g 16 | (fn [x] 17 | (let [guard-result (macros/try-catchall 18 | (g x) 19 | (catch e# ::exception))] 20 | (cond (= ::exception guard-result) 21 | (macros/validation-error 22 | (:schema o) 23 | x 24 | (list (symbol (utils/fn-name g)) (utils/value-name x)) 25 | 'throws?) 26 | 27 | guard-result 28 | (c x) 29 | 30 | :else 31 | (else x)))) 32 | c)] 33 | (if-let [wrap-error (:wrap-error o)] 34 | (fn [x] 35 | (let [res (step x)] 36 | (if-let [e (utils/error-val res)] 37 | (utils/error (wrap-error e)) 38 | res))) 39 | step))) 40 | 41 | (defrecord VariantSpec [pre options err-f post] 42 | spec/CoreSpec 43 | (subschemas [this] (map :schema options)) 44 | (checker [this params] 45 | (let [t (reduce 46 | (fn [f o] 47 | (option-step o params f)) 48 | (fn [x] (macros/validation-error this x (err-f (utils/value-name x)))) 49 | (reverse options))] 50 | (if post 51 | (fn [x] 52 | (or (pre x) 53 | (let [v (t x)] 54 | (if (utils/error? v) 55 | v 56 | (or (post (if (:return-walked? params) v x)) v))))) 57 | (fn [x] 58 | (or (pre x) 59 | (t x))))))) 60 | 61 | (defn variant-spec 62 | "A variant spec represents a choice between a set of alternative 63 | subschemas, e.g., a tagged union. It has an overall precondition, 64 | set of options, and error function. 65 | 66 | The semantics of `options` is that the options are processed in 67 | order. During checking, the datum must match the schema for the 68 | first option for which `guard` passes. During generation, any datum 69 | generated from an option will pass the corresponding `guard`. 70 | 71 | err-f is a function to produce an error message if none 72 | of the guards match (and must be passed unless the last option has no 73 | guard)." 74 | ([pre options] 75 | (variant-spec pre options nil)) 76 | ([pre options err-f] 77 | (variant-spec pre options err-f nil)) 78 | ([pre ;- spec/Precondition 79 | options ;- [{:schema (s/protocol Schema) 80 | ;; (s/optional-key :guard) (s/pred fn?) 81 | ;; (s/optional-key :error-wrap) (s/pred fn?)}] 82 | err-f ;- (s/pred fn?) 83 | post ;- (s/maybe spec/Precondition) 84 | ] 85 | (macros/assert! (or err-f (nil? (:guard (last options)))) 86 | "when last option has a guard, err-f must be provided") 87 | (->VariantSpec pre options err-f post))) 88 | -------------------------------------------------------------------------------- /src/cljc/schema/test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.test 2 | "Utilities for testing with schemas" 3 | (:require #?(:clj [schema.core :as s] 4 | :cljs [schema.core :as s :include-macros true]) 5 | #?(:clj clojure.test))) 6 | 7 | (defn validate-schemas 8 | "A fixture for tests: put 9 | (use-fixtures :once schema.test/validate-schemas) 10 | in your test file to turn on schema validation globally during all test executions." 11 | [fn-test] 12 | (s/with-fn-validation (fn-test))) 13 | 14 | #?(:clj 15 | (defmacro deftest 16 | "A test with schema validation turned on globally during execution of the body." 17 | [name & body] 18 | `(clojure.test/deftest ~name 19 | (s/with-fn-validation 20 | ~@body)))) 21 | -------------------------------------------------------------------------------- /src/cljc/schema/utils.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.utils 2 | "Private utilities used in schema implementation." 3 | (:refer-clojure :exclude [record?]) 4 | #?(:clj (:require [clojure.string :as string]) 5 | :cljs (:require 6 | goog.string.format 7 | [goog.object :as gobject] 8 | [goog.string :as gstring] 9 | [clojure.string :as string])) 10 | #?(:cljs (:require-macros [schema.utils :refer [char-map]]))) 11 | 12 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 13 | ;;; Miscellaneous helpers 14 | 15 | (defn assoc-when 16 | "Like assoc but only assocs when value is truthy. Copied from plumbing.core so that 17 | schema need not depend on plumbing." 18 | [m & kvs] 19 | (assert (even? (count kvs))) 20 | (into (or m {}) 21 | (for [[k v] (partition 2 kvs) 22 | :when v] 23 | [k v]))) 24 | 25 | (defn type-of [x] 26 | #?(:clj (class x) 27 | :cljs (js* "typeof ~{}" x))) 28 | 29 | (defn fn-schema-bearer 30 | "What class can we associate the fn schema with? In Clojure use the class of the fn; in 31 | cljs just use the fn itself." 32 | [f] 33 | #?(:bb f 34 | :clj (class f) 35 | :cljs f)) 36 | 37 | (defn format* [fmt & args] 38 | (apply #?(:clj format :cljs gstring/format) fmt args)) 39 | 40 | (def max-value-length (atom 19)) 41 | 42 | (defn value-name 43 | "Provide a descriptive short name for a value." 44 | [value] 45 | (let [t (type-of value)] 46 | (if (<= (count (str value)) @max-value-length) 47 | value 48 | (symbol (str "a-" #?(:clj (.getName ^Class t) :cljs t)))))) 49 | 50 | #?(:clj 51 | (defmacro char-map [] 52 | clojure.lang.Compiler/CHAR_MAP)) 53 | 54 | #?(:clj 55 | (defn unmunge 56 | "TODO: eventually use built in demunge in latest cljs." 57 | [s] 58 | (->> (char-map) 59 | (sort-by #(- (count (second %)))) 60 | (reduce (fn [^String s [to from]] (string/replace s from (str to))) s)))) 61 | 62 | (defn fn-name 63 | "A meaningful name for a function that looks like its symbol, if applicable." 64 | [f] 65 | #?(:cljs 66 | (let [[_ s] (re-matches #"#object\[(.*)\]" (pr-str f))] 67 | (if (= "Function" s) 68 | "function" 69 | (->> s demunge (re-find #"[^/]+(?:$|(?=/+$))")))) 70 | :clj (let [s (.getName (class f)) 71 | slash (.lastIndexOf s "$") 72 | raw (unmunge 73 | (if (>= slash 0) 74 | (str (subs s 0 slash) "/" (subs s (inc slash))) 75 | s))] 76 | (string/replace raw #"^clojure.core/" "")))) 77 | 78 | (defn record? [x] 79 | #?(:clj (instance? clojure.lang.IRecord x) 80 | :cljs (satisfies? IRecord x))) 81 | 82 | 83 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 84 | ;;; Error descriptions 85 | 86 | ;; A leaf schema validation error, describing the schema and value and why it failed to 87 | ;; match the schema. In Clojure, prints like a form describing the failure that would 88 | ;; return true. 89 | 90 | (declare validation-error-explain) 91 | 92 | (deftype ValidationError [schema value expectation-delay fail-explanation] 93 | #?(:cljs IPrintWithWriter) 94 | #?(:cljs (-pr-writer [this writer opts] 95 | (-pr-writer (validation-error-explain this) writer opts)))) 96 | 97 | (defn validation-error-explain [^ValidationError err] 98 | (list (or (.-fail-explanation err) 'not) @(.-expectation-delay err))) 99 | 100 | #?(:clj ;; Validation errors print like forms that would return false 101 | (defmethod print-method ValidationError [err writer] 102 | (print-method (validation-error-explain err) writer))) 103 | 104 | (defn make-ValidationError 105 | "for cljs sake (easier than normalizing imports in macros.clj)" 106 | [schema value expectation-delay fail-explanation] 107 | (ValidationError. schema value expectation-delay fail-explanation)) 108 | 109 | 110 | ;; Attach a name to an error from a named schema. 111 | (declare named-error-explain) 112 | 113 | (deftype NamedError [name error] 114 | #?(:cljs IPrintWithWriter) 115 | #?(:cljs (-pr-writer [this writer opts] 116 | (-pr-writer (named-error-explain this) writer opts)))) 117 | 118 | (defn named-error-explain [^NamedError err] 119 | (list 'named (.-error err) (.-name err))) 120 | 121 | #?(:clj ;; Validation errors print like forms that would return false 122 | (defmethod print-method NamedError [err writer] 123 | (print-method (named-error-explain err) writer))) 124 | 125 | 126 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 127 | ;;; Monoidish error containers, which wrap errors (to distinguish from success values). 128 | 129 | (defrecord ErrorContainer [error]) 130 | 131 | (defn error 132 | "Distinguish a value (must be non-nil) as an error." 133 | [x] (assert x) (->ErrorContainer x)) 134 | 135 | (defn error? [x] 136 | (instance? ErrorContainer x)) 137 | 138 | (defn error-val [x] 139 | (when (error? x) 140 | (.-error ^ErrorContainer x))) 141 | 142 | 143 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 144 | ;;; Registry for attaching schemas to classes, used for defn and defrecord 145 | 146 | #?(:clj 147 | (let [^java.util.Map +class-schemata+ (java.util.Collections/synchronizedMap (java.util.WeakHashMap.))] 148 | (defn declare-class-schema! 149 | "Globally set the schema for a class (above and beyond a simple instance? check). 150 | Use with care, i.e., only on classes that you control. Also note that this 151 | schema only applies to instances of the concrete type passed, i.e., 152 | (= (class x) klass), not (instance? klass x)." 153 | [klass schema] 154 | #?(:bb nil ;; fn identity is used as klass in bb 155 | :default (assert (class? klass) 156 | (format* "Cannot declare class schema for non-class %s" (pr-str (class klass))))) 157 | (.put +class-schemata+ klass schema)) 158 | 159 | (defn class-schema 160 | "The last schema for a class set by declare-class-schema!, or nil." 161 | [klass] 162 | (.get +class-schemata+ klass)))) 163 | 164 | #?(:cljs 165 | (do 166 | (defn declare-class-schema! [klass schema] 167 | (gobject/set klass "schema$utils$schema" schema)) 168 | 169 | (defn class-schema [klass] 170 | (gobject/get klass "schema$utils$schema")))) 171 | 172 | 173 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 174 | ;;; Utilities for fast-as-possible reference to use to turn fn schema validation on/off 175 | 176 | (def use-fn-validation 177 | "Turn on run-time function validation for functions compiled when 178 | s/compile-fn-validation was true -- has no effect for functions compiled 179 | when it is false." 180 | ;; specialize in Clojure for performance 181 | #?(:bb (atom false) 182 | :clj (java.util.concurrent.atomic.AtomicReference. false) 183 | :cljs (atom false))) 184 | -------------------------------------------------------------------------------- /test/bb/schema/bb_test_runner.clj: -------------------------------------------------------------------------------- 1 | (ns schema.bb-test-runner 2 | (:require [clojure.test :as t] 3 | [babashka.classpath :as cp])) 4 | 5 | (def test-nsyms [;'schema.experimental.complete-test ;;deprecated 6 | ;'schema.experimental.generators-test ;; deprecated 7 | 'schema.core-test 8 | 'schema.macros-test 9 | 'schema.coerce-test 10 | 'schema.experimental.abstract-map-test 11 | 'schema.test-test 12 | 'schema.utils-test]) 13 | 14 | (apply require test-nsyms) 15 | 16 | (def test-results 17 | (apply t/run-tests test-nsyms)) 18 | 19 | (def failures-and-errors 20 | (let [{:keys [:fail :error]} test-results] 21 | (+ fail error))) 22 | 23 | (System/exit failures-and-errors) 24 | -------------------------------------------------------------------------------- /test/clj/schema/experimental/complete_test.clj: -------------------------------------------------------------------------------- 1 | (ns schema.experimental.complete-test 2 | (:use clojure.test) 3 | (:require 4 | [schema.coerce :as coerce] 5 | [schema.core :as s] 6 | [schema.experimental.abstract-map :as abstract-map] 7 | [schema.experimental.complete :as complete])) 8 | 9 | (deftest complete-test 10 | (let [s [{:a s/Int :b s/Str :c [s/Str]}] 11 | [r1 r2 r3 :as rs] (complete/complete [{:a 1} {:b "bob"} {:c ["foo" "bar"]}] s)] 12 | (is (not (s/check s rs))) 13 | (is (= (:a r1) 1)) 14 | (is (= (:b r2) "bob")) 15 | (is (= (:c r3) ["foo" "bar"]))) 16 | (testing "complete through variant" 17 | (let [s (s/cond-pre s/Str {:foo s/Int})] 18 | (is (= "test" (complete/complete "test" s))) 19 | (is (integer? (:foo (complete/complete {} s))))))) 20 | 21 | (s/defschema Animal 22 | (abstract-map/abstract-map-schema 23 | :type 24 | {:name s/Str})) 25 | 26 | (abstract-map/extend-schema Cat Animal [:cat] {:claws? s/Bool}) 27 | (abstract-map/extend-schema Dog Animal [:dog] {:barks? s/Bool}) 28 | 29 | (s/defrecord User 30 | [id :- long 31 | cash :- double 32 | friends :- [User] 33 | pet :- (s/maybe Animal)]) 34 | 35 | (def complete-user 36 | (complete/completer 37 | User 38 | {User (fn [x] (if (number? x) {:id x} x)) 39 | Animal (fn [x] (if (keyword? x) {:type x} x))})) 40 | 41 | (defn pull-pattern-matcher [s] 42 | (when (and (instance? clojure.lang.APersistentMap s) 43 | (not (s/find-extra-keys-schema s))) 44 | (fn [x] 45 | (select-keys x (->> s keys (map s/explicit-schema-key)))))) 46 | 47 | (defn pull [s x] 48 | ((coerce/coercer s pull-pattern-matcher) x)) 49 | 50 | (deftest fancy-complete-test 51 | (is (s/validate User (complete-user {}))) 52 | (is (= {:id 2} 53 | (pull {:id long} (complete-user 2)))) 54 | (is (= {:id 2 :pet {:type :cat}} 55 | (pull {:id s/Any :pet {:type s/Keyword}} (complete-user {:id 2 :pet :cat})))) 56 | (is (= {:id 10 :friends [{:id 2} {:id 3}]} 57 | (pull {:id s/Any :friends [{:id long}]} 58 | (complete-user {:id 10 :friends [2 {:id 3}]})))) 59 | (is (= {:id 10 :friends [{:id 2 :pet nil} {:id 3 :pet {:type :dog}}]} 60 | (pull {:id s/Any :friends [{:id long :pet (s/maybe {:type s/Keyword})}]} 61 | (complete-user {:id 10 :friends [{:id 2 :pet nil} {:id 3 :pet :dog}]}))))) 62 | -------------------------------------------------------------------------------- /test/clj/schema/experimental/generators_test.clj: -------------------------------------------------------------------------------- 1 | (ns schema.experimental.generators-test 2 | (:use clojure.test) 3 | (:require 4 | [clojure.test.check.properties :as properties] 5 | [clojure.test.check.generators :as check-generators] 6 | [clojure.test.check.clojure-test :as check-clojure-test] 7 | [schema.core :as s] 8 | [schema.experimental.generators :as generators])) 9 | 10 | 11 | (def OGInner 12 | {(s/required-key "l") [s/Int] 13 | s/Keyword s/Str}) 14 | 15 | (def OGInner2 16 | {:c OGInner 17 | :d s/Str}) 18 | 19 | (def OGSchema 20 | {:a [s/Str] 21 | :b OGInner2}) 22 | 23 | (def FinalSchema 24 | {:a (s/eq ["bob"]) 25 | :b {:c (s/conditional (fn [{:strs [l]}] (and (every? even? l) (seq l))) OGInner) 26 | :d (s/eq "mary")}}) 27 | 28 | (deftest sample-test 29 | (let [res (generators/sample 30 | 20 OGSchema 31 | {[s/Str] (generators/always ["bob"]) 32 | s/Int ((generators/fmap #(inc (* % 2))) check-generators/int)} 33 | {[s/Int] (comp (generators/such-that seq) 34 | (generators/fmap (partial mapv inc))) 35 | OGInner2 (generators/merged {:d "mary"})})] 36 | (is (= (count res) 20)) 37 | (is (s/validate [FinalSchema] res)))) 38 | 39 | (deftest simple-leaf-generators-smoke-test 40 | (doseq [leaf-schema [double float long int short char byte boolean 41 | Double Float Long Integer Short Character Byte Boolean 42 | doubles floats longs ints shorts chars bytes booleans 43 | s/Str String s/Bool s/Num s/Int s/Keyword s/Symbol s/Inst 44 | Object s/Any s/Uuid (s/eq "foo") (s/enum :a :b :c)]] 45 | (testing (str leaf-schema) 46 | (is (= 10 (count (generators/sample 10 leaf-schema))))))) 47 | 48 | (def FancySeq 49 | "A sequence that starts with a String, followed by an optional Keyword, 50 | followed by any number of Numbers." 51 | [(s/one s/Str "s") 52 | (s/optional s/Keyword "k") 53 | s/Num]) 54 | 55 | (deftest fancy-seq-smoke-test 56 | (testing "Catch issues with a fancier schema with optional keys and such." 57 | (is (= 100 (count (generators/sample 100 FancySeq)))))) 58 | 59 | (check-clojure-test/defspec spec-test 60 | 100 61 | (properties/for-all [x (generators/generator OGSchema)] 62 | (not (s/check OGSchema x)))) 63 | -------------------------------------------------------------------------------- /test/clj/schema/macros_test.clj: -------------------------------------------------------------------------------- 1 | (ns schema.macros-test 2 | (:use clojure.test) 3 | (:require 4 | [schema.core :as s] 5 | [schema.macros :as macros])) 6 | 7 | (deftest normalized-defn-args-test 8 | (doseq [explicit-meta [{} {:a -1 :c 3}] 9 | [schema-attrs schema-forms] {{:schema `s/Any} [] 10 | {:schema 'Long :tag 'Long} [:- 'Long]} 11 | [doc-attrs doc-forms] {{} [] 12 | {:doc "docstring"} ["docstring"]} 13 | [attr-map attr-forms] {{} {} 14 | {:a 1 :b 2} [{:a 1 :b 2}]}] 15 | (let [simple-body ['[x] `(+ 1 1)] 16 | full-args (concat [(with-meta 'abc explicit-meta)] schema-forms doc-forms attr-forms simple-body) 17 | [name & more] (macros/normalized-defn-args {} full-args)] 18 | (testing (vec full-args) 19 | (is (= (concat ['abc (merge explicit-meta schema-attrs doc-attrs attr-map) simple-body]) 20 | (concat [name (meta name) more]))))))) 21 | 22 | (deftest compile-fn-validation?-test 23 | (is (macros/compile-fn-validation? {} 'foo)) 24 | (is (not (macros/compile-fn-validation? {} (with-meta 'foo {:never-validate true})))) 25 | (macros/set-compile-fn-validation! false) 26 | (is (not (macros/compile-fn-validation? {} 'foo))) 27 | (is (not (macros/compile-fn-validation? {} (with-meta 'foo {:always-validate true})))) 28 | (macros/set-compile-fn-validation! true) 29 | (binding [*assert* false] 30 | (is (not (macros/compile-fn-validation? {} 'foo))) 31 | (is (macros/compile-fn-validation? {} (with-meta 'foo {:always-validate true}))))) 32 | -------------------------------------------------------------------------------- /test/clj/schema/test_macros.clj: -------------------------------------------------------------------------------- 1 | (ns schema.test-macros 2 | "Macros to help cross-language testing of schemas." 3 | (:require 4 | clojure.test 5 | [schema.core :as s] 6 | [schema.macros :as sm] 7 | [schema.spec.core :as spec])) 8 | 9 | (defmacro valid! 10 | "Assert that x satisfies schema s, and the walked value is equal to the original." 11 | [s x] 12 | `(let [x# ~x] (~'is (= x# ((spec/run-checker #(spec/checker (s/spec %1) %2) true ~s) x#))))) 13 | 14 | (defmacro invalid! 15 | "Assert that x does not satisfy schema s, optionally checking the stringified return value" 16 | ([s x] 17 | `(~'is (s/check ~s ~x))) 18 | ([s x expected] 19 | `(do (invalid! ~s ~x) 20 | (sm/if-cljs nil (~'is (= ~expected (pr-str (s/check ~s ~x)))))))) 21 | 22 | (defmacro invalid-call! 23 | "Assert that f throws (presumably due to schema validation error) when called on args." 24 | [f & args] 25 | (when (sm/compile-fn-validation? &env f) 26 | `(~'is (~'thrown? ~'Throwable (~f ~@args))))) 27 | 28 | (defmacro is-assert! 29 | "Assert that an assert! is thrown with msg" 30 | [form expected-msg] 31 | `(~'is (~'thrown-with-msg? 32 | ~(if (sm/cljs-env? &env) 'js/Error 'java.lang.RuntimeException) 33 | ~expected-msg 34 | ~form))) 35 | -------------------------------------------------------------------------------- /test/cljc/schema/coerce_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.coerce-test 2 | #?(:clj (:use clojure.test) 3 | :cljs (:use-macros 4 | [cljs.test :only [is deftest]])) 5 | (:require 6 | [schema.core :as s] 7 | [schema.utils :as utils] 8 | [schema.coerce :as coerce] 9 | #?(:cljs cljs.test))) 10 | 11 | ;; s/Num s/Int 12 | 13 | (def Generic 14 | {:i s/Int 15 | (s/optional-key :b) s/Bool 16 | (s/optional-key :n) s/Num 17 | (s/optional-key :s) s/Str 18 | (s/optional-key :k1) {s/Int s/Keyword} 19 | (s/optional-key :k2) s/Keyword 20 | (s/optional-key :e) (s/enum :a :b :c) 21 | (s/optional-key :eq) (s/eq :k) 22 | (s/optional-key :set) #{s/Keyword} 23 | (s/optional-key :u) s/Uuid}) 24 | 25 | (def JSON 26 | {(s/optional-key :is) [s/Int]}) 27 | 28 | #?(:clj 29 | (def JVM 30 | {(s/optional-key :jb) Boolean 31 | (s/optional-key :l) long 32 | (s/optional-key :d) Double 33 | (s/optional-key :f) Float 34 | (s/optional-key :jk) clojure.lang.Keyword})) 35 | 36 | (defn err-ks [res] 37 | (set (keys (utils/error-val res)))) 38 | 39 | (deftest json-coercer-test 40 | (let [coercer (coerce/coercer 41 | (merge Generic JSON) 42 | coerce/json-coercion-matcher) 43 | res {:i 1 :is [1 2] :n 3.0 :s "asdf" :k1 {1 :hi} :k2 :bye :e :a :eq :k :set #{:a :b}}] 44 | (is (= res 45 | (coercer {:i 1.0 :is [1.0 2.0] :n 3.0 :s "asdf" :k1 {1.0 "hi"} :k2 "bye" :e "a" :eq "k" :set ["a" "a" "b"]}))) 46 | (is (= res (coercer res))) 47 | (is (= {:i 1 :b true} (coercer {:i 1.0 :b "TRUE"}))) 48 | (is (= {:i 1 :b false} (coercer {:i 1.0 :b "Yes"}))) 49 | (is (= #{:i :set} (err-ks (coercer {:i 1.1 :n 3 :set "a"}))))) 50 | 51 | #?(:clj (testing "jvm specific" 52 | (let [coercer (coerce/coercer JVM coerce/json-coercion-matcher) 53 | res {:l 1 :d 1.0 :jk :asdf :f (float 0.1)}] 54 | (is (= res (coercer {:l 1.0 :d 1 :jk "asdf" :f 0.1}) )) 55 | (is (= res (coercer res))) 56 | (is (= {:jb true} (coercer {:jb "TRUE"}))) 57 | (is (= {:jb false} (coercer {:jb "Yes"}))) 58 | (is (= #{:l :jk :f} (err-ks (coercer {:l 1.2 :jk 1.0 :f "0"})))) 59 | (is (= #{:f} (err-ks (coercer {:f nil})))) 60 | (is (= #{:d} (err-ks (coercer {:d nil})))) 61 | (is (= #{:d} (err-ks (coercer {:d "1.0"}))))))) 62 | #?(:clj (testing "malformed uuid" 63 | (let [coercer (coerce/coercer Generic coerce/json-coercion-matcher)] 64 | (is (= #{:u} (err-ks (coercer {:i 1 :u "uuid-wannabe"})))))))) 65 | 66 | (deftest string-coercer-test 67 | (let [coercer (coerce/coercer Generic coerce/string-coercion-matcher)] 68 | (is (= {:b true :i 1 :n 3.0 :s "asdf" :k1 {1 :hi} :k2 :bye :e :a :eq :k :u #uuid "550e8400-e29b-41d4-a716-446655440000" :set #{:a :b}} 69 | (coercer {:b "true" :i "1" :n "3.0" :s "asdf" :k1 {"1" "hi"} :k2 "bye" :e "a" :eq "k" :u "550e8400-e29b-41d4-a716-446655440000" :set ["a" "a" "b"]}))) 70 | (is (= #{:i} (err-ks (coercer {:i "1.1"}))))) 71 | 72 | #?(:clj (testing "jvm specific" 73 | (let [coercer (coerce/coercer JVM coerce/string-coercion-matcher) 74 | res {:jb false :l 2 :d 1.0 :jk :asdf}] 75 | (is (= res (coercer {:jb "false" :l "2.0" :d "1" :jk "asdf"}))) 76 | (is (= #{:l} (err-ks (coercer {:l "1.2"})))))))) 77 | 78 | (deftest coercer!-test 79 | (let [coercer (coerce/coercer! {:k s/Keyword :i s/Int} coerce/string-coercion-matcher)] 80 | (is (= {:k :key :i 12} (coercer {:k "key" :i "12"}))) 81 | (is (thrown-with-msg? #?(:clj Exception :cljs js/Error) #"keyword\? 12" (coercer {:k 12 :i 12}))))) 82 | 83 | #?(:clj 84 | (do 85 | (def NestedVecs 86 | [(s/one s/Num "Node ID") (s/recursive #'NestedVecs)]) 87 | 88 | (deftest recursive-coercion-test 89 | (testing "Test that recursion (which rebinds subschema-walker) works with coercion." 90 | (is (= [1 [2 [3] [4]]] 91 | ((coerce/coercer NestedVecs coerce/string-coercion-matcher) 92 | ["1" ["2" ["3"] ["4"]]]))))))) 93 | 94 | (deftest constrained-test 95 | (is (= 1 ((coerce/coercer! (s/constrained s/Int odd?) coerce/string-coercion-matcher) "1"))) 96 | (is (= {1 1} ((coerce/coercer! (s/constrained {s/Int s/Int} #(odd? (count %))) coerce/string-coercion-matcher) {"1" "1"})))) 97 | 98 | (deftest map-entry-test 99 | (let [entry (first {:foo :bar}) 100 | coercer (coerce/coercer! (s/map-entry s/Any s/Any) (constantly nil)) 101 | coerced-value (coercer entry)] 102 | (is (= entry coerced-value)) 103 | (is (= (type entry) (type coerced-value))))) 104 | -------------------------------------------------------------------------------- /test/cljc/schema/experimental/abstract_map_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.experimental.abstract-map-test 2 | #?(:clj (:use clojure.test [schema.test-macros :only [valid! invalid! invalid-call!]]) 3 | :cljs (:use-macros 4 | [cljs.test :only [is deftest testing]] 5 | [schema.test-macros :only [valid! invalid! invalid-call!]])) 6 | (:require 7 | [schema.core :as s] 8 | [schema.coerce :as coerce] 9 | #?(:clj [schema.experimental.abstract-map :as abstract-map] 10 | :cljs [schema.experimental.abstract-map :as abstract-map :include-macros true]) 11 | #?(:cljs cljs.test))) 12 | 13 | (s/defschema Animal 14 | (abstract-map/abstract-map-schema 15 | :type 16 | {:age s/Num 17 | :vegan? s/Bool})) 18 | 19 | (abstract-map/extend-schema Cat Animal [:cat] {:fav-catnip s/Str}) 20 | 21 | (deftest extend-schema-test 22 | (valid! Cat {:age 3 :vegan? false :fav-catnip "cosmic" :type :cat}) 23 | (invalid! Cat {:age 3 :vegan? false :fav-catnip "cosmic" :type :cat :foobar false}) 24 | 25 | (valid! Animal {:age 3 :vegan? false :fav-catnip "cosmic" :type :cat}) 26 | (invalid! Animal {:age 3 :vegan? false :type :cat} 27 | "{:fav-catnip missing-required-key}") 28 | (invalid! Animal {:age 3 :vegan? false :fav-catnip "cosmic" :type :dog})) 29 | 30 | (s/defschema TV 31 | (abstract-map/open-abstract-map-schema 32 | :make 33 | {:channel s/Int 34 | :power? s/Bool})) 35 | 36 | (abstract-map/extend-schema HondaTV TV [:honda] {:num-wheels s/Int}) 37 | 38 | (deftest open-abstract-map-schema-test 39 | (valid! TV {:channel 30 :power? true :num-wheels 1 :make :honda}) 40 | (valid! HondaTV {:channel 30 :power? true :num-wheels 1 :make :honda}) 41 | (valid! TV {:channel 30 :power? false :missiles "short range" :make :dod}) 42 | (invalid! TV {:channel 30 :make :unknown} "{:power? missing-required-key}")) 43 | 44 | (deftest json-coercer-test 45 | (let [animal-coercer (coerce/coercer Animal coerce/json-coercion-matcher) 46 | cat-coercer (coerce/coercer Cat coerce/json-coercion-matcher) 47 | cat {:type :cat :age 12 :vegan? false :fav-catnip "cosmic"}] 48 | (is (= cat (animal-coercer (update-in cat [:type] name)))) 49 | (is (= cat (cat-coercer (update-in cat [:type] name)))))) 50 | -------------------------------------------------------------------------------- /test/cljc/schema/other_namespace.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.other-namespace 2 | "Code for tests that need to setup things in a different 3 | namespace." 4 | (:require [schema.core :as s])) 5 | 6 | (defmulti ef408750 identity) 7 | -------------------------------------------------------------------------------- /test/cljc/schema/test_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.test-test 2 | #?(:clj (:use clojure.test)) 3 | (:require 4 | [schema.core :as s] 5 | [schema.test :as st])) 6 | 7 | #?(:clj 8 | (do 9 | (s/defn test-fn :- s/Str [] 5) 10 | 11 | (deftest validation-off-test 12 | (is (= 5 (test-fn)))) 13 | 14 | (st/deftest validation-on-test 15 | (is (thrown? Exception (test-fn)))))) 16 | -------------------------------------------------------------------------------- /test/cljc/schema/utils_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema.utils-test 2 | #?(:clj (:use clojure.test) 3 | :cljs (:use-macros 4 | [cljs.test :only [are deftest]])) 5 | (:require 6 | [schema.utils :as utils])) 7 | 8 | (defn ^:private a-defn-function-with-a-normal-name [a b c d]) 9 | 10 | #?(:bb nil :default 11 | (deftest fn-name-test 12 | (are [in pattern] (re-matches pattern (utils/fn-name in)) 13 | 14 | (fn a-fn-function-with-a-normal-name [x]) 15 | #?(:clj #".*/a-fn-function-with-a-normal-name.*" 16 | :cljs #"a-fn-function-with-a-normal-name") 17 | 18 | a-defn-function-with-a-normal-name 19 | #?(:clj #".*/a-defn-function-with-a-normal-name" 20 | :cljs #"a-defn-function-with-a-normal-name") 21 | 22 | ;; regression for issue #416 23 | (fn foo$$ [x]) 24 | #?(:clj #"schema.utils.*foo.*" 25 | :cljs #"foo") 26 | 27 | 28 | #(+ 1 2) 29 | #?(:clj #"schema\.utils-test.*" 30 | :cljs #"function")))) 31 | -------------------------------------------------------------------------------- /test/cljs/schema/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns schema.test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [schema.coerce-test] 4 | [schema.core-test] 5 | [schema.experimental.abstract-map-test] 6 | [schema.other-namespace] 7 | [schema.test-test] 8 | [schema.utils-test])) 9 | 10 | (doo-tests 11 | 'schema.coerce-test 12 | 'schema.core-test 13 | 'schema.experimental.abstract-map-test 14 | 'schema.other-namespace 15 | 'schema.test-test 16 | 'schema.utils-test) 17 | --------------------------------------------------------------------------------