├── .github └── workflows │ └── clojure.yml ├── .gitignore ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── project.clj ├── scripts ├── build-docs.sh ├── build.clj ├── repl.clj ├── repl.sh └── test.sh ├── src └── schema_tools │ ├── coerce.cljc │ ├── core.cljc │ ├── experimental │ └── walk.cljc │ ├── impl.cljc │ ├── openapi │ └── core.cljc │ ├── swagger │ └── core.cljc │ ├── util.cljc │ └── walk.cljc └── test ├── cljc └── schema_tools │ ├── coerce_test.cljc │ ├── core_test.cljc │ ├── experimental │ └── walk_test.cljc │ ├── openapi │ └── core_test.cljc │ ├── runner.cljs │ ├── select_schema_test.cljc │ ├── swagger │ └── core_test.cljc │ └── walk_test.cljc └── cljs └── schema_tools └── doo_runner.cljs /.github/workflows/clojure.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build-clj: 10 | strategy: 11 | matrix: 12 | # Supported Java versions: LTS releases 8 and 11 and the latest release 13 | jdk: [8, 11, 15] 14 | 15 | name: Clojure (Java ${{ matrix.jdk }}) 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Setup Java ${{ matrix.jdk }} 22 | uses: actions/setup-java@v1.4.3 23 | with: 24 | java-version: ${{ matrix.jdk }} 25 | - name: Setup Clojure 26 | uses: DeLaGuardo/setup-clojure@3.1 27 | with: 28 | lein: latest 29 | - name: Run Clojure test 30 | run: ./scripts/test.sh clj 31 | 32 | build-cljs: 33 | name: ClojureScript 34 | 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | - name: Setup Java ${{ matrix.jdk }} 40 | uses: actions/setup-java@v1.4.3 41 | with: 42 | java-version: 8 43 | - name: Setup Clojure 44 | uses: DeLaGuardo/setup-clojure@3.1 45 | with: 46 | lein: latest 47 | - name: Install deps 48 | run: npm ci 49 | - name: Run ClojureScript test 50 | run: ./scripts/test.sh cljs 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | doc 11 | node_modules 12 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Schema-tools CHANGELOG 2 | 3 | We use [Break Versioning][breakver]. The version numbers follow a `..` scheme with the following intent: 4 | 5 | | Bump | Intent | 6 | | ------- | ---------------------------------------------------------- | 7 | | `major` | Major breaking changes -- check the changelog for details. | 8 | | `minor` | Minor breaking changes -- check the changelog for details. | 9 | | `patch` | No breaking changes, ever!! | 10 | 11 | `-SNAPSHOT` versions are preview versions for upcoming releases. 12 | 13 | [breakver]: https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md 14 | 15 | ## 0.13.1 (2023-05-17) 16 | 17 | **[compare](https://github.com/metosin/schema-tools/compare/0.13.0...0.13.1)** 18 | 19 | * `:description` and `:openapi/description` get used for populating the OpenAPI parameter `:description`. 20 | [#84](https://github.com/metosin/schema-tools/pull/84) 21 | [metosin/reitit#612](https://github.com/metosin/reitit/issues/612) 22 | 23 | ## 0.13.0 (2023-03-15) 24 | 25 | **[compare](https://github.com/metosin/schema-tools/compare/0.12.3...0.13.0)** 26 | 27 | * In addition to keys `:swagger/*` and `:openapi/*` contributing to swagger/openapi schemas, you can override the whole swagger/openapi schema with `:swagger` or `:openapi`: 28 | 29 | ```clj 30 | (require '[schema.core :as s]) 31 | (require '[schema-tools.core :as st]) 32 | (require '[schema-tools.openapi.core :as openapi]) 33 | 34 | (openapi/transform (st/schema {:foo s/Num} 35 | {:openapi {:type "string" :format "binary"}}) 36 | {}) 37 | ;; {:type "string", :format "binary"} 38 | ``` 39 | 40 | ## 0.12.3 (2021-02-12) 41 | 42 | **[compare](https://github.com/metosin/schema-tools/compare/0.12.2...0.12.3)** 43 | 44 | * OpenAPI3 support [#44](https://github.com/metosin/schema-tools/issues/44), [#63](https://github.com/metosin/schema-tools/pull/63), [#66](https://github.com/metosin/schema-tools/pull/66) 45 | 46 | ## 0.12.2 (2020-01-30) 47 | 48 | **[compare](https://github.com/metosin/schema-tools/compare/0.12.1...0.12.2)** 49 | 50 | * Fix {required,optional}-keys regression [#59](https://github.com/metosin/schema-tools/pull/59) 51 | 52 | ## 0.12.1 (2020-01-10) 53 | 54 | **[compare](https://github.com/metosin/schema-tools/compare/0.12.0...0.12.1)** 55 | 56 | * Updated deps: 57 | 58 | ```clj 59 | [prismatic/schema "1.1.12"] is available but we use "1.1.11" 60 | ``` 61 | 62 | ## 0.12.0 (2019-06-14) 63 | 64 | **[compare](https://github.com/metosin/schema-tools/compare/0.11.0...0.12.0)** 65 | 66 | * Both String & JSON Coercion also coerce from keywords. This is useful as map keys are commonly keywordized in ring/http. Fixes [#54](https://github.com/metosin/schema-tools/issues/54). Thanks to [Mitchel Kuijpers](https://github.com/mitchelkuijpers) 67 | 68 | ```clj 69 | (stc/coerce 70 | {:1 {:true "1", :false "2"}} 71 | {s/Int {s/Bool s/Any}} 72 | stc/json-coercion-matcher) 73 | ; {1 {true "1", false "2"}} 74 | ``` 75 | 76 | * Keys with `swagger` namespace in `st/schema` data contribute to Swagger Schema: 77 | 78 | ```clj 79 | (require '[schema.core :as st]) 80 | (require '[schema-tools.core :as st]) 81 | (require '[schema-tools.swagger.core :as swagger]) 82 | 83 | (swagger/transform 84 | (st/schema 85 | s/Str 86 | {:swagger/default "abba" 87 | :swagger/format "email"})) 88 | ; {:type "string" 89 | ; :format "email" 90 | ; :default "abba"} 91 | ``` 92 | 93 | * Updated deps: 94 | 95 | ```clj 96 | [prismatic/schema "1.1.11"] is available but we use "1.1.9" 97 | ``` 98 | 99 | ## 0.11.0 (2019-02-11) 100 | 101 | **[compare](https://github.com/metosin/schema-tools/compare/0.10.5...0.11.0)** 102 | 103 | - Prevent open-schema killing children ([#51](https://github.com/metosin/schema-tools/issues/51)) 104 | - `open-schema` now uses `s/Keyword` as open key Schema, instead of `s/Any` 105 | 106 | ## 0.10.5 (2018-11-01) 107 | 108 | **[compare](https://github.com/metosin/schema-tools/compare/0.10.4...0.10.5)** 109 | 110 | - New options for handling default values ([#25](https://github.com/metosin/schema-tools/issues/25)): 111 | - `schema-tools.coerce/default-key-matcher` which adds missing map keys if they have default values specified. 112 | - `schema-tools.coerce/default-coercion-matcher` has been renamed to `stc/default-value-matcher`. 113 | - `default-coercion-matcher` is now a deprecated alias for `default-value-matcher`. 114 | - `schema-tools.coerce/default-matcher` combines the effects of `default-key-matcher` and `default-value-matcher`. 115 | 116 | ## 0.10.4 (2018-09-04) 117 | 118 | **[compare](https://github.com/metosin/schema-tools/compare/0.10.3...0.10.4)** 119 | 120 | - Fix ClojureScript (Closure) warning about reference to global RegExp object. 121 | - Using `js/RegExp` as Schema is no longer supported, instead one should use `schema.core/Regex` 122 | 123 | ## 0.10.3 (2018-05-23) 124 | 125 | **[compare](https://github.com/metosin/schema-tools/compare/0.10.2...0.10.3)** 126 | 127 | * `schema-tools.core/optional-keys-schema` to make all Map Schema keys optional (recursively) 128 | 129 | ## 0.10.2 (2018-05-08) 130 | 131 | **[compare](https://github.com/metosin/schema-tools/compare/0.10.1...0.10.2)** 132 | 133 | * Initial support of Schema->Swagger2, ported from [ring-swagger](https://github.com/metosin/ring-swagger) with added support for ClojureScript! 134 | * Few [issues](https://github.com/metosin/schema-tools/issues) still. 135 | * See [code](https://github.com/metosin/schema-tools/blob/master/src/schema_tools/swagger/core.cljc) for details. 136 | 137 | ## 0.10.1 (2018-03-27) 138 | 139 | **[compare](https://github.com/metosin/schema-tools/compare/0.10.0...0.10.1)** 140 | 141 | * **BUGFIX**: Works now with ClojureScript 1.10.238 142 | * MapEntry changes in the latest ClojureScript broke `walk` 143 | * One Swagger, please. 144 | 145 | ## 0.10.0 (2018-02-19) 146 | 147 | **[compare](https://github.com/metosin/schema-tools/compare/0.9.1...0.10.0)** 148 | 149 | * **BREAKING**: Requires now Java1.8 (date coercions using `java.time`) 150 | * **BREAKING**: `Default` record value is now `value`, not `default`, fixes [#34](https://github.com/metosin/schema-tools/issues/34) 151 | * `schema-tools.coercion` contains now `json-coercion-matcher` and `string-coercion-matcher`, ported and polished from [Ring-swagger](https://github.com/metosin/ring-swagger) 152 | 153 | ## 0.9.1 (16.10.2017) 154 | 155 | **[compare](https://github.com/metosin/schema-tools/compare/0.9.0...0.9.1)** 156 | 157 | - `stc/corce` and `stc/coercer` default to `(constantly nil)` matcher 158 | - `st/open-schema` transforms all nested Map Schemas to accept any extra keys 159 | - Tested also against `[org.clojure/clojurescript "1.9.946"]` & `[org.clojure/clojure "1.9.0-beta2"]` 160 | - Updated dependencies: 161 | 162 | ```clj 163 | [prismatic/schema "1.1.7"] is available but we use "1.0.5" 164 | [org.clojure/clojurescript "1.9.946"] is available but we use "1.9.562" 165 | ``` 166 | 167 | ## 0.9.0 (20.4.2016) 168 | 169 | **[compare](https://github.com/metosin/schema-tools/compare/0.8.0...0.9.0)** 170 | 171 | - **BREAKING**: `schema-tools.walk/walk` argument order has been changed to match 172 | `clojure.walk/walk` 173 | 174 | ## 0.8.0 (17.3.2016) 175 | 176 | **[compare](https://github.com/metosin/schema-tools/compare/0.7.0...0.8.0)** 177 | 178 | - Add `postwalk` and `prewalk` to `schema-tools.walk` 179 | - `select-schema` migration helper has been dropped off 180 | - Handle defaults via `(st/default Long 1)`& `stc/default-coercion-matcher` 181 | - `stc/multi-matcher` for applying multiple coercions for same schemas & values 182 | - Use Clojure 1.8 by default, test also with 1.7.0 183 | - Updated dependencies: 184 | 185 | ```clj 186 | [prismatic/schema "1.0.5"] is available but we use "1.0.3" 187 | ``` 188 | 189 | ## 0.7.0 (8.11.2015) 190 | 191 | **[compare](https://github.com/metosin/schema-tools/compare/0.6.0...0.7.0)** 192 | 193 | - Fixed problem with `walk` losing metadata for IMapEntries (or vectors) on 194 | Clojure 1.8 195 | - Converted source from Cljx to cljc 196 | - Dropped support for Clojure 1.6 197 | - Updated dependencies: 198 | 199 | ```clojure 200 | [prismatic/schema "1.0.3"] is available but we use "1.0.2" 201 | ``` 202 | 203 | ## 0.6.2 (28.10.2015) 204 | 205 | - Fix select-schema bug introduced in 0.6.0: [#21](https://github.com/metosin/schema-tools/issues/21) 206 | - `schema-tools.walk` 207 | - Added support for `s/constrained` 208 | - Updated dependencies: 209 | 210 | ```clojure 211 | [prismatic/schema "1.0.2"] is available but we use "1.0.1" 212 | ``` 213 | 214 | ## 0.6.1 (29.9.2015) 215 | 216 | - Fixed `walk` for `ConditionalSchema` 217 | 218 | ## 0.6.0 (9.9.2015) 219 | 220 | - **BREAKING**: Supports and depends on Schema 1.0.0 221 | - `schema-tools.walk` 222 | - Added support for walking `Conditional` and `CondPre` schemes. 223 | - Made sure leaf schemes (such as enum, pred, eq) are walked properly, 224 | i.e. `inner` is not called for them as they don't have sub-schemas. 225 | - Added `schema-tools.experimental.walk` which provides support for 226 | `schema.experimental.abstract-map` 227 | - Updated dependencies: 228 | 229 | ```clojure 230 | [prismatic/schema "1.0.1"] is available but we use "0.4.4" 231 | ``` 232 | 233 | ## 0.5.2 (19.8.2015) 234 | 235 | - fixed `select-schema` WARNING on ClojureScript. 236 | - updated dependencies: 237 | 238 | ```clojure 239 | [prismatic/schema "0.4.4"] is available but we use "0.4.3" 240 | [org.clojure/clojurescript "1.7.107"] is available but we use "1.7.28" 241 | ``` 242 | 243 | ## 0.5.1 (5.8.2015) 244 | 245 | - new functions in `schema-tools.coerce` (idea by [Michael Griffiths](https://github.com/metosin/schema-tools/issues/10#issuecomment-124976346) & ring-swagger) 246 | - `coercer` to create a coercer, which throws exception if the value can't be coerced to match the schema. 247 | - `coerce` to create and apply a coercer, which throws exception if the value can't be coerced to match the schema. 248 | - error `:type` can overridden, defaulting to `:schema-tools.coerce/error` 249 | 250 | ## 0.5.0 (29.7.2015) 251 | 252 | - remove `safe-coercer` 253 | - new `map-filter-matcher` to strip illegal keys from non-record maps 254 | - original code by [abp](https://gist.github.com/abp/0c4106eba7b72802347b) 255 | - **breaking**: `select-schema` signature and functionality has changed 256 | - fn argument order has changed to be consistent with other fns 257 | - `[schema value]` -> `[value schema]` 258 | - `[matcher schema value]` -> `[value schema matcher]` 259 | - to help migration: throws ex-info with message `"Illegal argument order - breaking change in 0.5.0."` if second argument is not a schema 260 | - uses schema coercion (`map-filter-matcher`) to drop illegal keys 261 | - fixes [#4](https://github.com/metosin/schema-tools/issues/4) - works now also with predicate keys 262 | - if a value can't be coerced, Exception is thrown - just like from `schema.core/validate` 263 | 264 | ```clojure 265 | (st/select-schema 266 | {:a "a" 267 | :z "disallowed key" 268 | :b "disallowed key" 269 | :x-kikka "x-kikka" 270 | :x-kukka "x-kukka" 271 | :y-kikka "invalid key"} 272 | {(s/pred #(re-find #"x-" (name %)) ":x-.*") s/Any, :a String}) 273 | ; {:a "a", :x-kikka "x-kikka", :x-kukka "x-kukka"} 274 | ``` 275 | 276 | ```clojure 277 | (st/select-schema {:beer "ipa" :taste "good"} {:beer (s/enum :ipa :apa)} ) 278 | ; clojure.lang.ExceptionInfo: Could not coerce value to schema: {:beer (not (#{:ipa :apa} "ipa"))} 279 | ; data: {:type :schema.core/error, 280 | ; :schema {:beer {:vs #{:ipa :apa}}}, 281 | ; :value {:beer "ipa", :taste "good"}, 282 | ; :error {:beer (not (#{:ipa :apa} "ipa"))}} 283 | 284 | (require '[schema.coerce :as sc]) 285 | 286 | (st/select-schema {:beer "ipa" :taste "good"} {:beer (s/enum :ipa :apa)} sc/json-coercion-matcher) 287 | ; {:beer :ipa} 288 | ``` 289 | 290 | ## 0.4.3 (11.6.2015) 291 | 292 | - `select-schema` takes now optional coercion matcher - to coerce values safely in a single sweep 293 | - `or-matcher` 294 | 295 | ## 0.4.2 (26.5.2015) 296 | 297 | - fix for [#7](https://github.com/metosin/schema-tools/issues/7) 298 | - updated dependencies: 299 | 300 | ```clojure 301 | [prismatic/schema "0.4.3"] is available but we use "0.4.2" 302 | [org.clojure/clojurescript "0.0-3297"] is available but we use "0.0-3269" 303 | ``` 304 | 305 | ## 0.4.1 (17.5.2015) 306 | 307 | - meta-data helpers: `schema-with-description` `schema-description`, `resolve-schema` (clj only), `resolve-schema-description` (clj only) 308 | - updated dependencies: 309 | 310 | ```clojure 311 | [prismatic/schema "0.4.2"] is available but we use "0.4.0" 312 | [codox "0.8.12"] is available but we use "0.8.11" 313 | [org.clojure/clojurescript "0.0-3269"] is available but we use "0.0-3196" 314 | ``` 315 | 316 | ## 0.4.0 (13.4.2015) 317 | 318 | - implemented `assoc` 319 | - dissoc away schema-name from meta-data (key `:name`) if the transforming functions have changed the schema. 320 | - `assoc`, `dissoc`, `select-keys`, `assoc-in`, `update-in`, `dissoc-in`, `update`, `merge`, `optional-keys`, `required-keys` 321 | - fixes [#2](https://github.com/metosin/schema-tools/issues/2) 322 | 323 | ## 0.3.0 (21.3.2015) 324 | 325 | - Added `schema-tools.walk` namespace 326 | - Implements `clojure.walk/walk` like `walk` function which knows how to 327 | traverse through Schemas. 328 | - Updated to `[prismatic/schema "0.4.0"]` 329 | 330 | ## 0.2.0 (1.2.2015) 331 | 332 | - **BREAKING**: `with-optional-keys` and `with-required-keys` are renamed to `optional-keys` and `required-keys` and take vector now of keys instead of vararg keys 333 | - implemented `merge`, `update` 334 | - updated deps: 335 | ```clojure 336 | [prismatic/schema "0.3.6"] is available but we use "0.3.3" 337 | [org.clojure/clojurescript "0.0-2740"] is available but we use "0.0-2665" 338 | ``` 339 | 340 | ## 0.1.2 (21.1.2015) 341 | 342 | - ClojureScript tests 343 | - Fixed warning about `Object` on Cljs. 344 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Contributions are welcome. 4 | 5 | Please file bug reports and feature requests to https://github.com/metosin/schema-tools/issues. 6 | 7 | ## Making changes 8 | 9 | * Fork the repository on Github 10 | * Create a topic branch from where you want to base your work (usually the master branch) 11 | * Check the formatting rules from existing code (no trailing whitepace, mostly default indentation) 12 | * Ensure any new code is well-tested, and if possible, any issue fixed is covered by one or more new tests 13 | * Verify that all tests pass using `lein test` and `./scripts/test.sh cljs` 14 | * Push your code to your fork of the repository 15 | * Make a Pull Request 16 | 17 | ## Commit messages 18 | 19 | 1. Separate subject from body with a blank line 20 | 2. Limit the subject line to 50 characters 21 | 3. Capitalize the subject line 22 | 4. Do not end the subject line with a period 23 | 5. Use the imperative mood in the subject line 24 | - "Add x", "Fix y", "Support z", "Remove x" 25 | 6. Wrap the body at 72 characters 26 | 7. Use the body to explain what and why vs. how 27 | 28 | For comprehensive explanation read this [post by Chris Beams](http://chris.beams.io/posts/git-commit/#seven-rules). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF 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 content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schema-tools ![Build status](https://github.com/metosin/schema-tools/actions/workflows/clojure.yml/badge.svg) [![cljdoc badge](https://cljdoc.org/badge/metosin/schema-tools)](https://cljdoc.org/d/metosin/schema-tools/CURRENT) 2 | 3 | Common utilities for [Prismatic Schema](https://github.com/Prismatic/schema) for Clojure/Script. Big sister to [spec-tools](https://github.com/metosin/spec-tools). 4 | 5 | * common Schema definitions: `any-keys`, `any-keyword-keys`, `open-schema`, `optional-keys-schema` 6 | * schema-aware selectors: `get-in`, `select-keys`, `select-schema` 7 | * schema-aware transformers: `assoc`, `dissoc`, `assoc-in`, `update-in`, `update`, `dissoc-in`, `merge`, `optional-keys`, `required-keys` 8 | * removes the schema name and ns if the schema (value) has changed. 9 | * handle schema default values via `default` & `default-coercion-matcher` 10 | * meta-data helpers: `schema-with-description` `schema-description`, `resolve-schema` (clj only), `resolve-schema-description` (clj only) 11 | * coercion tools: `or-matcher`, `map-filter-matcher`, `multi-matcher`, `coercer`, `coerce` 12 | * tuned JSON & String matchers: from keywords, Java8 dates etc. 13 | * Protocol-based walker for manipulating Schemas in `schema-tools.walk`: `walk`, `prewalk` and `postwalk`. 14 | * Swagger2 generation 15 | * Coercion tools 16 | 17 | [API Docs](https://cljdoc.org/d/metosin/schema-tools/CURRENT). 18 | 19 | ## Latest version 20 | 21 | [![Clojars Project](http://clojars.org/metosin/schema-tools/latest-version.svg)](http://clojars.org/metosin/schema-tools) 22 | 23 | Requires Java 1.8. 24 | 25 | ## Examples 26 | 27 | Normal `clojure.core` functions don't work well with Schemas: 28 | 29 | ```clojure 30 | (require '[schema.core :as s]) 31 | 32 | (s/defschema Address {:street s/Str 33 | (s/optional-key :city) s/Str 34 | (s/required-key :country) {:name s/Str}}) 35 | 36 | ;; where's my city? 37 | (select-keys Address [:street :city]) 38 | ; {:street java.lang.String} 39 | 40 | ; this should not return the original Schema name... 41 | (s/schema-name (select-keys Address [:street :city])) 42 | ; Address 43 | ``` 44 | 45 | With schema-tools: 46 | 47 | ```clojure 48 | (require '[schema-tools.core :as st]) 49 | 50 | (st/select-keys Address [:street :city]) 51 | ; {:street java.lang.String, #schema.core.OptionalKey{:k :city} java.lang.String} 52 | 53 | (s/schema-name (st/select-keys Address [:street :city])) 54 | ; nil 55 | ``` 56 | 57 | ### Coercion 58 | 59 | If a given value can't be coerced to match a schema, ex-info is thrown (like `schema.core/validate`): 60 | 61 | ```clojure 62 | (require '[schema-tools.coerce :as stc]) 63 | 64 | (def matcher (constantly nil)) 65 | (def coercer (stc/coercer String matcher)) 66 | 67 | (coercer 123) 68 | ; clojure.lang.ExceptionInfo: Could not coerce value to schema: (not (instance? java.lang.String 123)) 69 | ; error: (not (instance? java.lang.String 123)) 70 | ; schema: java.lang.String 71 | ; type: :schema-tools.coerce/error 72 | ; value: 123 73 | 74 | (coercer "123") 75 | ; "123" 76 | 77 | ; same behavior with coerce (creates coercer on each invocation, slower) 78 | (stc/coerce 123 String matcher) 79 | (stc/coerce "123" String matcher) 80 | ``` 81 | 82 | Coercion error `:type` can be overridden in both cases with an extra argument. 83 | 84 | ```clojure 85 | (stc/coerce 123 String matcher :domain/horror) 86 | ; clojure.lang.ExceptionInfo: Could not coerce value to schema: (not (instance? java.lang.String 123)) 87 | ; error: (not (instance? java.lang.String 123)) 88 | ; schema: java.lang.String 89 | ; type: :domain/horror 90 | ; value: 123 91 | ``` 92 | 93 | ### Select Schema 94 | 95 | Filtering out illegal schema keys (using coercion): 96 | 97 | ```clojure 98 | (st/select-schema {:street "Keskustori 8" 99 | :city "Tampere" 100 | :description "Metosin HQ" ; disallowed-key 101 | :country {:weather "-18" ; disallowed-key 102 | :name "Finland"}} 103 | Address) 104 | ; {:city "Tampere", :street "Keskustori 8", :country {:name "Finland"}} 105 | ``` 106 | 107 | Filtering out illegal schema map keys using coercion with additional Json-coercion - in a single sweep: 108 | 109 | ```clojure 110 | (s/defschema Beer {:beer (s/enum :ipa :apa)}) 111 | 112 | (def ipa {:beer "ipa" :taste "good"}) 113 | 114 | (st/select-schema ipa Beer) 115 | ; clojure.lang.ExceptionInfo: Could not coerce value to schema: {:beer (not (#{:ipa :apa} "ipa"))} 116 | ; data: {:type :schema.core/error, 117 | ; :schema {:beer {:vs #{:ipa :apa}}}, 118 | ; :value {:beer "ipa", :taste "good"}, 119 | ; :error {:beer (not (#{:ipa :apa} "ipa"))}} 120 | 121 | (require '[schema.coerce :as sc]) 122 | 123 | (st/select-schema ipa Beer sc/json-coercion-matcher) 124 | ; {:beer :ipa} 125 | ``` 126 | 127 | ## Usage 128 | 129 | See the [tests](https://github.com/metosin/schema-tools/tree/master/test/). 130 | 131 | ## License 132 | 133 | Copyright © 2014-2019 [Metosin Oy](http://www.metosin.fi) 134 | 135 | Distributed under the Eclipse Public License 2.0. 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "schema-tools", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "karma": "^6.3.16", 7 | "karma-chrome-launcher": "^3.1.0", 8 | "karma-cljs-test": "^0.1.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject metosin/schema-tools "0.13.1" 2 | :description "Common utilities for Prismatic Schema" 3 | :url "https://github.com/metosin/schema-tools" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v20.html"} 6 | :dependencies [[prismatic/schema "1.1.12"]] 7 | :plugins [[funcool/codeina "0.5.0"] 8 | [lein-doo "0.1.11"]] 9 | :test-paths ["test/clj" "test/cljc"] 10 | :codeina {:target "doc" 11 | :src-uri "http://github.com/metosin/schema-tools/blob/master/" 12 | :src-uri-prefix "#L"} 13 | :deploy-repositories [["releases" {:url "https://repo.clojars.org/" 14 | :sign-releases false}]] 15 | :profiles {:dev {:plugins [[jonase/eastwood "0.3.7"]] 16 | :dependencies [[criterium "0.4.6"] 17 | [org.clojure/clojure "1.10.2"] 18 | [org.clojure/clojurescript "1.10.773"]]} 19 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} 20 | :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}} 21 | :aliases {"all" ["with-profile" "dev:dev,1.8:dev,1.9"] 22 | "all-cljs" ["with-profile" "dev"] 23 | "test-clj" ["all" "do" ["test"] ["check"]] 24 | "test-cljs" ["all-cljs" "do" ["test-node"] ["test-chrome"] ["test-advanced"]] 25 | "test-chrome" ["doo" "chrome-headless" "test" "once"] 26 | "test-advanced" ["doo" "chrome-headless" "advanced-test" "once"] 27 | "test-node" ["doo" "node" "node-test" "once"]} 28 | :doo {:paths {:karma "npx karma"}} 29 | :cljsbuild {:builds [{:id "test" 30 | :source-paths ["src" "test/cljc" "test/cljs"] 31 | :compiler {:output-to "target/out/test.js" 32 | :output-dir "target/out" 33 | :main schema-tools.doo-runner 34 | :optimizations :none}} 35 | {:id "advanced-test" 36 | :source-paths ["src" "test/cljc" "test/cljs"] 37 | :compiler {:output-to "target/advanced_out/test.js" 38 | :output-dir "target/advanced_out" 39 | :main schema-tools.doo-runner 40 | :optimizations :advanced}} 41 | ;; Node.js requires :target :nodejs, hence the separate 42 | ;; build configuration. 43 | {:id "node-test" 44 | :source-paths ["src" "test/cljc" "test/cljs"] 45 | :compiler {:output-to "target/node_out/test.js" 46 | :output-dir "target/node_out" 47 | :main schema-tools.doo-runner 48 | :optimizations :none 49 | :target :nodejs}}]}) 50 | -------------------------------------------------------------------------------- /scripts/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rev=$(git rev-parse HEAD) 4 | remoteurl=$(git ls-remote --get-url origin) 5 | 6 | if [[ ! -d doc ]]; then 7 | git clone --branch gh-pages ${remoteurl} doc 8 | fi 9 | ( 10 | cd doc 11 | git pull 12 | ) 13 | 14 | lein doc 15 | cd doc 16 | git add --all 17 | git commit -m "Build docs from ${rev}." 18 | git push origin gh-pages 19 | -------------------------------------------------------------------------------- /scripts/build.clj: -------------------------------------------------------------------------------- 1 | (require 'cljs.closure) 2 | 3 | (cljs.closure/build 4 | ; Includes :source-paths and :test-paths already 5 | "test" 6 | {:main "schema-tools.runner" 7 | :output-to "target/generated/js/out/tests.js" 8 | :source-map true 9 | :output-dir "target/generated/js/out" 10 | :optimizations :none 11 | :target :nodejs}) 12 | 13 | (shutdown-agents) 14 | -------------------------------------------------------------------------------- /scripts/repl.clj: -------------------------------------------------------------------------------- 1 | (require 2 | '[cljs.repl :as repl] 3 | '[cljs.repl.node :as node]) 4 | 5 | (repl/repl* (node/repl-env) 6 | {:output-dir "target/generated/js/out" 7 | :optimizations :none 8 | :cache-analysis true 9 | :source-map true}) 10 | -------------------------------------------------------------------------------- /scripts/repl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rlwrap lein trampoline run -m clojure.main scripts/repl.clj 3 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | case $1 in 4 | cljs) 5 | lein test-cljs 6 | ;; 7 | clj) 8 | lein test-clj 9 | ;; 10 | *) 11 | echo "Please select [clj|cljs]" 12 | exit 1 13 | ;; 14 | esac 15 | -------------------------------------------------------------------------------- /src/schema_tools/coerce.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.coerce 2 | (:require [schema.core :as s] 3 | [schema.spec.core :as ss] 4 | [schema.utils :as su] 5 | [schema.coerce :as sc] 6 | [schema-tools.impl :as impl] 7 | #?@(:clj [clojure.edn] 8 | :cljs [[cljs.reader] 9 | [goog.date.UtcDateTime]])) 10 | #?(:clj 11 | (:import [java.util Date UUID] 12 | [java.util.regex Pattern] 13 | [java.time LocalDate LocalTime Instant] 14 | (clojure.lang APersistentSet Keyword)))) 15 | 16 | ;; 17 | ;; Internals 18 | ;; 19 | 20 | (defn- coerce-or-error! [value schema coercer type] 21 | (let [coerced (coercer value)] 22 | (if-let [error (su/error-val coerced)] 23 | (throw 24 | (ex-info 25 | (str "Could not coerce value to schema: " (pr-str error)) 26 | {:type type :schema schema :value value :error error})) 27 | coerced))) 28 | 29 | ; original: https://gist.github.com/abp/0c4106eba7b72802347b 30 | (defn- filter-schema-keys [m schema-keys extra-keys-checker] 31 | (reduce-kv 32 | (fn [m k _] 33 | (if (or (contains? schema-keys k) 34 | (and extra-keys-checker 35 | (not (su/error? (extra-keys-checker k))))) 36 | m 37 | (dissoc m k))) 38 | m 39 | m)) 40 | 41 | ;; 42 | ;; Matchers 43 | ;; 44 | 45 | ; original: https://gist.github.com/abp/0c4106eba7b72802347b 46 | (defn map-filter-matcher 47 | "Creates a matcher which removes all illegal keys from non-record maps." 48 | [schema] 49 | (when (and (map? schema) (not (record? schema))) 50 | (let [extra-keys-schema (s/find-extra-keys-schema schema) 51 | extra-keys-checker (when extra-keys-schema 52 | (ss/run-checker 53 | (fn [s params] 54 | (ss/checker (s/spec s) params)) 55 | true 56 | extra-keys-schema)) 57 | explicit-keys (some->> (dissoc schema extra-keys-schema) 58 | keys 59 | (mapv s/explicit-schema-key) 60 | set)] 61 | (when (or extra-keys-checker (seq explicit-keys)) 62 | (fn [x] 63 | (if (map? x) 64 | (filter-schema-keys x explicit-keys extra-keys-checker) 65 | x)))))) 66 | 67 | ; original: https://groups.google.com/forum/m/#!topic/prismatic-plumbing/NWUnqbYhfac 68 | (defn default-value-matcher 69 | "Creates a matcher which converts nils to default values. You can set default values 70 | with [[schema-tools.core/default]]." 71 | [schema] 72 | (when (impl/default? schema) 73 | (fn [value] 74 | (if (nil? value) (:value schema) value)))) 75 | 76 | (def ^:deprecated default-coercion-matcher 77 | "Deprecated - use [[default-value-matcher]] instead." 78 | default-value-matcher) 79 | 80 | (defn default-key-matcher 81 | "Creates a matcher which adds missing keys to a map if they have default values. 82 | You can set default values with [[schema-tools.core/default]]." 83 | [schema] 84 | ;; Can't use `map?` here, since we're looking for a map literal, but records 85 | ;; satisfy `map?`. 86 | (when (and (map? schema) (not (record? schema))) 87 | (let [default-map (reduce-kv (fn [acc k v] 88 | (if (impl/default? v) 89 | (assoc acc k (:value v)) 90 | acc)) 91 | {} 92 | schema)] 93 | (when (seq default-map) 94 | (fn [x] (merge default-map x)))))) 95 | 96 | (defn default-matcher 97 | "Combination of [[default-value-matcher]] and [[default-key-matcher]]: Creates 98 | a matcher which adds missing keys with default values to a map and converts 99 | nils to default values. You can set default values with 100 | [[schema-tools.core/default]]." 101 | [schema] 102 | (or (default-key-matcher schema) 103 | (default-value-matcher schema))) 104 | 105 | (defn multi-matcher 106 | "Creates a matcher for (accept-schema schema), reducing 107 | value with fs functions if (accept-value value)." 108 | [accept-schema accept-value fs] 109 | (fn [schema] 110 | (when (accept-schema schema) 111 | (fn [value] 112 | (if (accept-value value) 113 | (reduce #(%2 %1) value fs) 114 | value))))) 115 | 116 | (defn or-matcher 117 | "Creates a matcher where the first matcher matching the 118 | given schema is used." 119 | [& matchers] 120 | (fn [schema] 121 | (some #(% schema) matchers))) 122 | 123 | ;; alpha 124 | (defn ^:no-doc forwarding-matcher 125 | "Creates a matcher where all matchers are combined with OR, 126 | but if the lead-matcher matches, it creates a sub-coercer and 127 | forwards the coerced value to tail-matchers." 128 | [lead-matcher & tail-matchers] 129 | (let [match-tail (apply or-matcher tail-matchers)] 130 | (or-matcher 131 | (fn [schema] 132 | (if-let [f (lead-matcher schema)] 133 | (fn [x] 134 | (let [x1 (f x)] 135 | ; don't sub-coerce untouched values 136 | (if (and x1 (not= x x1)) 137 | (let [coercer (sc/coercer schema match-tail)] 138 | (coercer x1)) 139 | x1))))) 140 | match-tail))) 141 | 142 | ;; 143 | ;; coercion 144 | ;; 145 | 146 | (defn coercer 147 | "Produce a function that simultaneously coerces and validates a value against a `schema.` 148 | If a value can't be coerced to match the schema, an `ex-info` is thrown - like `schema.core/validate`, 149 | but with overridable `:type`, defaulting to `:schema-tools.coerce/error.`" 150 | ([schema] 151 | (coercer schema (constantly nil))) 152 | ([schema matcher] 153 | (coercer schema matcher ::error)) 154 | ([schema matcher type] 155 | (let [coercer (sc/coercer schema matcher)] 156 | (fn [value] 157 | (coerce-or-error! value schema coercer type))))) 158 | 159 | (defn coerce 160 | "Simultaneously coerces and validates a value to match the given `schema.` If a `value` can't 161 | be coerced to match the `schema`, an `ex-info` is thrown - like `schema.core/validate`, 162 | but with overridable `:type`, defaulting to `:schema-tools.coerce/error.`" 163 | ([value schema] 164 | (coerce value schema (constantly nil))) 165 | ([value schema matcher] 166 | (coerce value schema matcher ::error)) 167 | ([value schema matcher type] 168 | ((coercer schema matcher type) value))) 169 | 170 | ;; 171 | ;; coercions 172 | ;; 173 | 174 | (defn- safe [f] 175 | (fn [x] 176 | (try 177 | (f x) 178 | (catch #?(:clj Exception, :cljs js/Error) _ x)))) 179 | 180 | (defn string->boolean [x] 181 | (if (string? x) 182 | (condp = x 183 | "true" true 184 | "false" false 185 | x) 186 | x)) 187 | 188 | #?(:clj 189 | (defn string->long [^String x] 190 | (if (string? x) 191 | (try 192 | (Long/valueOf x) 193 | (catch #?(:clj Exception, :cljs js/Error) _ x)) 194 | x))) 195 | 196 | #?(:clj 197 | (defn string->double [^String x] 198 | (if (string? x) 199 | (try 200 | (Double/valueOf x) 201 | (catch #?(:clj Exception, :cljs js/Error) _ x)) 202 | x))) 203 | 204 | (defn- safe-int [x] 205 | #?(:clj (sc/safe-long-cast x) 206 | :cljs x)) 207 | 208 | (defn string->number [^String x] 209 | (if (string? x) 210 | (try 211 | (let [parsed #?(:clj (clojure.edn/read-string x) 212 | :cljs (cljs.reader/read-string x))] 213 | (if (number? parsed) parsed x)) 214 | (catch #?(:clj Exception, :cljs js/Error) _ x)) 215 | x)) 216 | 217 | (defn string->uuid [x] 218 | (if (string? x) 219 | (try 220 | #?(:clj (UUID/fromString x) 221 | ;; http://stackoverflow.com/questions/7905929/how-to-test-valid-uuid-guid 222 | :cljs (if (re-find #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" x) 223 | (uuid x) 224 | x)) 225 | (catch #?(:clj Exception, :cljs js/Error) _ x)) 226 | x)) 227 | 228 | (defn string->date [x] 229 | (if (string? x) 230 | (try 231 | #?(:clj (Date/from (Instant/parse x)) 232 | :cljs (js/Date. (.getTime (goog.date.UtcDateTime.fromIsoString x)))) 233 | (catch #?(:clj Exception, :cljs js/Error) _ x)) 234 | x)) 235 | 236 | (defn keyword->string [x] 237 | (if (keyword? x) 238 | (if-let [kw-ns (namespace x)] 239 | (str kw-ns "/" (name x)) 240 | (name x)) 241 | x)) 242 | 243 | (defn keyword->number [x] 244 | (if (keyword? x) 245 | (-> x keyword->string string->number) 246 | x)) 247 | 248 | (defn keyword->bool [x] 249 | (if (keyword? x) 250 | (-> x keyword->string string->boolean) 251 | x)) 252 | 253 | (defn collection-matcher [schema] 254 | (if (or (and (coll? schema) (not (record? schema)))) 255 | (fn [x] (if (coll? x) x [x])))) 256 | 257 | (def +json-coercions+ 258 | {s/Keyword sc/string->keyword 259 | s/Str keyword->string 260 | #?@(:clj [Keyword sc/string->keyword]) 261 | s/Uuid (comp string->uuid keyword->string) 262 | s/Int (comp safe-int keyword->number) 263 | s/Bool keyword->bool 264 | #?@(:clj [Long (comp sc/safe-long-cast keyword->number)]) 265 | #?@(:clj [Double (comp double keyword->number)]) 266 | #?@(:clj [Pattern (safe (comp re-pattern keyword->string))]) 267 | #?@(:clj [Date (comp string->date keyword->string)]) 268 | #?@(:cljs [js/Date (comp string->date keyword->string)]) 269 | #?@(:clj [LocalDate (safe #(LocalDate/parse (keyword->string %)))]) 270 | #?@(:clj [LocalTime (safe #(LocalTime/parse (keyword->string %)))]) 271 | #?@(:clj [Instant (safe #(Instant/parse (keyword->string %)))])}) 272 | 273 | (def +string-coercions+ 274 | {s/Int (comp safe-int string->number keyword->string) 275 | s/Num (comp string->number keyword->string) 276 | s/Bool (comp string->boolean keyword->string) 277 | #?@(:clj [Long (comp safe-int string->long keyword->string)]) 278 | #?@(:clj [Double (comp double string->double keyword->string)])}) 279 | 280 | ;; 281 | ;; matchers 282 | ;; 283 | 284 | (def json-coercion-matcher 285 | (some-fn +json-coercions+ 286 | sc/keyword-enum-matcher 287 | sc/set-matcher)) 288 | 289 | (def string-coercion-matcher 290 | (some-fn +string-coercions+ 291 | collection-matcher 292 | json-coercion-matcher)) 293 | -------------------------------------------------------------------------------- /src/schema_tools/core.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.core 2 | (:require [schema.core :as s] 3 | [schema-tools.coerce :as stc] 4 | [schema-tools.util :as stu] 5 | [schema-tools.walk :as walk] 6 | [schema.spec.variant :as variant] 7 | [schema.spec.core :as spec] 8 | [schema-tools.impl :as impl]) 9 | (:refer-clojure :exclude [assoc dissoc select-keys update get-in assoc-in update-in merge])) 10 | 11 | (defn- explicit-key [k] (if (s/specific-key? k) (s/explicit-schema-key k) k)) 12 | 13 | (defn- explicit-key-set [ks] 14 | (reduce (fn [s k] (conj s (explicit-key k))) #{} ks)) 15 | 16 | (defn- single-sequence-element? [x] 17 | (instance? schema.core.One x)) 18 | 19 | (defn- index-in-schema [m k] 20 | (let [last-idx (dec (count m))] 21 | (cond 22 | (<= k last-idx) k 23 | (not (single-sequence-element? (get m last-idx))) last-idx 24 | :else nil))) 25 | 26 | (defn- key-in-schema [m k] 27 | (cond 28 | (and (sequential? m) (number? k)) (index-in-schema m k) 29 | (contains? m k) k 30 | (contains? m (s/optional-key k)) (s/optional-key k) 31 | (contains? m (s/required-key k)) (s/required-key k) 32 | (and (s/specific-key? k) (contains? m (s/explicit-schema-key k))) (s/explicit-schema-key k) 33 | :else k)) 34 | 35 | (defn- unwrap-sequence-schemas [m] 36 | (cond 37 | (single-sequence-element? m) (:schema m) 38 | :else m)) 39 | 40 | (defn- get-in-schema [m k & [default]] 41 | (unwrap-sequence-schemas (get m (key-in-schema m k) default))) 42 | 43 | (defn- maybe-anonymous [original current] 44 | (if (= original current) 45 | original 46 | (vary-meta 47 | current 48 | (fn [meta] 49 | (let [new-meta (clojure.core/dissoc meta :name :ns)] 50 | (if (empty? new-meta) 51 | nil 52 | new-meta)))))) 53 | 54 | (defn- transform-keys 55 | [schema f ks optional-keys-schema?] 56 | (assert (or (not ks) (vector? ks)) "input should be nil or a vector of keys.") 57 | (maybe-anonymous 58 | schema 59 | (let [ks? (explicit-key-set ks)] 60 | (stu/map-keys 61 | (fn [k] 62 | (cond 63 | (and ks (not (ks? (explicit-key k)))) k 64 | (s/specific-key? k) (f (s/explicit-schema-key k)) 65 | optional-keys-schema? k 66 | :else (f k))) 67 | schema)))) 68 | 69 | ;; 70 | ;; Definitions 71 | ;; 72 | 73 | (def AnyKeys {s/Any s/Any}) 74 | (defn any-keys [] AnyKeys) 75 | 76 | (def AnyKeywordKeys {s/Keyword s/Any}) 77 | (defn any-keyword-keys [& schemas] (apply clojure.core/merge AnyKeywordKeys schemas)) 78 | 79 | ;; 80 | ;; Core functions 81 | ;; 82 | 83 | (defn assoc 84 | "Assoc[iate]s key & vals into Schema." 85 | [schema & kvs] 86 | (maybe-anonymous 87 | schema 88 | (reduce 89 | (fn [schema [k v]] 90 | #?(:clj (when-not v 91 | (throw (IllegalArgumentException. 92 | "assoc expects even number of arguments after map/vector, found odd number")))) 93 | (let [rk (key-in-schema schema k)] 94 | (-> schema 95 | (clojure.core/dissoc rk) 96 | (clojure.core/assoc k v)))) 97 | schema 98 | (partition 2 2 nil kvs)))) 99 | 100 | (defn dissoc 101 | "Dissoc[iate]s keys from Schema." 102 | [schema & ks] 103 | (maybe-anonymous 104 | schema 105 | (reduce 106 | (fn [schema k] (clojure.core/dissoc schema (key-in-schema schema k))) 107 | schema ks))) 108 | 109 | (defn select-keys 110 | "Like `clojure.core/select-keys` but handles boths optional-keys and required-keys." 111 | [schema ks] 112 | (maybe-anonymous 113 | schema 114 | (let [ks? (explicit-key-set ks)] 115 | (into {} (filter (comp ks? explicit-key key) schema))))) 116 | 117 | (defn schema-value 118 | "Returns the sub-schema or sub-schemas of given schema." 119 | [s] 120 | (impl/schema-value s)) 121 | 122 | (defn get-in 123 | "Returns the value in a nested associative Schema, 124 | where `ks` is a sequence of keys. Returns `nil` if the key 125 | is not present, or the `not-found` value if supplied." 126 | ([m ks] 127 | (get-in m ks nil)) 128 | ([m ks not-found] 129 | (loop [sentinel #?(:clj (Object.) :cljs (js/Object.)) 130 | m m 131 | ks (seq ks)] 132 | (if ks 133 | (let [k (first ks) 134 | m (get-in-schema m k sentinel)] 135 | (if (identical? sentinel m) 136 | not-found 137 | (recur sentinel m (next ks)))) 138 | m)))) 139 | 140 | (defn assoc-in 141 | "Associates a value in a nested associative Schema, where `ks` is a 142 | sequence of keys and `v` is the new value and returns a new nested Schema. 143 | If any levels do not exist, hash-maps will be created." 144 | [schema [k & ks] v] 145 | (maybe-anonymous 146 | schema 147 | (let [kis (key-in-schema schema k)] 148 | (if ks 149 | (clojure.core/assoc schema kis (assoc-in (get-in-schema schema k) ks v)) 150 | (clojure.core/assoc schema kis v))))) 151 | 152 | (defn update-in 153 | "'Updates' a value in a nested associative Schema, where `ks` is a 154 | sequence of keys and `f` is a function that will take the old value 155 | and any supplied args and return the new value, and returns a new 156 | nested Schema. If any levels do not exist, hash-maps will be 157 | created." 158 | [schema [k & ks] f & args] 159 | (maybe-anonymous 160 | schema 161 | (let [kis (key-in-schema schema k)] 162 | (if ks 163 | (clojure.core/assoc schema kis (apply update-in (get-in-schema schema k) ks f args)) 164 | (clojure.core/assoc schema kis (apply f (get-in-schema schema k) args)))))) 165 | 166 | ;; (c) original https://github.com/clojure/core.incubator/blob/master/src/main/clojure/clojure/core/incubator.clj 167 | (defn dissoc-in 168 | "Dissociates an entry from a nested associative Schema returning a new 169 | nested structure. keys is a sequence of keys. Any empty maps that result 170 | will not be present in the new Schema." 171 | [schema [k & ks]] 172 | (let [k (key-in-schema schema k)] 173 | (if ks 174 | (if-let [nextmap (get schema k)] 175 | (let [newmap (dissoc-in nextmap ks)] 176 | (if (seq newmap) 177 | (clojure.core/assoc schema k newmap) 178 | (dissoc schema k))) 179 | schema) 180 | (dissoc schema k)))) 181 | 182 | (defn update 183 | "Updates a value in a map with a function." 184 | [schema k f & args] 185 | (apply update-in schema [k] f args)) 186 | 187 | (defn merge 188 | "Returns a Schema that consists of the rest of the Schemas conj-ed onto 189 | the first. If a schema key occurs in more than one map, the mapping from 190 | the latter (left-to-right) will be the mapping in the result. Works only 191 | with Map schemas." 192 | [& schemas] 193 | {:pre [(every? #(or (map? %) (nil? %)) schemas)]} 194 | (maybe-anonymous 195 | (first schemas) 196 | (when (some identity schemas) 197 | (reduce 198 | (fn [acc m] 199 | (reduce 200 | (fn [acc [k v]] 201 | (clojure.core/assoc (dissoc acc k) k v)) 202 | acc m)) schemas)))) 203 | 204 | ;; 205 | ;; Defaults 206 | ;; 207 | 208 | (defn default [schema default] 209 | (impl/default schema default)) 210 | 211 | ;; 212 | ;; Schema 213 | ;; 214 | 215 | (defrecord Schema [schema data] 216 | s/Schema 217 | (spec [_] 218 | (variant/variant-spec 219 | spec/+no-precondition+ 220 | [{:schema schema}])) 221 | (explain [this] 222 | (let [ops (select-keys data [:name :description])] 223 | (-> ['schema (-> this :schema s/explain)] 224 | (cond-> (seq ops) (conj ops)) 225 | (seq))))) 226 | 227 | (defn schema 228 | ([pred] 229 | (schema pred nil)) 230 | ([pred data] 231 | (->Schema pred data))) 232 | 233 | ;; 234 | ;; Extras 235 | ;; 236 | 237 | (defn optional-keys 238 | "Makes given map keys optional. Defaults to all keys." 239 | ([m] (optional-keys m nil)) 240 | ([m ks] (transform-keys m s/optional-key ks false))) 241 | 242 | (defn required-keys 243 | "Makes given map keys required. Defaults to all keys." 244 | ([m] (required-keys m nil)) 245 | ([m ks] (transform-keys m #(if (keyword? %) % (s/required-key %)) ks false))) 246 | 247 | (defn select-schema 248 | "Strips all disallowed keys from nested Map schemas via coercion. Takes an optional 249 | coercion matcher for extra coercing the selected value(s) on a single sweep. If a value 250 | can't be coerced to match the schema `ExceptionInfo` is thrown (like `schema.core/validate`)." 251 | ([value schema] 252 | (select-schema value schema (constantly nil))) 253 | ([value schema matcher] 254 | (stc/coerce value schema (stc/or-matcher stc/map-filter-matcher matcher)))) 255 | 256 | (defn open-schema 257 | "Walks a schema adding [`s/Keyword` `s/Any`] entry to all Map Schemas" 258 | [schema] 259 | (walk/prewalk 260 | (fn [x] 261 | (if (and (map? x) (not (record? x)) (not (s/find-extra-keys-schema x))) 262 | (assoc x s/Keyword s/Any) 263 | x)) 264 | schema)) 265 | 266 | (defn optional-keys-schema 267 | "Walks a schema making all keys optional in Map Schemas." 268 | [schema] 269 | (walk/prewalk 270 | (fn [x] 271 | (if (and (map? x) (not (record? x))) 272 | (transform-keys x s/optional-key nil true) 273 | x)) 274 | schema)) 275 | 276 | (defn schema-with-description 277 | "Records description in schema's metadata." 278 | [schema description] 279 | (vary-meta schema assoc :description description)) 280 | 281 | (defn schema-description 282 | "Returns the description of a schema attached via schema-with-description." 283 | [schema] 284 | (-> schema meta :description)) 285 | 286 | #?(:clj 287 | (defn resolve-schema 288 | "Returns the schema var if the schema contains the `:name` and `:ns` 289 | definitions (set by `schema.core/defschema`)." 290 | [schema] 291 | (if-let [schema-ns (s/schema-ns schema)] 292 | (ns-resolve schema-ns (s/schema-name schema))))) 293 | 294 | #?(:clj 295 | (defn resolve-schema-description 296 | "Returns the schema description, in this lookup order: 297 | a) schema meta :description 298 | b) schema var meta :doc if not \"\" 299 | c) nil" 300 | [schema] 301 | (or (schema-description schema) 302 | (if-let [schema-ns (s/schema-ns schema)] 303 | (let [doc (-> (ns-resolve schema-ns (s/schema-name schema)) meta :doc)] 304 | (if-not (= "" doc) doc)))))) 305 | -------------------------------------------------------------------------------- /src/schema_tools/experimental/walk.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.experimental.walk 2 | "Add walk support for schema.experimental.* Schemas. 3 | 4 | Extends the WalkableSchema so requiring this namespace somewhere provides 5 | global support. 6 | 7 | Note: Walking through either abstract-map or extended-schema doesn't change 8 | the other. I.e. if you have Animal and Cat, which extends Animal, and walk 9 | through the Cat the Animal is not changed." 10 | (:require [schema-tools.walk :as sw] 11 | [schema.experimental.abstract-map :as abstract-map])) 12 | 13 | (extend-protocol sw/WalkableSchema 14 | schema.experimental.abstract_map.AbstractSchema 15 | (-walk [this inner outer] 16 | (outer (with-meta (abstract-map/->AbstractSchema 17 | (atom (reduce-kv (fn [acc k sub-schema] 18 | (assoc acc k (inner sub-schema))) 19 | {} 20 | @(:sub-schemas this))) 21 | (:type this) 22 | (inner (:schema this)) 23 | (:open? this)) 24 | (meta this)))) 25 | 26 | schema.experimental.abstract_map.SchemaExtension 27 | (-walk [this inner outer] 28 | (outer (with-meta (abstract-map/->SchemaExtension 29 | (:schema-name this) 30 | (inner (:base-schema this)) 31 | (inner (:extended-schema this)) 32 | (:explain-value this)) 33 | (meta this))))) 34 | -------------------------------------------------------------------------------- /src/schema_tools/impl.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.impl 2 | (:require [schema.core :as s] 3 | [schema.spec.variant :as variant] 4 | [schema.spec.core :as spec])) 5 | 6 | (defn unlift-keys [data ns-name] 7 | (reduce 8 | (fn [acc [k v]] 9 | (if (= ns-name (namespace k)) 10 | (assoc acc (keyword (name k)) v) 11 | acc)) 12 | {} data)) 13 | 14 | (defprotocol SchemaValue 15 | (schema-value [this] "Returns the sub-schema for given schema.")) 16 | 17 | (extend-protocol SchemaValue 18 | schema.core.One 19 | (schema-value [this] (:schema this)) 20 | 21 | schema.core.Maybe 22 | (schema-value [this] (:schema this)) 23 | 24 | schema.core.Both 25 | (schema-value [this] (vec (:schemas this))) 26 | 27 | schema.core.Either 28 | (schema-value [this] (vec (:schemas this))) 29 | 30 | #?@(:clj [schema.core.Recursive 31 | (schema-value [this] @(:derefable this))]) 32 | 33 | ; schema.core.Predicate 34 | ; (schema-value [this] (:p? this)) 35 | 36 | schema.core.NamedSchema 37 | (schema-value [this] (:schema this)) 38 | 39 | schema.core.ConditionalSchema 40 | (schema-value [this] (vec (map second (:preds-and-schemas this)))) 41 | 42 | schema.core.CondPre 43 | (schema-value [this] (vec (:schemas this))) 44 | 45 | schema.core.Constrained 46 | (schema-value [this] (:schema this)) 47 | 48 | schema.core.EnumSchema 49 | (schema-value [this] (:vs this)) 50 | 51 | #?(:clj Object :cljs default) 52 | (schema-value [this] this) 53 | 54 | nil 55 | (schema-value [_] nil)) 56 | 57 | ;; 58 | ;; Default 59 | ;; 60 | 61 | (defrecord Default [schema value] 62 | s/Schema 63 | (spec [_] 64 | (variant/variant-spec spec/+no-precondition+ [{:schema schema}])) 65 | (explain [_] 66 | (list 'default (s/explain schema) value))) 67 | 68 | (def default? (partial instance? Default)) 69 | 70 | (defn default [schema value] 71 | (s/validate schema value) 72 | (->Default schema value)) 73 | -------------------------------------------------------------------------------- /src/schema_tools/openapi/core.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.openapi.core 2 | #?@ 3 | (:clj 4 | [(:require 5 | [clojure.walk :as walk] 6 | [schema-tools.impl :as impl] 7 | [schema.core :as s] 8 | [schema.utils :as su])] 9 | :cljs 10 | [(:require 11 | [clojure.string :as str] 12 | [clojure.walk :as walk] 13 | [schema-tools.impl :as impl] 14 | [schema.core :as s] 15 | [schema.utils :as su])])) 16 | 17 | ;; 18 | ;; common 19 | ;; 20 | 21 | (declare transform) 22 | 23 | (defn record-schema 24 | [x] 25 | (when-let [schema (some-> x su/class-schema :schema)] 26 | (let [name #?(:clj (.getSimpleName ^Class x), 27 | :cljs (some-> su/class-schema :klass pr-str (str/split "/") last))] 28 | (s/named schema (str name "Record"))))) 29 | 30 | (defn- collection-schema 31 | [e options] 32 | (-> {:type "array" 33 | :items (if (not (next e)) 34 | (transform (first e) (assoc options ::no-meta true)) 35 | {:oneOf (-> #(transform % (assoc options ::no-meta true)) 36 | (mapv e) 37 | (set) 38 | (vec))})})) 39 | 40 | (defn plain-map? 41 | [m] 42 | (and (map? m) 43 | (not (record? m)))) 44 | 45 | (defn remove-empty-keys 46 | [m] 47 | (into (empty m) (filter (comp not nil? val) m))) 48 | 49 | (defn schema-name 50 | [schema opts] 51 | (when-let [name (some-> 52 | (or (:name opts) 53 | (s/schema-name schema) 54 | (when (instance? #?(:clj schema.core.NamedSchema 55 | :cljs s/NamedSchema) 56 | schema) 57 | (:name schema))) 58 | (name))] 59 | (let [ns (s/schema-ns schema)] 60 | (if ns (str ns "/" name) name)))) 61 | 62 | (defn key-name 63 | [k] 64 | (if (keyword? k) 65 | (let [n (namespace k)] 66 | (str (and n (str n "/")) (name k))) 67 | k)) 68 | 69 | (defn properties 70 | [schema opts] 71 | (some->> (for [[k v] schema 72 | :when (s/specific-key? k) 73 | :let [v (transform v opts)]] 74 | (and v [(key-name (s/explicit-schema-key k)) v])) 75 | (seq) 76 | (into (empty schema)))) 77 | 78 | (defn additional-properties 79 | [schema] 80 | (if-let [extra-key (s/find-extra-keys-schema schema)] 81 | (let [v (get schema extra-key)] 82 | (transform v nil)) 83 | false)) 84 | 85 | (defn object-schema 86 | [this opts] 87 | (when (plain-map? this) 88 | (remove-empty-keys 89 | {:type "object" 90 | :title (schema-name this opts) 91 | :properties (properties this opts) 92 | :additionalProperties (additional-properties this) 93 | :required (some->> (filterv s/required-key? (keys this)) 94 | (seq) 95 | (mapv key-name))}))) 96 | 97 | (defn not-supported! 98 | [schema] 99 | (ex-info 100 | (str "don't know how to convert " schema " into a OpenAPI schema. ") 101 | {:schema schema})) 102 | 103 | ;; 104 | ;; transformations 105 | ;; 106 | 107 | (defmulti transform-pred (fn [pred _] pred) :default ::default) 108 | 109 | (defmethod transform-pred string? 110 | [_ _] 111 | {:type "string"}) 112 | 113 | (defmethod transform-pred integer? 114 | [_ _] 115 | {:type "integer" :format "int32"}) 116 | 117 | (defmethod transform-pred keyword? 118 | [_ _] 119 | {:type "string"}) 120 | 121 | (defmethod transform-pred symbol? 122 | [_ _] 123 | {:type "string"}) 124 | 125 | (defmethod transform-pred pos? 126 | [_ _] 127 | {:type "number" :minimum 0 :exclusiveMinimum true}) 128 | 129 | (defmethod transform-pred neg? 130 | [_ _] 131 | {:type "number" :maximum 0 :exclusiveMaximum true}) 132 | 133 | (defmethod transform-pred even? 134 | [_ _] 135 | {:type "number" :multipleOf 2}) 136 | 137 | (defmethod transform-pred ::default 138 | [e {:keys [ignore-missing-mappings?]}] 139 | (when-not ignore-missing-mappings? 140 | (not-supported! e))) 141 | 142 | (defmulti transform-type (fn [t _] t) :default ::default) 143 | 144 | (defmethod transform-type #?(:clj java.lang.Boolean :cljs js/Boolean) 145 | [_ _] 146 | {:type "boolean"}) 147 | 148 | (defmethod transform-type #?(:clj java.lang.Number :cljs js/Number) 149 | [_ _] 150 | {:type "number" :format "double"}) 151 | 152 | (defmethod transform-type #?(:clj clojure.lang.Keyword :cljs cljs.core.Keyword) 153 | [_ _] 154 | {:type "string"}) 155 | 156 | (defmethod transform-type #?(:clj java.util.Date :cljs js/Date) 157 | [_ _] 158 | {:type "string" :format "date-time"}) 159 | 160 | (defmethod transform-type #?(:clj java.util.UUID :cljs cljs.core/UUID) 161 | [_ _] 162 | {:type "string" :format "uuid"}) 163 | 164 | (defmethod transform-type #?(:clj java.util.regex.Pattern :cljs schema.core.Regex) 165 | [_ _] 166 | {:type "string" :format "regex"}) 167 | 168 | (defmethod transform-type #?(:clj java.lang.String :cljs js/String) 169 | [_ _] 170 | {:type "string"}) 171 | 172 | #?(:clj (defmethod transform-type clojure.lang.Symbol 173 | [_ _] 174 | {:type "string"})) 175 | 176 | #?(:clj (defmethod transform-type java.time.Instant 177 | [_ _] 178 | {:type "string" :format "date-time"})) 179 | 180 | #?(:clj (defmethod transform-type java.time.LocalDate 181 | [_ _] 182 | {:type "string" :format "date"})) 183 | 184 | #?(:clj (defmethod transform-type java.time.LocalTime 185 | [_ _] 186 | {:type "string" :format "time"})) 187 | 188 | #?(:clj (defmethod transform-type java.io.File 189 | [_ _] 190 | {:type "file"})) 191 | 192 | #?(:clj (defmethod transform-type java.lang.Integer 193 | [_ _] 194 | {:type "integer" :format "int32"})) 195 | 196 | #?(:clj (defmethod transform-type java.lang.Long 197 | [_ _] 198 | {:type "integer" :format "int64"})) 199 | 200 | #?(:clj (defmethod transform-type java.lang.Double 201 | [_ _] 202 | {:type "number" :format "double"})) 203 | 204 | #?(:cljs (defmethod transform-type goog.date.Date 205 | [_ _] 206 | {:type "string" :format "date"})) 207 | 208 | #?(:cljs (defmethod transform-type goog.date.UtcDateTime 209 | [_ _] 210 | {:type "string" :format "date-time"})) 211 | 212 | (defmethod transform-type ::default 213 | [t {:keys [ignore-missing-mappings?]}] 214 | (when-not ignore-missing-mappings? 215 | (not-supported! t))) 216 | 217 | (defprotocol OpenapiSchema 218 | (-transform [this opts])) 219 | 220 | (defn transform 221 | [schema opts] 222 | (if (satisfies? OpenapiSchema schema) 223 | (-transform schema opts) 224 | (if-let [rschema (record-schema schema)] 225 | (transform rschema opts) 226 | (transform-type schema opts)))) 227 | 228 | (extend-protocol OpenapiSchema 229 | 230 | nil 231 | (-transform [_ _]) 232 | 233 | schema_tools.core.Schema 234 | (-transform [{:keys [schema data]} opts] 235 | (or (:openapi data) 236 | (merge 237 | (transform schema (merge opts (select-keys data [:name]))) 238 | (select-keys data [:description]) 239 | (impl/unlift-keys data "openapi")))) 240 | 241 | #?(:clj java.util.regex.Pattern 242 | :cljs js/RegExp) 243 | (-transform [this _] 244 | {:type "string" :pattern (str #?(:clj this, :cljs (.-source this)))}) 245 | 246 | schema.core.Both 247 | (-transform [this opts] 248 | {:allOf (mapv #(transform % opts) (:schemas this))}) 249 | 250 | schema.core.Predicate 251 | (-transform [this opts] 252 | (transform-pred (:p? this) opts)) 253 | 254 | schema.core.EnumSchema 255 | (-transform [this opts] 256 | (assoc (transform (type (first (:vs this))) opts) :enum (vec (:vs this)))) 257 | 258 | schema.core.Maybe 259 | (-transform [this opts] 260 | {:oneOf [(transform (:schema this) opts) 261 | {:type "null"}]}) 262 | 263 | schema.core.Either 264 | (-transform [this opts] 265 | {:oneOf (mapv #(transform % opts) (:schemas this))}) 266 | 267 | #_#_schema.core.Recursive 268 | (-transform [this opts] 269 | (transform (:derefable this) opts)) 270 | 271 | schema.core.EqSchema 272 | (-transform [this opts] 273 | {:enum [(:v this)]}) 274 | 275 | schema.core.One 276 | (-transform [this opts] 277 | (transform (:schema this) opts)) 278 | 279 | schema.core.AnythingSchema 280 | (-transform [_ {:keys [in] :as opts}] 281 | (if (and in (not= :body in)) 282 | (transform (s/maybe s/Str) opts) 283 | {})) 284 | 285 | schema.core.ConditionalSchema 286 | (-transform [this opts] 287 | {:oneOf (-> #(transform % opts) 288 | (comp second) 289 | (keep (:preds-and-schemas this)) 290 | (vec))}) 291 | 292 | schema.core.CondPre 293 | (-transform [this opts] 294 | {:oneOf (mapv #(transform % opts) (:schemas this))}) 295 | 296 | schema.core.Constrained 297 | (-transform [this opts] 298 | (transform (:schema this) opts)) 299 | 300 | schema.core.NamedSchema 301 | (-transform [{:keys [schema name]} opts] 302 | (transform schema (assoc opts :name name))) 303 | 304 | #?(:clj clojure.lang.Sequential 305 | :cljs cljs.core/List) 306 | (-transform [this options] 307 | (collection-schema this options)) 308 | 309 | #?(:clj clojure.lang.IPersistentSet 310 | :cljs cljs.core/PersistentHashSet) 311 | (-transform [this options] 312 | (assoc (collection-schema this options) :uniqueItems true)) 313 | 314 | #?(:clj clojure.lang.APersistentVector 315 | :cljs cljs.core.PersistentVector) 316 | (-transform [this options] 317 | (collection-schema this options)) 318 | 319 | #?(:clj clojure.lang.PersistentArrayMap 320 | :cljs cljs.core.PersistentArrayMap) 321 | (-transform [this opts] 322 | (object-schema this opts)) 323 | 324 | #?(:clj clojure.lang.PersistentHashMap 325 | :cljs cljs.core.PersistentHashMap) 326 | (-transform [this opts] 327 | (object-schema this opts))) 328 | 329 | ;; 330 | ;; Extract OpenAPI parameters 331 | ;; 332 | 333 | (defn- is-nilable? 334 | [spec] 335 | (and (contains? spec :oneOf) 336 | (= 2 (count (:oneOf spec))) 337 | (-> :type 338 | (group-by (:oneOf spec)) 339 | (contains? "null")))) 340 | 341 | (defn- extract-nilable 342 | [spec] 343 | (->> (:oneOf spec) 344 | (remove #(= (:type %) "null")) 345 | (first))) 346 | 347 | (defn- extract-single-param 348 | [in spec] 349 | (let [nilable? (is-nilable? spec) 350 | new-spec (if nilable? 351 | (extract-nilable spec) 352 | spec)] 353 | {:name (or (schema-name new-spec nil) 354 | (:title new-spec) 355 | (:type new-spec)) 356 | :in in 357 | :description (or (:description spec) 358 | "") 359 | :required (case in 360 | :path true 361 | (not nilable?)) 362 | :schema new-spec})) 363 | 364 | (defn- extract-object-param 365 | [in {:keys [properties required]}] 366 | (mapv 367 | (fn [[k schema]] 368 | {:name (or (schema-name schema nil) 369 | (key-name k)) 370 | :in (name in) 371 | :description (or (:description schema) 372 | "") 373 | :required (case in 374 | :path true 375 | (contains? (set required) k)) 376 | :schema schema}) 377 | properties)) 378 | 379 | (defn extract-parameter 380 | [in spec] 381 | (let [parameter-spec (transform spec nil) 382 | object? (and (contains? parameter-spec :properties) 383 | (= "object" (:type parameter-spec)))] 384 | (if object? 385 | (extract-object-param in parameter-spec) 386 | (-> (extract-single-param in parameter-spec) vector)))) 387 | 388 | ;; 389 | ;; expand the spec 390 | ;; 391 | 392 | (defmulti expand (fn [k _ _ _] k)) 393 | 394 | (defmethod expand ::schemas 395 | [_ v acc _] 396 | {:schemas 397 | (into 398 | (or (:schemas acc) {}) 399 | (for [[name schema] v] 400 | {name (transform schema nil)}))}) 401 | 402 | (defmethod expand ::content 403 | [_ v acc _] 404 | {:content 405 | (into 406 | (or (:content acc) {}) 407 | (for [[content-type schema] v] 408 | {content-type {:schema (transform schema nil)}}))}) 409 | 410 | (defmethod expand ::parameters 411 | [_ v acc _] 412 | (let [old (or (:parameters acc) []) 413 | new (mapcat (fn [[in spec]] (extract-parameter in spec)) v) 414 | merged (->> (into old new) 415 | (reverse) 416 | (reduce 417 | (fn [[ps cache :as acc] p] 418 | (let [c (select-keys p [:in :name])] 419 | (if-not (cache c) 420 | [(conj ps p) (conj cache c)] 421 | acc))) 422 | [[] #{}]) 423 | (first) 424 | (reverse) 425 | (vec))] 426 | {:parameters merged})) 427 | 428 | (defmethod expand ::headers 429 | [_ v acc _] 430 | {:headers 431 | (into 432 | (or (:headers acc) {}) 433 | (for [[name spec] v] 434 | {name (-> (extract-single-param :header (transform spec nil)) 435 | (dissoc :in :name))}))}) 436 | 437 | (defn expand-qualified-keywords 438 | [x options] 439 | (let [accept? (set (keys (methods expand)))] 440 | (walk/postwalk 441 | (fn [x] 442 | (if (plain-map? x) 443 | (reduce-kv 444 | (fn [acc k v] 445 | (if (accept? k) 446 | (-> acc (dissoc k) (merge (expand k v acc options))) 447 | acc)) 448 | x 449 | x) 450 | x)) 451 | x))) 452 | 453 | ;; 454 | ;; Generate the OpenAPI spec 455 | ;; 456 | 457 | ;; Top-level openapi spec generation was moved to reitit in 458 | ;; https://github.com/metosin/reitit/pull/638 459 | ;; 460 | ;; Once reitit-0.7.0-alpha6 has been out for some time, this can be 461 | ;; deleted since it should have no other users. 462 | (defn ^:deprecated openapi-spec 463 | "Transform data into an OpenAPI spec. Input data must conform to the Swagger3 464 | Spec (https://swagger.io/specification/) with a exception that it can have 465 | any qualified keywords which are expanded with the 466 | `schema-tools.openapi.core/expand` multimethod." 467 | ([x] 468 | (openapi-spec x nil)) 469 | ([x options] 470 | (expand-qualified-keywords x options))) 471 | -------------------------------------------------------------------------------- /src/schema_tools/swagger/core.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.swagger.core 2 | (:require [clojure.walk :as walk] 3 | [schema-tools.core] 4 | [schema.utils :as su] 5 | [schema.core :as s] 6 | [clojure.string :as str] 7 | #?@(:cljs [goog.date.UtcDateTime 8 | goog.date.Date]) 9 | [schema-tools.impl :as impl])) 10 | 11 | ;; 12 | ;; common 13 | ;; 14 | 15 | (declare transform) 16 | 17 | (defn remove-empty-keys [m] 18 | (into (empty m) (filter (comp not nil? val) m))) 19 | 20 | (defn record-schema [x] 21 | (if-let [schema (some-> x su/class-schema :schema)] 22 | (let [name #?(:clj (.getSimpleName ^Class x) 23 | ;; TODO: phantom generates invalid names 24 | :cljs (some-> x su/class-schema :klass pr-str (str/split "/") last))] 25 | (s/named schema (str name "Record"))))) 26 | 27 | (defn plain-map? [x] 28 | (and (map? x) 29 | (not (record? x)))) 30 | 31 | (defn schema-name [schema opts] 32 | (if-let [name (some-> 33 | (or 34 | (:name opts) 35 | (s/schema-name schema) 36 | (if (instance? #?(:clj schema.core.NamedSchema 37 | :cljs s/NamedSchema) schema) 38 | (:name schema))) 39 | (name))] 40 | (let [ns (s/schema-ns schema)] 41 | (if ns (str ns "/" name) name)))) 42 | 43 | (defn key-name [x] 44 | (if (keyword? x) 45 | (let [n (namespace x)] 46 | (str (if n (str n "/")) (name x))) 47 | x)) 48 | 49 | (defn assoc-collection-format [m {:keys [in] :as options}] 50 | (cond-> m 51 | (#{:query :formData} in) 52 | (assoc :collectionFormat (:collection-format options "multi")))) 53 | 54 | (defn not-supported! [schema] 55 | (throw 56 | (ex-info 57 | (str "don't know how to convert " schema " into a Swagger Schema. ") 58 | {:schema schema}))) 59 | 60 | (defn maybe? [schema] 61 | (instance? #?(:clj schema.core.Maybe 62 | :cljs s/Maybe) 63 | schema)) 64 | 65 | #_(defn reference? [m] 66 | (contains? m :$ref)) 67 | 68 | #_(defn reference [e {:keys [ignore-missing-mappings?]}] 69 | (if-let [schema-name (s/schema-name e)] 70 | {:$ref (str "#/definitions/" schema-name)} 71 | (if-not ignore-missing-mappings? 72 | (not-supported! e)))) 73 | 74 | (defn- collection-schema [e options] 75 | (-> {:type "array" 76 | :items (transform (first e) (assoc options ::no-meta true))} 77 | (assoc-collection-format options))) 78 | 79 | (defn properties [schema opts] 80 | (some->> (for [[k v] schema 81 | :when (s/specific-key? k) 82 | :let [v (transform v opts)]] 83 | (and v [(key-name (s/explicit-schema-key k)) v])) 84 | (seq) (into (empty schema)))) 85 | 86 | (defn additional-properties [schema] 87 | (if-let [extra-key (s/find-extra-keys-schema schema)] 88 | (let [v (get schema extra-key)] 89 | (transform v nil)) 90 | false)) 91 | 92 | (defn object-schema [this opts] 93 | (if (plain-map? this) 94 | (remove-empty-keys 95 | {:type "object" 96 | :title (schema-name this opts) 97 | :properties (properties this opts) 98 | :additionalProperties (additional-properties this) 99 | :required (some->> (filterv s/required-key? (keys this)) seq (mapv key-name))}))) 100 | 101 | ;; 102 | ;; transformations 103 | ;; 104 | 105 | (defmulti transform-pred (fn [this _] this) :default ::default) 106 | (defmethod transform-pred string? [_ _] {:type "string"}) 107 | (defmethod transform-pred integer? [_ _] {:type "integer" :format "int32"}) 108 | (defmethod transform-pred keyword? [_ _] {:type "string"}) 109 | (defmethod transform-pred symbol? [_ _] {:type "string"}) 110 | 111 | (defmethod transform-pred ::default [e {:keys [ignore-missing-mappings?]}] 112 | (if-not ignore-missing-mappings? 113 | (not-supported! e))) 114 | 115 | (defmulti transform-type (fn [c _] c) :default ::default) 116 | 117 | (defmethod transform-type #?(:clj java.lang.Boolean, 118 | :cljs js/Boolean) [_ _] {:type "boolean"}) 119 | (defmethod transform-type #?(:clj java.lang.Number, 120 | :cljs js/Number) [_ _] {:type "number" :format "double"}) 121 | (defmethod transform-type #?(:clj clojure.lang.Keyword, 122 | :cljs cljs.core.Keyword) [_ _] {:type "string"}) 123 | (defmethod transform-type #?(:clj java.util.Date, 124 | :cljs js/Date) [_ _] {:type "string" :format "date-time"}) 125 | (defmethod transform-type #?(:clj java.util.UUID, 126 | :cljs cljs.core/UUID) [_ _] {:type "string" :format "uuid"}) 127 | (defmethod transform-type #?(:clj java.util.regex.Pattern 128 | :cljs schema.core.Regex) [_ _] {:type "string" :format "regex"}) 129 | (defmethod transform-type #?(:clj String, 130 | :cljs js/String) [_ _] {:type "string"}) 131 | 132 | #?(:clj (defmethod transform-type clojure.lang.Symbol [_ _] {:type "string"})) 133 | #?(:clj (defmethod transform-type java.time.Instant [_ _] {:type "string" :format "date-time"})) 134 | #?(:clj (defmethod transform-type java.time.LocalDate [_ _] {:type "string" :format "date"})) 135 | #?(:clj (defmethod transform-type java.time.LocalTime [_ _] {:type "string" :format "time"})) 136 | #?(:clj (defmethod transform-type java.io.File [_ _] {:type "file"})) 137 | #?(:clj (defmethod transform-type java.lang.Integer [_ _] {:type "integer" :format "int32"})) 138 | #?(:clj (defmethod transform-type java.lang.Long [_ _] {:type "integer" :format "int64"})) 139 | #?(:clj (defmethod transform-type java.lang.Double [_ _] {:type "number" :format "double"})) 140 | 141 | #?(:cljs (defmethod transform-type goog.date.Date [_ _] {:type "string" :format "date"})) 142 | #?(:cljs (defmethod transform-type goog.date.UtcDateTime [_ _] {:type "string" :format "date-time"})) 143 | 144 | (defmethod transform-type ::default [e {:keys [ignore-missing-mappings?]}] 145 | (if-not ignore-missing-mappings? 146 | (not-supported! e))) 147 | 148 | (defprotocol SwaggerSchema 149 | (-transform [this opts])) 150 | 151 | (defn transform [schema opts] 152 | (if (satisfies? SwaggerSchema schema) 153 | (-transform schema opts) 154 | (if-let [rschema (record-schema schema)] 155 | (transform rschema opts) 156 | (transform-type schema opts)))) 157 | 158 | (extend-protocol SwaggerSchema 159 | 160 | nil 161 | (-transform [_ _]) 162 | 163 | schema_tools.core.Schema 164 | (-transform [{:keys [schema data]} opts] 165 | (or (:swagger data) 166 | (merge 167 | (transform schema (merge opts (select-keys data [:name :description]))) 168 | (impl/unlift-keys data "swagger")))) 169 | 170 | #?(:clj java.util.regex.Pattern 171 | :cljs js/RegExp) 172 | (-transform [this _] 173 | {:type "string" :pattern (str #?(:clj this 174 | :cljs (.-source this)))}) 175 | 176 | schema.core.Both 177 | (-transform [this options] 178 | (transform (first (:schemas this)) options)) 179 | 180 | schema.core.Predicate 181 | (-transform [this options] 182 | (transform-pred (:p? this) options)) 183 | 184 | schema.core.EnumSchema 185 | (-transform [this options] 186 | (assoc (transform (type (first (:vs this))) options) :enum (:vs this))) 187 | 188 | schema.core.Maybe 189 | (-transform [e {:keys [in] :as opts}] 190 | (let [schema (transform (:schema e) opts)] 191 | (condp contains? in 192 | #{:query :formData} (assoc schema :allowEmptyValue true) 193 | #{nil :body} (assoc schema :x-nullable true) 194 | schema))) 195 | 196 | schema.core.Either 197 | (-transform [this opts] 198 | (transform (first (:schemas this)) opts)) 199 | 200 | #_#_schema.core.Recursive 201 | (-transform [this opts] 202 | (transform (:derefable this) opts)) 203 | 204 | schema.core.EqSchema 205 | (-transform [this opts] 206 | (transform (type (:v this)) opts)) 207 | 208 | schema.core.One 209 | (-transform [this opts] 210 | (transform (:schema this) opts)) 211 | 212 | schema.core.AnythingSchema 213 | (-transform [_ {:keys [in] :as opts}] 214 | (if (and in (not= :body in)) 215 | (transform (s/maybe s/Str) opts) 216 | {})) 217 | 218 | schema.core.ConditionalSchema 219 | (-transform [this opts] 220 | {:x-oneOf (vec (keep (comp #(transform % opts) second) (:preds-and-schemas this)))}) 221 | 222 | schema.core.CondPre 223 | (-transform [this opts] 224 | {:x-oneOf (mapv #(transform % opts) (:schemas this))}) 225 | 226 | schema.core.Constrained 227 | (-transform [this opts] 228 | (transform (:schema this) opts)) 229 | 230 | schema.core.NamedSchema 231 | (-transform [{:keys [schema name]} opts] 232 | (transform schema (assoc opts :name name))) 233 | 234 | #?(:clj clojure.lang.Sequential 235 | :cljs cljs.core/List) 236 | (-transform [this options] 237 | (collection-schema this options)) 238 | 239 | #?(:clj clojure.lang.IPersistentSet 240 | :cljs cljs.core/PersistentHashSet) 241 | (-transform [this options] 242 | (assoc (collection-schema this options) :uniqueItems true)) 243 | 244 | #?(:clj clojure.lang.APersistentVector 245 | :cljs cljs.core.PersistentVector) 246 | (-transform [this options] 247 | (collection-schema this options)) 248 | 249 | #?(:clj clojure.lang.PersistentArrayMap 250 | :cljs cljs.core.PersistentArrayMap) 251 | (-transform [this opts] 252 | (object-schema this opts)) 253 | 254 | #?(:clj clojure.lang.PersistentHashMap 255 | :cljs cljs.core.PersistentHashMap) 256 | (-transform [this opts] 257 | (object-schema this opts))) 258 | 259 | ;; 260 | ;; extract swagger2 parameters 261 | ;; 262 | 263 | (defmulti extract-parameter (fn [in _] in)) 264 | 265 | (defmethod extract-parameter :body [_ schema] 266 | (let [swagger (transform schema {:in :body, :type :parameter})] 267 | [{:in "body" 268 | :name (or (schema-name schema nil) "body") 269 | :description "" 270 | :required (not (maybe? schema)) 271 | :schema swagger}])) 272 | 273 | (defmethod extract-parameter :default [in schema] 274 | (let [{:keys [properties required]} (transform schema {:in in, :type :parameter})] 275 | (mapv 276 | (fn [[k {:keys [type] :as swagger}]] 277 | (merge 278 | {:in (name in) 279 | :name (key-name k) 280 | :description "" 281 | :type type 282 | :required (contains? (set required) k)} 283 | swagger)) 284 | properties))) 285 | 286 | ;; 287 | ;; expand the spec 288 | ;; 289 | 290 | (defmulti expand (fn [k _ _ _] k)) 291 | 292 | (defmethod expand ::responses [_ v acc _] 293 | {:responses 294 | (into 295 | (or (:responses acc) {}) 296 | (for [[status response] v] 297 | [status (-> response 298 | (update :schema transform {:type :schema}) 299 | (update :description (fnil identity "")) 300 | (remove-empty-keys))]))}) 301 | 302 | (defmethod expand ::parameters [_ v acc _] 303 | (let [old (or (:parameters acc) []) 304 | new (mapcat (fn [[in spec]] (extract-parameter in spec)) v) 305 | merged (->> (into old new) 306 | (reverse) 307 | (reduce 308 | (fn [[ps cache :as acc] p] 309 | (let [c (select-keys p [:in :name])] 310 | (if-not (cache c) 311 | [(conj ps p) (conj cache c)] 312 | acc))) 313 | [[] #{}]) 314 | (first) 315 | (reverse) 316 | (vec))] 317 | {:parameters merged})) 318 | 319 | (defn expand-qualified-keywords [x options] 320 | (let [accept? (set (keys (methods expand)))] 321 | (walk/postwalk 322 | (fn [x] 323 | (if (plain-map? x) 324 | (reduce-kv 325 | (fn [acc k v] 326 | (if (accept? k) 327 | (-> acc (dissoc k) (merge (expand k v acc options))) 328 | acc)) 329 | x 330 | x) 331 | x)) 332 | x))) 333 | 334 | ;; 335 | ;; generate the swagger spec 336 | ;; 337 | 338 | (defn swagger-spec 339 | "Transforms data into a swagger2 spec. Input data must conform 340 | to the Swagger2 Spec (http://swagger.io/specification/) with a 341 | exception that it can have any qualified keywords that are expanded 342 | with the `schema-tools.swagger.core/expand` multimethod." 343 | ([x] 344 | (swagger-spec x nil)) 345 | ([x options] 346 | (expand-qualified-keywords x options))) 347 | -------------------------------------------------------------------------------- /src/schema_tools/util.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.util) 2 | 3 | (defn path-vals 4 | "Returns vector of tuples containing path vector to the value and the value." 5 | ([m] (path-vals m identity)) 6 | ([m fk] 7 | (letfn 8 | [(pvals [l p m] 9 | (reduce 10 | (fn [l [k v]] 11 | (let [k (fk k)] 12 | (if (map? v) 13 | (pvals l (conj p k) v) 14 | (cons [(conj p k) v] l)))) 15 | l m))] 16 | (pvals [] [] m)))) 17 | 18 | ;; https://github.com/clojure/core.incubator/blob/master/src/main/clojure/clojure/core/incubator.clj 19 | (defn dissoc-in 20 | "Dissociates an entry from a nested associative structure returning a new 21 | nested structure. keys is a sequence of keys. Any empty maps that result 22 | will not be present in the new structure." 23 | [m [k & ks]] 24 | (if ks 25 | (if-let [nextmap (get m k)] 26 | (let [newmap (dissoc-in nextmap ks)] 27 | (if (seq newmap) 28 | (assoc m k newmap) 29 | (dissoc m k))) 30 | m) 31 | (dissoc m k))) 32 | 33 | (defn map-keys [f m] 34 | (with-meta 35 | (persistent! 36 | (reduce-kv 37 | (fn [acc k v] (assoc! acc (f k) v)) 38 | (transient (empty m)) 39 | m)) 40 | (meta m))) 41 | -------------------------------------------------------------------------------- /src/schema_tools/walk.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.walk 2 | "Provides walk function which can be used to transform schemas while 3 | preserving their structure and type." 4 | (:require [schema.core :as s]) 5 | #?(:clj (:import [java.util Map$Entry]))) 6 | 7 | (defprotocol WalkableSchema 8 | (-walk [this inner outer])) 9 | 10 | (defn- schema-record? 11 | "Tests if the parameter is Schema record. I.e. not vector, map or other 12 | collection but implements Schema protocol." 13 | [x] 14 | (and (record? x) 15 | #?(:clj (instance? schema.core.Schema x) 16 | :cljs (satisfies? schema.core.Schema x)))) 17 | 18 | (defn walk 19 | "Calls `inner` for sub-schemas of this schema, creating new Schema of the same 20 | type as given and preserving the metadata. Calls `outer` with the created 21 | Schema." 22 | {:added "0.3.0"} 23 | [inner outer this] 24 | (cond 25 | ; Schemas with children 26 | (satisfies? WalkableSchema this) (-walk this inner outer) 27 | ; Leaf schemas - Rest Schema records should be the leaf schemas. 28 | (schema-record? this) (outer this) 29 | ; Regular clojure datastructures 30 | (record? this) (outer (with-meta (reduce (fn [r x] (conj r (inner x))) this this) (meta this))) 31 | #?@(:clj [(list? this) (outer (with-meta (apply list (map inner this)) (meta this)))]) 32 | (seq? this) (outer (with-meta (doall (map inner this)) (meta this))) 33 | (coll? this) (outer (with-meta (into (empty this) (map inner this)) (meta this))) 34 | :else (outer this))) 35 | 36 | (defn postwalk 37 | "Performs a depth-first, post-order traversal of `schema`. Calls `f` on 38 | each sub-form, uses f's return value in place of the original. 39 | Works with Schemas implementing schema-tools.walk/WalkableSchema, 40 | implementation is provided for built-in schemas. 41 | Consumes seqs as with doall." 42 | {:added "0.8"} 43 | [f schema] 44 | (walk (partial postwalk f) f schema)) 45 | 46 | (defn prewalk 47 | "Like postwalk, but does pre-order traversal." 48 | {:added "0.8"} 49 | [f schema] 50 | (walk (partial prewalk f) identity (f schema))) 51 | 52 | (extend-protocol WalkableSchema 53 | ;; Walk for map-entries doesn't have to return new map-entry, because 54 | ;; the result is used in (into {} ...) and vector will 55 | ;; work in that case. 56 | #?(:clj Map$Entry 57 | :cljs MapEntry) 58 | (-walk [this inner outer] 59 | (outer (with-meta (vec (map inner this)) (meta this)))) 60 | 61 | schema.core.Maybe 62 | (-walk [this inner outer] 63 | (outer (with-meta (s/maybe (inner (:schema this))) (meta this)))) 64 | 65 | schema.core.Both 66 | (-walk [this inner outer] 67 | (outer (with-meta (apply s/both (map inner (:schemas this))) (meta this)))) 68 | 69 | schema.core.Either 70 | (-walk [this inner outer] 71 | (outer (with-meta (apply s/either (map inner (:schemas this))) (meta this)))) 72 | 73 | #?@(:clj [schema.core.Recursive 74 | (-walk [this inner outer] 75 | (outer (with-meta (s/recursive (inner (:derefable this))) (meta this))))]) 76 | 77 | schema.core.Predicate 78 | (-walk [this _ outer] 79 | (outer this)) 80 | 81 | schema.core.NamedSchema 82 | (-walk [this inner outer] 83 | (outer (with-meta (s/named (inner (:schema this)) (:name this)) (meta this)))) 84 | 85 | schema.core.ConditionalSchema 86 | (-walk [this inner outer] 87 | (outer (with-meta (s/->ConditionalSchema 88 | (doall (for [[pred schema] (:preds-and-schemas this)] 89 | [pred (inner schema)])) 90 | (:error-symbol this)) 91 | (meta this)))) 92 | 93 | schema.core.CondPre 94 | (-walk [this inner outer] 95 | (outer (with-meta (apply s/cond-pre (map inner (:schemas this))) (meta this)))) 96 | 97 | schema.core.Constrained 98 | (-walk [this inner outer] 99 | (outer (with-meta (s/constrained (inner (:schema this)) (:postcondition this) (:post-name this)) (meta this))))) 100 | -------------------------------------------------------------------------------- /test/cljc/schema_tools/coerce_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.coerce-test 2 | (:require #?@(:clj [[clojure.test :refer [deftest testing is are]]] 3 | :cljs [[cljs.test :as test :refer-macros [deftest testing is are]] 4 | [cljs.reader] 5 | [goog.date.UtcDateTime]]) 6 | [clojure.string :as string] 7 | [schema.core :as s] 8 | [schema.coerce :as sc] 9 | [clojure.string :as str] 10 | [schema-tools.coerce :as stc] 11 | [schema.utils :as su]) 12 | #?(:clj 13 | (:import [java.util Date UUID] 14 | [java.util.regex Pattern] 15 | [java.time LocalDate LocalTime Instant] 16 | (clojure.lang Keyword)))) 17 | 18 | (deftest forwarding-matcher-test 19 | (let [string->vec (fn [schema] 20 | (if (vector? schema) 21 | (fn [x] 22 | (if (string? x) 23 | (str/split x #",") 24 | x)))) 25 | string->long (fn [schema] 26 | (if (= s/Int schema) 27 | (fn [x] 28 | (if (string? x) 29 | #?(:clj (Long/parseLong x) 30 | :cljs (js/parseInt x 10)) 31 | x)))) 32 | string->vec->long (stc/forwarding-matcher string->vec string->long) 33 | string->long->vec (stc/forwarding-matcher string->long string->vec)] 34 | 35 | (testing "string->vec->long is able to parse Long(s) and String(s) of Long(s)." 36 | (is (= {:a [1 2 3] 37 | :b [1 2 3] 38 | :c [1 2 3] 39 | :d [[1 2 3] [4 5 6] [7 8 9]] 40 | :e 1 41 | :f 1} 42 | ((sc/coercer {:a [s/Int] 43 | :b [s/Int] 44 | :c [s/Int] 45 | :d [[s/Int]] 46 | :e s/Int 47 | :f s/Int} 48 | string->vec->long) 49 | {:a [1 2 3] 50 | :b "1,2,3" 51 | :c ["1" "2" "3"] 52 | :d ["1,2,3" "4,5,6" "7,8,9"] 53 | :e 1 54 | :f "1"})))) 55 | 56 | (testing "string->long->vec is able to parse Long(s) and String(s) of Long(s)." 57 | (is (= {:a [1 2 3] 58 | :b [1 2 3] 59 | :c [1 2 3] 60 | :d [[1 2 3] [4 5 6] [7 8 9]] 61 | :e 1 62 | :f 1} 63 | ((sc/coercer {:a [s/Int] 64 | :b [s/Int] 65 | :c [s/Int] 66 | :d [[s/Int]] 67 | :e s/Int 68 | :f s/Int} 69 | string->long->vec) 70 | {:a [1 2 3] 71 | :b "1,2,3" 72 | :c ["1" "2" "3"] 73 | :d ["1,2,3" "4,5,6" "7,8,9"] 74 | :e 1 75 | :f "1"})))))) 76 | 77 | (deftest or-matcher-test 78 | (let [boolean? #(or (true? %) (false? %)) 79 | base-matcher (fn [schema-pred value-pred value-fn] 80 | (fn [schema] 81 | (if (schema-pred schema) 82 | (fn [x] 83 | (if (value-pred x) 84 | (value-fn x)))))) 85 | m1 (base-matcher #(= s/Str %) string? #(string/upper-case %)) 86 | m2 (base-matcher #(= s/Int %) number? inc) 87 | m3 (base-matcher #(= s/Bool %) boolean? not) 88 | m4 (base-matcher #(= s/Str %) string? #(string/lower-case %))] 89 | (testing "or-matcher selects first matcher where schema matches" 90 | (is (= {:band "KISS", :number 42, :lucid true} 91 | ((sc/coercer {:band s/Str :number s/Int :lucid s/Bool} 92 | (stc/or-matcher m1 m2 m3 m4)) 93 | {:band "kiss", :number 41, :lucid false})))))) 94 | 95 | (deftest coercer-test 96 | 97 | (testing "1-arity just for validating" 98 | (is (= "kikka" ((stc/coercer s/Str) "kikka")))) 99 | 100 | (testing "default case" 101 | 102 | (let [matcher {s/Str #(if (string? %) (string/upper-case %) %)} 103 | coercer (stc/coercer s/Str matcher)] 104 | 105 | (testing "successfull coercion retuns coerced value" 106 | (is (= "KIKKA" (coercer "kikka")))) 107 | 108 | (testing "failed coercion throws ex-info" 109 | (try 110 | (coercer 123) 111 | (catch #?(:clj Exception :cljs js/Error) e 112 | (let [{:keys [schema type value]} (ex-data e)] 113 | (is (= :schema-tools.coerce/error type)) 114 | (is (= s/Str schema)) 115 | (is (= 123 value)))))))) 116 | 117 | (testing "custom type" 118 | (let [matcher {s/Str #(if (string? %) (string/upper-case %) %)} 119 | coercer (stc/coercer s/Str matcher ::horror)] 120 | 121 | (testing "successfull coercion retuns coerced value" 122 | (is (= "KIKKA" (coercer "kikka")))) 123 | 124 | (testing "failed coercion throws ex-info" 125 | (try 126 | (coercer 123) 127 | (catch #?(:clj Exception :cljs js/Error) e 128 | (let [{:keys [schema type value]} (ex-data e)] 129 | (is (= ::horror type)) 130 | (is (= s/Str schema)) 131 | (is (= 123 value))))))))) 132 | 133 | (deftest multi-matcher-test 134 | (let [schema {:a s/Int, :b s/Int} 135 | matcher (stc/multi-matcher (partial = s/Int) integer? [(partial * 2) dec])] 136 | (is (= {:a 3 :b 19} ((sc/coercer! schema matcher) {:a 2 :b 10}))))) 137 | 138 | (def shared-coercion-expectations 139 | {"s/Keyword" [s/Keyword :kikka :kikka 140 | s/Keyword ::kikka ::kikka 141 | s/Keyword "kikka" :kikka 142 | s/Keyword "kikka/kikka" :kikka/kikka 143 | s/Keyword 'kikka ::fails] 144 | #?@(:clj ["Keyword" [Keyword :kikka :kikka 145 | Keyword ::kikka ::kikka 146 | Keyword "kikka" :kikka 147 | Keyword "kikka/kikka" :kikka/kikka 148 | Keyword 'kikka ::fails]]) 149 | 150 | "s/Uuid" [s/Uuid "5f60751d-9bf7-4344-97ee-48643c9949ce" (stc/string->uuid "5f60751d-9bf7-4344-97ee-48643c9949ce") 151 | s/Uuid (keyword "5f60751d-9bf7-4344-97ee-48643c9949ce") (stc/string->uuid "5f60751d-9bf7-4344-97ee-48643c9949ce") 152 | s/Uuid #uuid "5f60751d-9bf7-4344-97ee-48643c9949ce" (stc/string->uuid "5f60751d-9bf7-4344-97ee-48643c9949ce") 153 | s/Uuid "INVALID" ::fails] 154 | 155 | "s/Bool" [s/Bool :true true 156 | s/Bool :false false 157 | s/Bool :invalid ::fails 158 | s/Bool "invalid" ::fails] 159 | 160 | "s/Str" [s/Str :text "text" 161 | s/Str :retain.ns/please "retain.ns/please"] 162 | 163 | "s/Int" [s/Int 1 1 164 | s/Int :1 1 165 | s/Int :1.0 1 166 | s/Int 92233720368547758071 92233720368547758071 167 | s/Int -92233720368547758071 -92233720368547758071 168 | s/Int "1.1" ::fails] 169 | 170 | "s/Num" [s/Num 1 1 171 | s/Num 1.0 1.0 172 | s/Num "invalid" ::fails] 173 | 174 | #?@(:clj ["Long" [Long 1 1 175 | Long :1 1 176 | Long 9223372036854775807 9223372036854775807 177 | Long -9223372036854775807 -9223372036854775807 178 | Long "1.0" ::fails]]) 179 | 180 | #?@(:clj ["Double" [Double 1 1.0 181 | Double 1.1 1.1 182 | Double :1 1.0 183 | Double 1.7976931348623157E308 1.7976931348623157E308 184 | Double -1.7976931348623157E308 -1.7976931348623157E308]]) 185 | 186 | #?@(:clj ["Date" [Date "2014-02-18T18:25:37.456Z" (stc/string->date "2014-02-18T18:25:37.456Z") 187 | Date (keyword "2014-02-18T18:25:37Z") (stc/string->date "2014-02-18T18:25:37Z") 188 | Date "2014-02-18T18:25:37Z" (stc/string->date "2014-02-18T18:25:37Z") 189 | Date "2014-02-18T18" ::fails 190 | Date "INVALID" ::fails]]) 191 | 192 | #?@(:cljs ["js/Date" [js/Date "2014-02-18T18:25:37.456Z" (stc/string->date "2014-02-18T18:25:37.456Z") 193 | js/Date "2014-02-18T18:25:37Z" (stc/string->date "2014-02-18T18:25:37Z") 194 | js/Date (keyword "2014-02-18T18:25:37Z") (stc/string->date "2014-02-18T18:25:37Z") 195 | ;; TODO: this works differently in clj! 196 | js/Date "2014-02-18T18" (stc/string->date "2014-02-18T18") 197 | js/Date "INVALID" ::fails]]) 198 | 199 | #?@(:clj ["LocalDate" [LocalDate "2014-02-19" (LocalDate/parse "2014-02-19") 200 | LocalDate (keyword "2014-02-19") (LocalDate/parse "2014-02-19") 201 | LocalDate "INVALID" ::fails]]) 202 | 203 | #?@(:clj ["LocalTime" [LocalTime "10:23" (LocalTime/parse "10:23") 204 | LocalTime (keyword "10:23:37") (LocalTime/parse "10:23:37") 205 | LocalTime "10:23:37" (LocalTime/parse "10:23:37") 206 | LocalTime "10:23:37.456" (LocalTime/parse "10:23:37.456") 207 | LocalTime "INVALID" ::fails]]) 208 | 209 | #?@(:clj ["Instant" [Instant "2014-02-18T18:25:37.456Z" (Instant/parse "2014-02-18T18:25:37.456Z") 210 | Instant "2014-02-18T18:25:37Z" (Instant/parse "2014-02-18T18:25:37Z") 211 | Instant (keyword "2014-02-18T18:25:37Z") (Instant/parse "2014-02-18T18:25:37Z") 212 | Instant "2014-02-18T18:25" ::fails 213 | Instant "INVALID" ::fails]])}) 214 | 215 | (def json-coercion-expectations 216 | {"s/Int" [s/Int "1" ::fails] 217 | 218 | #?@(:clj ["Long" [Long "1" ::fails]]) 219 | 220 | #?@(:clj ["Double" [Double "1.0" ::fails]])}) 221 | 222 | (def string-coercion-expectations 223 | {#?@(:clj ["Long" [Long "1" 1]]) 224 | 225 | #?@(:clj ["Double" [Double "1" 1.0 226 | Double "1.0" 1.0]]) 227 | 228 | "s/Int" [s/Int "1" 1 229 | s/Int "1.0" 1] 230 | 231 | "s/Num" [s/Num "1" 1 232 | s/Num "1.0" 1.0 233 | s/Num "-1.0" -1.0 234 | s/Num "+1.0" 1.0 235 | s/Num "1.0e10" 1.0e10] 236 | 237 | "s/Bool" [s/Bool "true" true 238 | s/Bool "false" false]}) 239 | 240 | (deftest json-matcher-test 241 | (doseq [[name ess] (concat shared-coercion-expectations json-coercion-expectations) 242 | :let [es (partition 3 ess)] 243 | [schema value expected] es] 244 | (testing name 245 | (let [result ((sc/coercer schema stc/json-coercion-matcher) value)] 246 | (if (= ::fails expected) 247 | (is (= true (boolean (su/error-val result)))) 248 | (is (= expected result)))))) 249 | 250 | #?(:clj 251 | (testing "Pattern" 252 | (is (instance? Pattern ((stc/coercer Pattern stc/json-coercion-matcher) ".*"))) 253 | (is (instance? Pattern ((stc/coercer Pattern stc/json-coercion-matcher) (keyword ".*"))))))) 254 | 255 | (deftest string-matcher-test 256 | (doseq [[name ess] (concat shared-coercion-expectations string-coercion-expectations) 257 | :let [es (partition 3 ess)] 258 | [schema value expected] es] 259 | (testing name 260 | (let [result ((sc/coercer schema stc/string-coercion-matcher) value)] 261 | (if (= ::fails expected) 262 | (is (= true (boolean (su/error-val result))) (pr-str value result)) 263 | (is (= expected result)))))) 264 | 265 | #?(:clj 266 | (testing "Pattern" 267 | (is (instance? Pattern ((stc/coercer Pattern stc/json-coercion-matcher) ".*"))) 268 | (is (instance? Pattern ((stc/coercer Pattern stc/json-coercion-matcher) (keyword ".*"))))))) 269 | -------------------------------------------------------------------------------- /test/cljc/schema_tools/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.core-test 2 | (:require #?(:clj [clojure.test :refer [deftest testing is]] 3 | :cljs [cljs.test :as test :refer-macros [deftest testing is]]) 4 | [schema-tools.core :as st] 5 | [schema.core :as s :include-macros true] 6 | [schema.coerce :as sc] 7 | [schema-tools.coerce :as stc])) 8 | 9 | (s/defschema Kikka {:a s/Str :b s/Str}) 10 | 11 | (deftest any-keys-test 12 | (is (= {s/Any s/Any} (st/any-keys))) 13 | (testing "allows any keys" 14 | (is (= nil (s/check (st/any-keys) {"a" true, [1 2 3] true}))))) 15 | 16 | (deftest any-keyword-keys-test 17 | (is (= {s/Keyword s/Any} (st/any-keyword-keys))) 18 | (is (= {s/Keyword s/Str} (st/any-keyword-keys {s/Keyword s/Str}))) 19 | (is (= {:a s/Str, s/Keyword s/Any} (st/any-keyword-keys {:a s/Str}))) 20 | (testing "does not allow non-keyword-keys" 21 | (is (s/check (st/any-keyword-keys) {:a true, "b" true}))) 22 | (testing "allows any keyword-keys" 23 | (is (= nil (s/check (st/any-keyword-keys) {:a true, :b true})))) 24 | (testing "can be used to extend schemas" 25 | (is (= nil (s/check (st/any-keyword-keys {(s/required-key "b") s/Bool}) {:a true, "b" true}))))) 26 | 27 | (def basic-schema 28 | {:a s/Str 29 | (s/optional-key :b) s/Str 30 | (s/required-key "c") s/Str 31 | s/Keyword s/Str}) 32 | 33 | (deftest assoc-test 34 | #?(:clj (testing "odd number of arguments" 35 | (is (thrown? IllegalArgumentException 36 | (st/assoc basic-schema :b s/Int :c))))) 37 | (testing "happy case" 38 | (is (= {(s/optional-key :a) s/Int 39 | :b s/Int 40 | "c" s/Int 41 | (s/optional-key :d) s/Int 42 | s/Keyword s/Int} 43 | (st/assoc basic-schema 44 | (s/optional-key :a) s/Int 45 | :b s/Int 46 | "c" s/Int 47 | (s/optional-key :d) s/Int 48 | s/Keyword s/Int)))) 49 | (testing "make anonymous if value changed" 50 | (is (not (nil? (meta (st/assoc Kikka :a s/Str))))) 51 | (is (nil? (meta (st/assoc Kikka :c s/Str)))))) 52 | 53 | (deftest dissoc-test 54 | (testing "dissoc" 55 | (is (= {s/Keyword s/Str} (st/dissoc basic-schema :a :b "c" :d)))) 56 | (testing "make anonymous if value changed" 57 | (is (not (nil? (meta (st/dissoc Kikka :d))))) 58 | (is (nil? (meta (st/dissoc Kikka :a)))))) 59 | 60 | (deftest select-keys-test 61 | (testing "select-keys" 62 | (is (= {:a s/Str 63 | (s/optional-key :b) s/Str 64 | (s/required-key "c") s/Str} 65 | (st/select-keys basic-schema [:a :b "c" :d])))) 66 | (testing "make anonymous if value changed" 67 | (is (not (nil? (meta (st/select-keys Kikka [:a :b]))))) 68 | (is (nil? (meta (st/select-keys Kikka [:a])))))) 69 | 70 | (deftest open-schema-test 71 | (let [schema {:a s/Int, :b [(s/maybe {:a s/Int, s/Keyword s/Any})]} 72 | value {:a 1, :b [{:a 1, :kikka "kukka"}], :kukka "kakka"}] 73 | (is (= {:a s/Int, :b [(s/maybe {:a s/Int, s/Keyword s/Any})], s/Keyword s/Any} 74 | (st/open-schema schema))) 75 | (is (= value ((stc/coercer (st/open-schema schema)) value))))) 76 | 77 | (deftest open-schema-does-not-kill-children-test 78 | (let [schema {:a s/Str, :b {s/Int {:c s/Str}}} 79 | value {:a "nakki", :b {1 {:c "kukka"}}}] 80 | (is (= {:a s/Str, :b {s/Int {:c s/Str, s/Keyword s/Any}} s/Keyword s/Any} 81 | (st/open-schema schema))) 82 | (is (= value ((stc/coercer (st/open-schema schema)) value))))) 83 | 84 | (def get-in-schema 85 | {:a {(s/optional-key :b) {(s/required-key :c) s/Str}} 86 | (s/optional-key "d") {s/Keyword s/Str}}) 87 | 88 | (def uniform-schema [s/Str]) 89 | (def schema-with-singles [(s/one s/Str "0") (s/optional s/Int "1") s/Keyword]) 90 | (def bounded-schema [(s/one s/Int "0") (s/optional s/Int "1")]) 91 | (def complex-schema {:a [(s/one {:b s/Int} "0") [s/Str]]}) 92 | 93 | (deftest get-in-test 94 | (is (= s/Str (st/get-in get-in-schema [:a (s/optional-key :b) (s/required-key :c)]))) 95 | (is (= s/Str (st/get-in get-in-schema [:a :b :c]))) 96 | (is (= s/Str (st/get-in get-in-schema ["d" s/Keyword]))) 97 | (is (= nil (st/get-in get-in-schema [:e]))) 98 | (testing "works with defaults" 99 | (is (= s/Str (st/get-in get-in-schema [:e] s/Str))) 100 | (is (= {:a s/Str} (st/get-in get-in-schema [:e :a] {:a s/Str})))) 101 | 102 | (testing "works with sequences" 103 | (is (= s/Str (st/get-in uniform-schema [0]))) 104 | (is (= s/Str (st/get-in uniform-schema [1000]))) 105 | (is (= s/Str (st/get-in schema-with-singles [0]))) 106 | (is (= s/Int (st/get-in schema-with-singles [1]))) 107 | (is (= s/Keyword (st/get-in schema-with-singles [2]))) 108 | (is (= s/Keyword (st/get-in schema-with-singles [1000]))) 109 | (is (= s/Int (st/get-in bounded-schema [1]))) 110 | (is (= nil (st/get-in bounded-schema [2]))) 111 | (is (= s/Str (st/get-in complex-schema [:a 1 1000]))) 112 | (is (= s/Str (st/get-in complex-schema [:a 1000 1]))) 113 | (is (= s/Int (st/get-in complex-schema [:a 0 :b])))) 114 | 115 | (testing "schema records in path are walked over as normal records" 116 | (let [schema {:a (s/maybe {:b s/Str})}] 117 | (is (= (s/maybe {:b s/Str}) (st/get-in schema [:a]))) 118 | (is (= {:b s/Str} (st/get-in schema [:a :schema]))) 119 | (is (= s/Str (st/get-in schema [:a :schema :b]))))) 120 | 121 | #_(testing "maybe" 122 | (is (= s/Str (st/schema-value (s/maybe s/Str)))) 123 | (is (= s/Str (st/get-in {:a (s/maybe {:b s/Str})} [:a :b])))) 124 | 125 | #_(testing "named" 126 | (is (= s/Str (st/schema-value (s/named s/Str 'FooBar)))) 127 | (is (= s/Str (st/get-in {:a (s/named {:b s/Str} 'FooBar)} [:a :b])))) 128 | 129 | #_(testing "constrained" 130 | (is (= s/Str (st/schema-value (s/constrained s/Str odd?)))) 131 | (is (= s/Str (st/get-in {:a (s/constrained {:b s/Str} odd?)} [:a :b])))) 132 | 133 | #_(testing "both" 134 | (is (= [{:a s/Str} {:a s/Int}] (st/schema-value (s/both {:a s/Str} {:a s/Int})))) 135 | (is (= s/Str (st/get-in {:a (s/both s/Str)} [:a 0]))) 136 | (is (= s/Int (st/get-in {:a (s/both s/Str s/Int)} [:a 1]))) 137 | (is (= s/Str (st/get-in (s/both {:a s/Str} {:a s/Int}) [0 :a]))) 138 | (is (= s/Int (st/get-in (s/both {:a s/Str} {:a s/Int}) [1 :a])))) 139 | 140 | #_(testing "either" 141 | (is (= [{:a s/Str} {:a s/Int}] (st/schema-value (s/either {:a s/Str} {:a s/Int})))) 142 | (is (= s/Str (st/get-in {:a (s/either s/Str)} [:a 0]))) 143 | (is (= s/Int (st/get-in {:a (s/either s/Str s/Int)} [:a 1]))) 144 | (is (= s/Str (st/get-in (s/either {:a s/Str} {:a s/Int}) [0 :a]))) 145 | (is (= s/Int (st/get-in (s/either {:a s/Str} {:a s/Int}) [1 :a])))) 146 | 147 | #_(testing "conditional" 148 | (is (= [{:a s/Str} {:a s/Int}] (st/schema-value (s/conditional odd? {:a s/Str} even? {:a s/Int})))) 149 | (is (= s/Str (st/get-in {:a (s/conditional odd? s/Str)} [:a 0]))) 150 | (is (= s/Int (st/get-in {:a (s/conditional odd? s/Str even? s/Int)} [:a 1]))) 151 | (is (= s/Str (st/get-in (s/conditional odd? {:a s/Str} even? {:a s/Int}) [0 :a]))) 152 | (is (= s/Int (st/get-in (s/conditional odd? {:a s/Str} even? {:a s/Int}) [1 :a])))) 153 | 154 | #_(testing "cond-pre" 155 | (is (= [{:a s/Str} {:a s/Int}] (st/schema-value (s/cond-pre {:a s/Str} {:a s/Int})))) 156 | (is (= s/Str (st/get-in {:a (s/cond-pre s/Str)} [:a 0]))) 157 | (is (= s/Int (st/get-in {:a (s/cond-pre s/Str s/Int)} [:a 1]))) 158 | (is (= s/Str (st/get-in (s/cond-pre {:a s/Str} {:a s/Int}) [0 :a]))) 159 | (is (= s/Int (st/get-in (s/cond-pre {:a s/Str} {:a s/Int}) [1 :a])))) 160 | 161 | #_(testing "enum" 162 | (is (= #{:a :b} (st/schema-value (s/enum :a :b)))))) 163 | 164 | (def assoc-in-schema 165 | {:a {(s/optional-key [1 2 3]) {(s/required-key "d") {}}}}) 166 | 167 | (deftest assoc-in-test 168 | (testing "assoc-in" 169 | (is (= {:a {(s/optional-key [1 2 3]) {(s/required-key "d") {:e {:f s/Str}}}}} 170 | (st/assoc-in assoc-in-schema [:a [1 2 3] "d" :e :f] s/Str)))) 171 | (testing "schema records in path are walked over as normal records" 172 | (let [schema {:a (s/maybe {:b s/Str})}] 173 | (is (= {:a s/Str} (st/update-in schema [:a] (constantly s/Str)))) 174 | (is (= {:a (s/maybe {:b s/Int})} (st/update-in schema [:a :schema :b] (constantly s/Int)))) 175 | (is (= {:a (s/map->Maybe {:schema {:b s/Str} :b s/Int})} (st/update-in schema [:a :b] (constantly s/Int)))))) 176 | (testing "make anonymous if value changed" 177 | (is (not (nil? (meta (st/assoc-in Kikka [:a] s/Str))))) 178 | (is (nil? (meta (st/assoc-in Kikka [:c :d] s/Str)))))) 179 | 180 | (def update-in-schema 181 | {:a {(s/optional-key [1 2 3]) {(s/required-key "d") s/Str}}}) 182 | 183 | (deftest update-in-test 184 | (testing "update-in" 185 | (is (= {:a {(s/optional-key [1 2 3]) {(s/required-key "d") s/Int}}} 186 | (st/update-in update-in-schema [:a [1 2 3] "d"] (constantly s/Int)))) 187 | (is (= {:a {:b s/Str, :c s/Int}} (st/update-in {:a {:b s/Str}} [:a :c] (constantly s/Int))))) 188 | (testing "schema records in path are walked over as normal records" 189 | (let [schema {:a (s/maybe {:b s/Str})}] 190 | (is (= {:a s/Str} (st/update-in schema [:a] (constantly s/Str)))) 191 | (is (= {:a (s/maybe {:b s/Int})} (st/update-in schema [:a :schema :b] (constantly s/Int)))) 192 | (is (= {:a (s/map->Maybe {:schema {:b s/Str} :b s/Int})} (st/update-in schema [:a :b] (constantly s/Int)))))) 193 | (testing "make anonymous if value changed" 194 | (is (not (nil? (meta (st/update-in Kikka [:a] (constantly s/Str)))))) 195 | (is (nil? (meta (st/update-in Kikka [:c :d] (constantly s/Str))))))) 196 | 197 | (def dissoc-in-schema 198 | {:a {(s/optional-key [1 2 3]) {(s/required-key "d") s/Str 199 | :kikka s/Str}}}) 200 | 201 | (def dissoc-in-schema-2 202 | (s/schema-with-name {:a {:b {:c s/Str}}} 'Kikka)) 203 | 204 | (deftest dissoc-in-test 205 | (testing "dissoc-in" 206 | (is (= {:a {(s/optional-key [1 2 3]) {:kikka s/Str}}} 207 | (st/dissoc-in dissoc-in-schema [:a [1 2 3] "d"])))) 208 | (testing "resulting empty maps are removed" 209 | (is (= {} (st/dissoc-in dissoc-in-schema [:a [1 2 3]])))) 210 | (testing "schema records in path are walked over as normal records" 211 | (let [schema {:a (s/maybe {:b s/Str})}] 212 | (is (= {} (st/dissoc-in schema [:a]))) 213 | (is (= {} (st/dissoc-in schema [:a :schema]))) 214 | (is (= schema (st/dissoc-in schema [:a :b]))))) 215 | (testing "make anonymous if value changed" 216 | (is (not (nil? (meta (st/dissoc-in dissoc-in-schema-2 [:a :b :d]))))) 217 | (is (nil? (meta (st/dissoc-in dissoc-in-schema-2 [:a :b :c])))))) 218 | 219 | (deftest update-test 220 | (testing "update" 221 | (is (= {:a 2} (st/update {:a 1} :a inc))) 222 | (is (= {(s/optional-key :a) 2} (st/update {(s/optional-key :a) 1} :a inc))) 223 | (is (= {(s/required-key :a) 2} (st/update {(s/required-key :a) 1} :a inc)))) 224 | (testing "make anonymous if value changed" 225 | (is (not (nil? (meta (st/update Kikka :a (constantly s/Str)))))) 226 | (is (nil? (meta (st/update Kikka :c (constantly s/Str))))))) 227 | 228 | (deftest merge-test 229 | (testing "is merged left to right" 230 | (is (= {(s/optional-key :a) s/Num 231 | (s/optional-key :b) s/Num 232 | (s/required-key :c) s/Str} 233 | (st/merge {:a s/Str 234 | (s/optional-key :b) s/Str 235 | (s/required-key :c) s/Str} 236 | {(s/optional-key :a) s/Num 237 | (s/optional-key :b) s/Num})))) 238 | (testing "nills" 239 | (is (= nil (st/merge nil nil))) 240 | (is (= {:a s/Str} (st/merge {:a s/Str} nil))) 241 | (is (= {:a s/Str} (st/merge nil {:a s/Str})))) 242 | (testing "non-maps can't be mapped" 243 | (is (thrown? #?(:clj AssertionError :cljs js/Error) (st/merge [s/Str] [s/Num])))) 244 | (testing "make anonymous if value changed" 245 | (is (nil? (meta (st/merge {:b s/Str} Kikka)))) 246 | (is (not (nil? (meta (st/merge Kikka {:b s/Str}))))) 247 | (is (nil? (meta (st/merge Kikka {:c s/Str})))))) 248 | 249 | (deftest default-value-test 250 | (let [schema {:a (st/default s/Str "a") (s/optional-key :b) [{:c (st/default s/Int 42)}]} 251 | coerce (sc/coercer! schema stc/default-value-matcher)] 252 | (testing "missing keys are not added" 253 | (is (thrown? #?(:clj Exception :cljs js/Error) (coerce {})))) 254 | (testing "defaults are applied" 255 | (is (= {:a "a"} (coerce {:a nil}))) 256 | (is (= {:a "a", :b [{:c 42}]} (coerce {:a nil :b [{:c nil}]})))))) 257 | 258 | (deftest default-key-test 259 | (let [schema {:a (st/default s/Str "a") 260 | (s/optional-key :b) 261 | [{:c (st/default s/Int 42)}]} 262 | coerce (sc/coercer! schema stc/default-key-matcher)] 263 | (testing "missing keys are added" 264 | (is (= {:a "a"} (coerce {}))) 265 | (is (= {:a "b"} (coerce {:a "b"}))) 266 | (is (= {:a "a" :b [{:c 42}]} (coerce {:b [{}]})))) 267 | (testing "nils are not punned" 268 | (is (thrown? #?(:clj Exception :cljs js/Error) (coerce {:a nil})))))) 269 | 270 | (deftest default-test 271 | (let [schema {:a (st/default s/Str "a"), :b (st/default s/Str "b")} 272 | coerce (sc/coercer! schema stc/default-matcher)] 273 | (is (= {:a "a" :b "b"} (coerce {:a nil}))))) 274 | 275 | (def optional-keys-schema 276 | {(s/optional-key :a) s/Str 277 | (s/required-key :b) s/Str 278 | :c s/Str 279 | (s/required-key "d") s/Str}) 280 | 281 | (def optional-keys-schema-2 282 | (s/schema-with-name {(s/optional-key :a) s/Str, :b s/Str} 'Kikka)) 283 | 284 | (deftest optional-keys-test 285 | (testing "without extra arguments makes all top-level keys optional" 286 | (is (= (keys (st/optional-keys optional-keys-schema)) 287 | [(s/optional-key :a) (s/optional-key :b) (s/optional-key :c) (s/optional-key "d")]))) 288 | (testing "invalid input" 289 | (is (thrown-with-msg? #?(:clj AssertionError :cljs js/Error) 290 | #"input should be nil or a vector of keys." 291 | (st/optional-keys optional-keys-schema :ANY)))) 292 | (testing "makes all given top-level keys are optional, ignoring missing keys" 293 | (is (= optional-keys-schema (st/optional-keys optional-keys-schema [:NON-EXISTING]))) 294 | (is (= [(s/optional-key :a) (s/optional-key :b) :c (s/optional-key "d")] 295 | (keys (st/optional-keys optional-keys-schema [:a :b "d" :NON-EXISTING]))))) 296 | (testing "make anonymous if value changed" 297 | (is (not (nil? (meta (st/optional-keys optional-keys-schema-2 []))))) 298 | (is (nil? (meta (st/optional-keys optional-keys-schema-2 [:b]))))) 299 | (testing "missing keyword key coerces" 300 | (is (= ((sc/coercer (st/optional-keys {:a s/Str}) (constantly nil)) 301 | {}) 302 | {}))) 303 | (testing "missing string key coerces" 304 | (is (= ((sc/coercer (st/optional-keys {"a" s/Str}) (constantly nil)) 305 | {}) 306 | {})))) 307 | 308 | (def required-keys-schema 309 | {(s/required-key :a) s/Str 310 | (s/optional-key :b) s/Str 311 | :c s/Str 312 | (s/optional-key "d") s/Str}) 313 | 314 | (def required-keys-schema-2 315 | (s/schema-with-name {(s/optional-key :a) s/Str, :b s/Str} 'Kikka)) 316 | 317 | (deftest required-keys-test 318 | (testing "without extra arguments makes all top-level keys required" 319 | (is (= [:a :b :c (s/required-key "d")] 320 | (keys (st/required-keys required-keys-schema))))) 321 | (testing "invalid input" 322 | (is (thrown-with-msg? #?(:clj AssertionError :cljs js/Error) 323 | #"input should be nil or a vector of keys." 324 | (st/required-keys required-keys-schema :ANY)))) 325 | (testing "makes all given top-level keys are required, ignoring missing keys" 326 | (is (= required-keys-schema (st/required-keys required-keys-schema [:NON-EXISTING]))) 327 | (is (= [:a :b :c (s/required-key "d")] 328 | (keys (st/required-keys required-keys-schema [:b [1 2 3] "d" :NON-EXISTING]))))) 329 | (testing "make anonymous if value changed" 330 | (is (not (nil? (meta (st/required-keys required-keys-schema-2 []))))) 331 | (is (nil? (meta (st/required-keys required-keys-schema-2 [:a]))))) 332 | (testing "required string key coerces" 333 | (is (= ((sc/coercer (st/required-keys {"a" s/Str}) (constantly nil)) 334 | {"a" "b"}) 335 | {"a" "b"})))) 336 | 337 | (deftest schema-description 338 | (testing "schema-with-description" 339 | (is (= {:description "It's a ping"} (meta (st/schema-with-description {:ping s/Str} "It's a ping"))))) 340 | (testing "schema-description" 341 | (is (= "It's a ping" (st/schema-description (st/schema-with-description {:ping s/Str} "It's a ping")))))) 342 | 343 | (s/defschema Omena 344 | "Omena is an apple" 345 | {:color (s/enum :green :red)}) 346 | 347 | #?(:clj 348 | (deftest resolve-schema-test 349 | (testing "defined schema can be resolved" 350 | (is (= #'Omena (st/resolve-schema Omena)))) 351 | (testing "just named schema can't be resolved" 352 | (is (= nil (st/resolve-schema (s/schema-with-name {:ping s/Str} "Ping"))))))) 353 | 354 | #?(:clj 355 | (deftest resolve-schema-description-test 356 | (testing "schema with description" 357 | (is (= "Banaani" (st/resolve-schema-description (st/schema-with-description Omena "Banaani"))))) 358 | (testing "schema with docstring" 359 | (is (= "Omena is an apple" (st/resolve-schema-description Omena)))) 360 | (testing "schema without docstring" 361 | (is (= nil (st/resolve-schema-description Kikka)))) 362 | (testing "anonymous schema" 363 | (is (= nil (st/resolve-schema-description {:ping s/Str})))))) 364 | 365 | (deftest schema-test 366 | (is (= 1 (s/validate (st/schema s/Int {}) 1))) 367 | (is (= 1 (stc/coerce "1" (st/schema s/Int {}) stc/string-coercion-matcher)))) 368 | 369 | (deftest optional-keys-schema-test 370 | (let [coercer (fn [schema matcher {:keys [open? loose?]}] 371 | (let [f (comp (if loose? st/optional-keys-schema identity) 372 | (if open? st/open-schema identity))] 373 | (sc/coercer (f schema) matcher))) 374 | schema {:a s/Int, :b [(s/maybe {:a s/Int, s/Keyword s/Any})]} 375 | schema-coercer (coercer schema (constantly nil) {:open? true, :loose? true})] 376 | 377 | (testing "coerces values correctly" 378 | (is (= {:a 1, :b [{:a 1, :kikka "kukka"}], :kukka "kakka"} 379 | (schema-coercer {:a 1, :b [{:a 1, :kikka "kukka"}], :kukka "kakka"})))) 380 | 381 | (testing "returns coerced data even if missing keys/errors" 382 | (is (= {:a 1} 383 | (schema-coercer {:a 1})))) 384 | 385 | (testing "leaves extra keys" 386 | (is (= {:a 1 :z "extra"} 387 | (schema-coercer {:a 1 388 | :z "extra"})))) 389 | 390 | (testing "coerces nested data" 391 | (is (= {:a 1, :b [{:a 1, :kikka "kukka"}], :kukka "kakka"} 392 | (schema-coercer {:a 1, :b [{:a 1, :kikka "kukka"}], :kukka "kakka"})))) 393 | 394 | (testing "leaves extra nested data" 395 | (is (= {:a 1, :b [{:a 1, :kikka :kukka 396 | :nested :keep-me-please}], :kukka "kakka"} 397 | (schema-coercer {:a 1, :b [{:a 1, :kikka :kukka 398 | :nested :keep-me-please}], :kukka "kakka"})))) 399 | 400 | (testing "leave data" 401 | (is (= {:a 1, :b [{:a 1, :kikka "kukka"} 402 | {:b "keep-me-too"}], :kukka "kakka"} 403 | (schema-coercer {:a 1, :b [{:a 1, :kikka "kukka"} 404 | {:b "keep-me-too"}], :kukka "kakka"})))))) 405 | -------------------------------------------------------------------------------- /test/cljc/schema_tools/experimental/walk_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.experimental.walk-test 2 | (:require #?(:clj [clojure.test :refer [deftest testing is]] 3 | :cljs [cljs.test :as test :refer-macros [deftest testing is]]) 4 | schema-tools.experimental.walk 5 | [schema-tools.walk :as sw] 6 | [schema.core :as s] 7 | [schema.experimental.abstract-map :as abstract-map :include-macros true])) 8 | 9 | (s/defschema Animal 10 | (abstract-map/abstract-map-schema 11 | :type 12 | {:age s/Num 13 | :vegan? s/Bool})) 14 | 15 | (abstract-map/extend-schema Cat Animal [:cat] {:fav-catnip s/Str}) 16 | 17 | (deftest abstract-map-test 18 | (let [k (atom [])] 19 | (sw/walk (fn [x] (swap! k conj x) x) identity Animal) 20 | (is (= [Cat {:age s/Num, :vegan? s/Bool}] @k))) 21 | (let [k (atom [])] 22 | (sw/walk (fn [x] (swap! k conj x) x) identity Cat) 23 | (is (= [Animal {:age s/Num, :vegan? s/Bool, :fav-catnip s/Str, :type (s/enum :cat)}] @k)))) 24 | -------------------------------------------------------------------------------- /test/cljc/schema_tools/openapi/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.openapi.core-test 2 | (:require [clojure.test :refer [deftest is testing]] 3 | [schema-tools.openapi.core :as openapi] 4 | [schema.core :as s] 5 | [schema-tools.core :as st] 6 | #?@(:cljs [goog.date.UtcDateTime 7 | goog.date.Date]))) 8 | 9 | (s/defschema Item 10 | {:field s/Str}) 11 | 12 | (s/defrecord Param [a :- s/Str]) 13 | 14 | ;; FIXME: This is broken 15 | #_ 16 | (deftest record-schema-test 17 | (testing "Test convert record to schema" 18 | (is (= (s/named {:a s/Str} "ParamRecord") (openapi/record-schema Param))))) 19 | 20 | (deftest plain-map-test 21 | (testing "Test plain-map? for plain map" 22 | (is (openapi/plain-map? {:id s/Int :title s/Str}))) 23 | 24 | (testing "Test plain-map? for record" 25 | (is (not (openapi/plain-map? Param))))) 26 | 27 | (deftest remove-empty-keys-test 28 | (testing "Test remove empty keys" 29 | (is (= {:id s/Int :title s/Str} 30 | (openapi/remove-empty-keys {:id s/Int :title s/Str :extra nil}))))) 31 | 32 | (def expectations 33 | [[s/Bool 34 | {:type "boolean"}] 35 | 36 | [s/Num 37 | {:type "number" :format "double"}] 38 | 39 | [s/Int 40 | {:type "integer" :format "int32"}] 41 | 42 | [s/Str 43 | {:type "string"}] 44 | 45 | [s/Symbol 46 | {:type "string"}] 47 | 48 | [s/Keyword 49 | {:type "string"}] 50 | 51 | [s/Inst 52 | {:type "string" :format "date-time"}] 53 | 54 | [s/Uuid 55 | {:type "string" :format "uuid"}] 56 | 57 | [s/Regex 58 | {:type "string" :format "regex"}] 59 | 60 | [#"a[6-9]" 61 | {:type "string" :pattern "a[6-9]"}] 62 | 63 | [#{s/Keyword} 64 | {:type "array" 65 | :items {:type "string"} 66 | :uniqueItems true}] 67 | 68 | [(list s/Keyword) 69 | {:type "array" 70 | :items {:type "string"}}] 71 | 72 | [[s/Keyword] 73 | {:type "array" 74 | :items {:type "string"}}] 75 | 76 | [Item 77 | {:type "object" 78 | :title "schema-tools.openapi.core-test/Item" 79 | :properties {"field" {:type "string"}} 80 | :additionalProperties false 81 | :required ["field"]}] 82 | 83 | [(st/schema {:field s/Str}) 84 | {:type "object" 85 | :properties {"field" {:type "string"}} 86 | :additionalProperties false 87 | :required ["field"]}] 88 | 89 | [(st/schema {:field s/Str} {:name "OpenAPI"}) 90 | {:type "object" 91 | :title "OpenAPI" 92 | :properties {"field" {:type "string"}} 93 | :additionalProperties false 94 | :required ["field"]}] 95 | 96 | [(st/schema {:field s/Str} {:openapi {:type "string" 97 | :format "bytes"}}) 98 | {:type "string" 99 | :format "bytes"}] 100 | 101 | [(st/schema s/Str {:openapi/default "openapi" 102 | :openapi/format "email" 103 | :swagger/default "swagger"}) 104 | {:type "string" 105 | :format "email" 106 | :default "openapi"}] 107 | 108 | [(s/maybe s/Keyword) 109 | {:oneOf [{:type "string"} 110 | {:type "null"}]}] 111 | 112 | [(s/both s/Num (s/pred even? 'even?)) 113 | {:allOf [{:type "number" :format "double"} 114 | {:type "number" :multipleOf 2}]}] 115 | 116 | [(s/named {} "Named") 117 | {:type "object" 118 | :title "Named" 119 | :additionalProperties false}] 120 | 121 | [(s/pred neg? 'neg?) 122 | {:type "number" 123 | :maximum 0 124 | :exclusiveMaximum true}] 125 | 126 | [{:string s/Str 127 | (s/required-key :req) s/Str 128 | (s/optional-key :opt) s/Str} 129 | {:type "object" 130 | :properties 131 | {"string" {:type "string"} 132 | "req" {:type "string"} 133 | "opt" {:type "string"}} 134 | :additionalProperties false 135 | :required ["string" "req"]}] 136 | 137 | [{:string s/Str 138 | s/Int s/Int} 139 | {:type "object" 140 | :properties {"string" {:type "string"}} 141 | :additionalProperties {:type "integer" :format "int32"} 142 | :required ["string"]}] 143 | 144 | ;; clj only 145 | #?(:clj 146 | [Param 147 | {:type "object" 148 | :title "ParamRecord" 149 | :properties {"a" {:type "string"}} 150 | :additionalProperties false 151 | :required ["a"]}]) 152 | 153 | #?(:clj 154 | [java.util.regex.Pattern 155 | {:type "string" :format "regex"}]) 156 | 157 | #?(:clj 158 | [java.time.Instant 159 | {:type "string" :format "date-time"}]) 160 | 161 | #?(:clj 162 | [java.time.LocalDate 163 | {:type "string" :format "date"}]) 164 | 165 | #?(:clj 166 | [java.time.LocalTime 167 | {:type "string" :format "time"}]) 168 | 169 | #?(:clj 170 | [java.io.File 171 | {:type "file"}]) 172 | 173 | ;; cljs only 174 | #?(:cljs 175 | [js/Date 176 | {:type "string" :format "date-time"}]) 177 | 178 | #?(:cljs 179 | [goog.date.Date 180 | {:type "string" :format "date"}]) 181 | 182 | #?(:cljs 183 | [goog.date.UtcDateTime 184 | {:type "string" :format "date-time"}]) 185 | ]) 186 | 187 | (deftest transform-test 188 | (doseq [[schema openapi-spec] expectations] 189 | (testing "transform" 190 | (is (= openapi-spec (openapi/transform schema nil))))) 191 | 192 | (testing "transform enum" 193 | (let [spec (openapi/transform (s/enum "s" "m" "l") nil)] 194 | (is (= "string" (:type spec))) 195 | (is (= (set ["s" "l" "m"]) (set (:enum spec))))))) 196 | 197 | (def Id s/Int) 198 | (def Name s/Str) 199 | (def Street s/Str) 200 | (s/defschema City (st/schema (s/maybe (s/enum :tre :hki)) 201 | {:openapi/description "a city"})) 202 | (s/defschema Filters [s/Str]) 203 | (s/defschema Address 204 | {:street Street 205 | :city City}) 206 | (s/defschema User 207 | {:id Id 208 | :name Name 209 | :address Address}) 210 | (def Token s/Str) 211 | 212 | (deftest expand-test 213 | (testing "::parameters" 214 | (is (= {:parameters 215 | [{:name "username" 216 | :in "path" 217 | :description "username to fetch" 218 | :required true 219 | :schema {:type "string"} 220 | :style "simple"} 221 | {:name "id" 222 | :in "path" 223 | :description "" 224 | :required true 225 | :schema {:type "integer" 226 | :format "int32"}} 227 | {:name "name" 228 | :in "query" 229 | :description "" 230 | :required true 231 | :schema {:type "string"}} 232 | {:name "city" 233 | :in "query" 234 | :description "a city" 235 | :required false 236 | :schema {:description "a city" 237 | :oneOf [{:enum [:tre :hki], :type "string"} ;from set 238 | {:type "null"}]}} 239 | {:name "street" 240 | :in "query" 241 | :description "" 242 | :required false 243 | :schema {:type "string"}} 244 | {:name "filters" 245 | :in "query" 246 | :description "" 247 | :required false 248 | :schema {:type "array" 249 | :items {:type "string"}}} 250 | {:name "id" 251 | :in "header" 252 | :description "" 253 | :required true 254 | :schema {:type "integer" 255 | :format "int32"}} 256 | {:name "name" 257 | :in "header" 258 | :description "" 259 | :required true 260 | :schema {:type "string"}} 261 | {:name "address" 262 | :in "header" 263 | :description "" 264 | :required true 265 | :schema 266 | {:type "object" 267 | :properties 268 | {"street" {:type "string"} 269 | "city" {:description "a city" 270 | :oneOf [{:enum [:tre :hki] :type "string"} 271 | {:type "null"}]}} 272 | :required ["street" "city"] 273 | :additionalProperties false 274 | :title "schema-tools.openapi.core-test/Address"}}]} 275 | (openapi/openapi-spec 276 | {:parameters 277 | [{:name "username" 278 | :in "path" 279 | :description "username to fetch" 280 | :required true 281 | :schema {:type "string"} 282 | :style "simple"}] 283 | ::openapi/parameters 284 | {:path {:id Id} 285 | :query {:name Name 286 | (s/optional-key :city) City 287 | (s/optional-key :street) Street 288 | (s/optional-key :filters) Filters} 289 | :header User}}))) 290 | 291 | (is (= {:parameters 292 | [{:name "name2" 293 | :in "query" 294 | :description "Will be the same" 295 | :required true 296 | :schema {:type "string"}} 297 | {:name "id" 298 | :in "path" 299 | :description "" 300 | :required true 301 | :schema {:type "integer" :format "int32"}} 302 | {:name "city" 303 | :in "query" 304 | :description "a city" 305 | :required true 306 | :schema {:description "a city" 307 | :oneOf [{:enum [:tre :hki] :type "string"} {:type "null"}]}} 308 | {:name "name" 309 | :in "query" 310 | :description "" 311 | :required false 312 | :schema {:type "string"}} 313 | {:name "street" 314 | :in "query" 315 | :description "" 316 | :required false 317 | :schema {:type "string"}} 318 | {:name "filters" 319 | :in "query" 320 | :description "" 321 | :required false 322 | :schema {:type "array" :items {:type "string"}}} 323 | {:name "street" 324 | :in "cookie" 325 | :description "" 326 | :required true 327 | :schema {:type "string"}} 328 | {:name "city" 329 | :in "cookie" 330 | :description "a city" 331 | :required true 332 | :schema {:description "a city" 333 | :oneOf [{:enum [:tre :hki] :type "string"} {:type "null"}]}}]} 334 | (openapi/openapi-spec 335 | {:parameters 336 | [{:name "name" 337 | :in "query" 338 | :description "Will be overridden" 339 | :required false 340 | :schema {:type "string"}} 341 | {:name "name2" 342 | :in "query" 343 | :description "Will be the same" 344 | :required true 345 | :schema {:type "string"}}] 346 | ::openapi/parameters 347 | {:path {:id Id} 348 | :query {:city City 349 | (s/optional-key :name) Name 350 | (s/optional-key :street) Street 351 | (s/optional-key :filters) Filters} 352 | :cookie Address}})))) 353 | 354 | (testing "::schemas" 355 | (is (= {:components 356 | {:schemas 357 | {:some-object 358 | {:type "object" 359 | :properties 360 | {"name" {:type "string"} 361 | "desc" {:type "string"}}} 362 | :id {:type "integer" :format "int32"} 363 | :user 364 | {:type "object" 365 | :properties 366 | {"id" {:type "integer" :format "int32"}, 367 | "name" {:type "string"} 368 | "address" {:type "object" 369 | :properties 370 | {"street" {:type "string"}, 371 | "city" {:description "a city" 372 | :oneOf [{:enum [:tre :hki] :type "string"} 373 | {:type "null"}]}} 374 | :required ["street" "city"] 375 | :additionalProperties false 376 | :title "schema-tools.openapi.core-test/Address"}} 377 | :required ["id" "name" "address"] 378 | :additionalProperties false 379 | :title "schema-tools.openapi.core-test/User"} 380 | :address 381 | {:type "object" 382 | :properties 383 | {"street" {:type "string"} 384 | "city" {:description "a city" 385 | :oneOf [{:enum [:tre :hki] :type "string"} 386 | {:type "null"}]}} 387 | :required ["street" "city"] 388 | :additionalProperties false 389 | :title "schema-tools.openapi.core-test/Address"} 390 | :some-request 391 | {:type "object" 392 | :properties 393 | {"id" {:type "integer" :format "int32"} 394 | "name" {:type "string"} 395 | "street" {:type "string"} 396 | "filters" {:type "array" :items {:type "string"}}} 397 | :required ["id" "name"] 398 | :additionalProperties false}}}} 399 | (openapi/openapi-spec 400 | {:components 401 | {:schemas 402 | {:some-object 403 | {:type "object" 404 | :properties 405 | {"name" {:type "string"} 406 | "desc" {:type "string"}}} 407 | :user 408 | {:type "string" 409 | :title "Will be overridden"}} 410 | ::openapi/schemas 411 | {:id Id 412 | :user User 413 | :address Address 414 | :some-request {:id Id 415 | :name Name 416 | (s/optional-key :street) Street 417 | (s/optional-key :filters) Filters}}}})))) 418 | 419 | (testing "::content" 420 | (is (= {:content 421 | {"text/html" 422 | {:schema 423 | {:type "string"}} 424 | "application/json" 425 | {:schema 426 | {:type "object" 427 | :properties 428 | {"id" {:type "integer" :format "int32"} 429 | "name" {:type "string"} 430 | "address" 431 | {:type "object" 432 | :properties 433 | {"street" {:type "string"} 434 | "city" 435 | {:description "a city" 436 | :oneOf [{:enum [:tre :hki] :type "string"} 437 | {:type "null"}]}} 438 | :required ["street" "city"] 439 | :additionalProperties false 440 | :title "schema-tools.openapi.core-test/Address"}} 441 | :required ["id" "name" "address"] 442 | :additionalProperties false 443 | :title "schema-tools.openapi.core-test/User"}} 444 | "application/xml" 445 | {:schema 446 | {:type "object" 447 | :properties 448 | {"street" {:type "string"} 449 | "city" 450 | {:description "a city" 451 | :oneOf [{:enum [:tre :hki] :type "string"} 452 | {:type "null"}]}} 453 | :required ["street" "city"] 454 | :additionalProperties false 455 | :title "schema-tools.openapi.core-test/Address"}} 456 | "*/*" 457 | {:schema 458 | {:type "object" 459 | :properties 460 | {"id" {:type "integer" :format "int32"} 461 | "name" {:type "string"} 462 | "street" {:type "string"} 463 | "filters" {:type "array" :items {:type "string"}}} 464 | :required ["id" "name"] 465 | :additionalProperties false}}}} 466 | (openapi/openapi-spec 467 | {:content 468 | {"text/html" 469 | {:schema 470 | {:type "string"}}} 471 | ::openapi/content 472 | {"application/json" User 473 | "application/xml" Address 474 | "*/*" {:id Id 475 | :name Name 476 | (s/optional-key :street) Street 477 | (s/optional-key :filters) Filters}}}))) 478 | 479 | (is (= {:content 480 | {"application/json" 481 | {:schema 482 | {:type "object" 483 | :properties 484 | {"id" {:type "integer" :format "int32"} 485 | "name" {:type "string"} 486 | "address" 487 | {:type "object" 488 | :properties 489 | {"street" {:type "string"} 490 | "city" 491 | {:description "a city" 492 | :oneOf [{:enum [:tre :hki] :type "string"} 493 | {:type "null"}]}} 494 | :required ["street" "city"] 495 | :additionalProperties false 496 | :title "schema-tools.openapi.core-test/Address"}} 497 | :required ["id" "name" "address"] 498 | :additionalProperties false 499 | :title "schema-tools.openapi.core-test/User" 500 | :example "Some examples here" 501 | :examples 502 | {:admin 503 | {:summary "Admin user" 504 | :description "Super user" 505 | :value {:anything :here} 506 | :externalValue "External value"}} 507 | :encoding {:contentType "application/json"}}}}} 508 | (openapi/openapi-spec 509 | {::openapi/content 510 | {"application/json" 511 | (st/schema 512 | User 513 | {:openapi/example "Some examples here" 514 | :openapi/examples {:admin 515 | {:summary "Admin user" 516 | :description "Super user" 517 | :value {:anything :here} 518 | :externalValue "External value"}} 519 | :openapi/encoding {:contentType "application/json"}})}})))) 520 | 521 | (testing "::headers" 522 | (is (= {:headers 523 | {:X-Rate-Limit-Limit 524 | {:description "The number of allowed requests in the current period", 525 | :schema {:type "integer"}}, 526 | :City 527 | {:description "a city", 528 | :required false, 529 | :schema 530 | {:enum [:tre :hki] :type "string"}} 531 | :Authorization 532 | {:description "" 533 | :required true 534 | :schema {:type "string"}} 535 | :User 536 | {:description "" 537 | :required true 538 | :schema 539 | {:type "object" 540 | :properties 541 | {"id" {:type "integer" :format "int32"} 542 | "name" {:type "string"} 543 | "address" 544 | {:type "object" 545 | :properties 546 | {"street" {:type "string"} 547 | "city" 548 | {:description "a city" 549 | :oneOf [{:enum [:tre :hki] :type "string"} 550 | {:type "null"}]}} 551 | :required ["street" "city"] 552 | :additionalProperties false 553 | :title "schema-tools.openapi.core-test/Address"}} 554 | :required ["id" "name" "address"] 555 | :additionalProperties false 556 | :title "schema-tools.openapi.core-test/User"}}}} 557 | (openapi/openapi-spec 558 | {:headers 559 | {:X-Rate-Limit-Limit 560 | {:description "The number of allowed requests in the current period" 561 | :schema {:type "integer"}}} 562 | ::openapi/headers 563 | {:City City 564 | :Authorization Token 565 | :User User}}))))) 566 | 567 | ;; TODO: This test does not really validate schema 568 | #?(:clj 569 | (deftest test-schema-validation 570 | (is (not 571 | (nil? 572 | (openapi/openapi-spec 573 | {:openapi "3.0.3" 574 | :info 575 | {:title "Sample Pet Store App" 576 | :description "This is a sample server for a pet store." 577 | :termsOfService "http://example.com/terms/" 578 | :contact 579 | {:name "API Support", 580 | :url "http://www.example.com/support" 581 | :email "support@example.com"} 582 | :license 583 | {:name "Apache 2.0", 584 | :url "https://www.apache.org/licenses/LICENSE-2.0.html"} 585 | :version "1.0.1"} 586 | :servers 587 | [{:url "https://development.gigantic-server.com/v1" 588 | :description "Development server"} 589 | {:url "https://staging.gigantic-server.com/v1" 590 | :description "Staging server"} 591 | {:url "https://api.gigantic-server.com/v1" 592 | :description "Production server"}] 593 | :components 594 | {::openapi/schemas {:user User 595 | :address Address} 596 | ::openapi/headers {:token Token}} 597 | :paths 598 | {"/api/ping" 599 | {:get 600 | {:description "Returns all pets from the system that the user has access to" 601 | :responses {200 {::openapi/content 602 | {"application/xml" User 603 | "application/json" 604 | (st/schema 605 | Address 606 | {:openapi/example "Some examples here" 607 | :openapi/examples {:admin 608 | {:summary "Admin user" 609 | :description "Super user" 610 | :value {:anything :here} 611 | :externalValue "External value"}} 612 | :openapi/encoding {:contentType "application/json"}})}}}}} 613 | "/user/:id" 614 | {:post 615 | {:tags ["user"] 616 | :description "Returns pets based on ID" 617 | :summary "Find pets by ID" 618 | :operationId "getPetsById" 619 | :requestBody {::openapi/content {"application/json" User}} 620 | :responses {200 {:description "pet response" 621 | ::openapi/content 622 | {"application/json" User}} 623 | :default {:description "error payload", 624 | ::openapi/content 625 | {"text/html" User}}} 626 | ::openapi/parameters {:path {:id Id} 627 | :header {:token Token}}}}}})))))) 628 | 629 | (deftest backport-openapi-meta-unnamespaced 630 | (is (= {:type "string" :format "password" :random-value "42"} 631 | (openapi/transform 632 | (st/schema 633 | s/Str 634 | {:openapi/type "string" 635 | :openapi/format "password" 636 | :openapi/random-value "42"}) 637 | nil)))) 638 | 639 | (deftest description-test 640 | (is (= [{:name "string" 641 | :in :query 642 | :description "xyz" 643 | :required true 644 | :schema {:type "string" :description "xyz"}}] 645 | (openapi/extract-parameter :query (st/schema s/Str {:openapi/description "xyz"})))) 646 | (is (= [{:name "string" 647 | :in :query 648 | :description "xyz" 649 | :required true 650 | :schema {:type "string" :description "xyz"}}] 651 | (openapi/extract-parameter :query (st/schema s/Str {:description "xyz"})))) 652 | (is (= [{:name "a" 653 | :in "query" 654 | :description "xyz" 655 | :required true 656 | :schema {:type "string" :description "xyz"}} 657 | {:name "b" 658 | :in "query" 659 | :description "abc" 660 | :required true 661 | :schema {:type "string" :description "abc"}}] 662 | (openapi/extract-parameter :query {:a (st/schema s/Str {:openapi/description "xyz"}) 663 | :b (st/schema s/Str {:description "abc"})})))) 664 | -------------------------------------------------------------------------------- /test/cljc/schema_tools/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns schema-tools.runner 2 | (:require [cljs.test :as test] 3 | [cljs.nodejs :as nodejs] 4 | schema-tools.core-test 5 | schema-tools.walk-test 6 | schema-tools.select-schema-test 7 | schema-tools.coerce-test 8 | schema-tools.experimental.walk-test)) 9 | 10 | (nodejs/enable-util-print!) 11 | 12 | (def status (atom nil)) 13 | 14 | (defn -main [] 15 | (test/run-all-tests #"^schema-tools.*-test$") 16 | (js/process.exit @status)) 17 | 18 | (defmethod test/report [:cljs.test/default :end-run-tests] [m] 19 | (reset! status (if (test/successful? m) 0 1))) 20 | 21 | (set! *main-cli-fn* -main) 22 | -------------------------------------------------------------------------------- /test/cljc/schema_tools/select_schema_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.select-schema-test 2 | (:require #?(:clj [clojure.test :refer [deftest testing is]] 3 | :cljs [cljs.test :as test :refer-macros [deftest testing is]]) 4 | [schema-tools.core :as st] 5 | [schema.coerce :as sc] 6 | [schema.core :as s :include-macros true] 7 | [schema.utils :as su])) 8 | 9 | (defn valid? [schema value] 10 | (nil? (s/check schema value))) 11 | 12 | (defn invalid? [schema value] 13 | (not (valid? schema value))) 14 | 15 | (deftest select-schema-test 16 | 17 | (testing "simple case" 18 | (is (= "kikka" (st/select-schema "kikka" s/Str)))) 19 | 20 | (testing "strictly defined schema, with disallowed keys" 21 | (let [schema {:a s/Str 22 | :b {(s/optional-key [1 2 3]) [{(s/required-key "d") s/Str}]}} 23 | value {:a "kikka" 24 | :b {[1 2 3] [{"d" "kukka" 25 | ":d" "kikka" 26 | :d "kukka"}]}}] 27 | 28 | (testing "value does not match schema" 29 | (is (invalid? schema value))) 30 | 31 | (testing "select-schema drops disallowed keys making value match schema" 32 | (let [selected (st/select-schema value schema)] 33 | (is (valid? schema selected)) 34 | (is (= {:a "kikka", :b {[1 2 3] [{"d" "kukka"}]}} selected)))))) 35 | 36 | (testing "loosely defined schema, with disallowed keys" 37 | (let [schema {s/Keyword s/Str 38 | :a {:b {s/Str s/Str} 39 | :c {s/Any s/Str}}} 40 | value {:kikka "kukka" 41 | :a {:b {"abba" "jabba"} 42 | :c {[1 2 3] "kakka"} 43 | :d :ILLEGAL-KEY}}] 44 | 45 | (testing "value does not match schema" 46 | (is (invalid? schema value))) 47 | 48 | (testing "select-schema drops disallowed keys making value match schema" 49 | (let [selected (st/select-schema value schema)] 50 | (is (valid? schema selected)) 51 | (is (= {:kikka "kukka", :a {:b {"abba" "jabba"}, :c {[1 2 3] "kakka"}}} selected)))))) 52 | 53 | (testing "other errors cause coercion exception" 54 | (is (thrown-with-msg? 55 | #?(:clj clojure.lang.ExceptionInfo :cljs js/Error) 56 | #"Could not coerce value to schema" 57 | (st/select-schema {:a 123} {:a s/Str})))) 58 | 59 | (testing "with coercion matcher" 60 | (let [schema {:name s/Str, :sex (s/enum :male :female)} 61 | value {:name "Linda", :age 66, :sex "female"}] 62 | 63 | (testing "select-schema fails on type mismatch" 64 | (is (thrown-with-msg? 65 | #?(:clj clojure.lang.ExceptionInfo :cljs js/Error) 66 | #"Could not coerce value to schema" 67 | (st/select-schema value schema)))) 68 | 69 | (testing "select-schema with extra coercion matcher succeeds" 70 | (let [selected (st/select-schema value schema sc/json-coercion-matcher)] 71 | (is (valid? schema selected)) 72 | (is (= {:name "Linda" :sex :female} selected)))))) 73 | 74 | (testing "with predicate keys" 75 | (let [x- (s/pred #(re-find #"x-" (name %)) ":x-.*") 76 | schema {x- s/Any 77 | :a s/Any} 78 | value {:x-abba "kikka" 79 | :y-abba "kukka" 80 | :a "kakka"}] 81 | 82 | (testing "value does not match schema" 83 | (is (invalid? schema value))) 84 | 85 | (testing "select-schema drops disallowed keys making value match schema" 86 | (let [selected (st/select-schema value schema)] 87 | (is (valid? schema selected)) 88 | (is (= {:x-abba "kikka", :a "kakka"} selected))))))) 89 | -------------------------------------------------------------------------------- /test/cljc/schema_tools/swagger/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.swagger.core-test 2 | (:require 3 | [clojure.test :refer [deftest testing is are]] 4 | [schema-tools.swagger.core :as swagger] 5 | [schema-tools.core :as st] 6 | [schema.core :as s] 7 | #?@(:cljs [goog.date.UtcDateTime 8 | goog.date.Date]))) 9 | 10 | (s/defschema Abba 11 | {:string s/Str}) 12 | 13 | (s/defrecord Kikka [a :- s/Str]) 14 | 15 | (def exceptations 16 | [[s/Bool {:type "boolean"}] 17 | [s/Num {:type "number", :format "double"}] 18 | [s/Int {:type "integer", :format "int32"}] 19 | [s/Str {:type "string"}] 20 | [s/Symbol {:type "string"}] 21 | ;; TODO: phantom generates invalid names 22 | #?(:clj 23 | [Kikka {:type "object" 24 | :title "KikkaRecord" 25 | :properties {"a" {:type "string"}} 26 | :additionalProperties false 27 | :required ["a"]}]) 28 | [s/Keyword {:type "string"}] 29 | [s/Inst {:type "string", :format "date-time"}] 30 | [s/Uuid {:type "string", :format "uuid"}] 31 | #?(:clj [java.util.regex.Pattern {:type "string", :format "regex"}]) 32 | [s/Regex {:type "string", :format "regex"}] 33 | 34 | [#"a[6-9]" {:type "string", :pattern "a[6-9]"}] 35 | [#{s/Keyword} {:type "array" 36 | :items {:type "string"} 37 | :uniqueItems true}] 38 | [(list s/Keyword) {:type "array" 39 | :items {:type "string"}}] 40 | [[s/Keyword] {:type "array" 41 | :items {:type "string"}}] 42 | [Abba {:type "object" 43 | :title "schema-tools.swagger.core-test/Abba" 44 | :properties {"string" {:type "string"}} 45 | :additionalProperties false 46 | :required ["string"]}] 47 | [(st/schema {:string s/Str}) {:type "object" 48 | :properties {"string" {:type "string"}} 49 | :additionalProperties false 50 | :required ["string"]}] 51 | [(st/schema {:string s/Str} {:name "Schema2"}) {:type "object" 52 | :title "Schema2" 53 | :properties {"string" {:type "string"}} 54 | :additionalProperties false 55 | :required ["string"]}] 56 | [(st/schema s/Str {:swagger/default "abba" 57 | :swagger/format "email"}) {:type "string" 58 | :format "email" 59 | :default "abba"}] 60 | [(st/schema {:field s/Str} {:swagger {:type "file"}}) {:type "file"}] 61 | [(s/maybe s/Keyword) {:type "string", :x-nullable true}] 62 | [(s/enum "s" "m" "l") {:type "string", :enum #{"s" "l" "m"}}] 63 | [(s/both s/Num (s/pred odd? 'odd?)) {:type "number", :format "double"}] 64 | [(s/named {} "Named") {:type "object" 65 | :title "Named" 66 | :additionalProperties false}] 67 | [(s/pred integer? 'integer?) {:type "integer", :format "int32"}] 68 | [{:string s/Str 69 | (s/required-key :req) s/Str 70 | (s/optional-key :opt) s/Str} {:type "object" 71 | :properties {"string" {:type "string"} 72 | "req" {:type "string"} 73 | "opt" {:type "string"}} 74 | :additionalProperties false 75 | :required ["string" "req"]}] 76 | [{:string s/Str 77 | s/Int s/Int} {:type "object" 78 | :properties {"string" {:type "string"}} 79 | :additionalProperties {:type "integer", :format "int32"} 80 | :required ["string"]}] 81 | 82 | ;; clj only 83 | #?(:clj [java.time.Instant {:type "string", :format "date-time"}]) 84 | #?(:clj [java.time.LocalDate {:type "string", :format "date"}]) 85 | #?(:clj [java.time.LocalTime {:type "string", :format "time"}]) 86 | #?(:clj [java.io.File {:type "file"}]) 87 | 88 | ;; cljs only 89 | #?(:cljs [js/Date {:type "string", :format "date-time"}]) 90 | #?(:cljs [goog.date.Date {:type "string", :format "date"}]) 91 | #?(:cljs [goog.date.UtcDateTime {:type "string", :format "date-time"}])]) 92 | 93 | (deftest test-expectations 94 | (doseq [[schema swagger] exceptations] 95 | (is (= swagger (swagger/transform schema nil))))) 96 | 97 | (def Id s/Str) 98 | (def Name s/Str) 99 | (def Street s/Str) 100 | (s/defschema City (s/maybe (s/enum :tre :hki))) 101 | (s/defschema Address {:street Street 102 | :city City}) 103 | (s/defschema User {:id Id 104 | :name Name 105 | :address Address}) 106 | 107 | (deftest maybe-pred-test 108 | (is (true? (swagger/maybe? City)))) 109 | 110 | (deftest expand-test 111 | 112 | (testing "non-registered are not affected" 113 | (is (= {::kikka "kukka"} 114 | (swagger/swagger-spec 115 | {::kikka "kukka"})))) 116 | 117 | (testing "::parameters" 118 | #_(println "E:" (pr-str (swagger/transform (s/enum :a :b) nil))) 119 | (is (= {:parameters [{:in "query" 120 | :name "name2" 121 | :description "this survives the merge" 122 | :type "string" 123 | :required true} 124 | {:in "query" 125 | :name "name" 126 | :description "" 127 | :type "string" 128 | :required false} 129 | {:in "query" 130 | :name "street" 131 | :description "" 132 | :type "string" 133 | :required false} 134 | {:in "query" 135 | :name "city" 136 | :description "" 137 | :type "string" 138 | :required false 139 | :enum #{:tre :hki} 140 | :allowEmptyValue true} 141 | {:in "path" 142 | :name "id" 143 | :description "" 144 | :type "string" 145 | :required true} 146 | {:in "body", 147 | :name "schema-tools.swagger.core-test/Address", 148 | :description "", 149 | :required true, 150 | :schema {:type "object", 151 | :title "schema-tools.swagger.core-test/Address", 152 | :properties {"street" {:type "string"}, 153 | "city" {:enum #{:tre :hki}, 154 | :type "string" 155 | :x-nullable true}}, 156 | :additionalProperties false, 157 | :required ["street" "city"]}}]} 158 | (swagger/swagger-spec 159 | {:parameters [{:in "query" 160 | :name "name" 161 | :description "this will be overridden" 162 | :required false} 163 | {:in "query" 164 | :name "name2" 165 | :description "this survives the merge" 166 | :type "string" 167 | :required true}] 168 | ::swagger/parameters {:query {(s/optional-key :name) Name 169 | (s/optional-key :street) Street 170 | (s/optional-key :city) City} 171 | :path {:id Id} 172 | :body Address}})))) 173 | 174 | (testing "::responses" 175 | (is (= {:responses 176 | {200 {:schema {:type "object" 177 | :title "schema-tools.swagger.core-test/User" 178 | :properties {"id" {:type "string"} 179 | "name" {:type "string"} 180 | "address" {:type "object" 181 | :title "schema-tools.swagger.core-test/Address" 182 | :properties {"street" {:type "string"} 183 | "city" {:enum #{:tre :hki} 184 | :type "string" 185 | :x-nullable true}} 186 | :additionalProperties false 187 | :required ["street" "city"]}} 188 | :additionalProperties false 189 | :required ["id" "name" "address"]} 190 | :description ""} 191 | 404 {:description "Ohnoes."} 192 | 500 {:description "fail"}}} 193 | (swagger/swagger-spec 194 | {:responses {404 {:description "fail"} 195 | 500 {:description "fail"}} 196 | ::swagger/responses {200 {:schema User} 197 | 404 {:description "Ohnoes."}}}))))) 198 | -------------------------------------------------------------------------------- /test/cljc/schema_tools/walk_test.cljc: -------------------------------------------------------------------------------- 1 | (ns schema-tools.walk-test 2 | (:refer-clojure :exclude [map-entry?]) 3 | (:require #?(:clj [clojure.test :refer [deftest testing is are]] 4 | :cljs [cljs.test :as test :refer-macros [deftest testing is are]]) 5 | [schema-tools.walk :as sw] 6 | [schema-tools.core :as st] 7 | [schema.core :as s])) 8 | 9 | (deftest walk-test 10 | (testing "identity doesn't change the schema" 11 | (let [s {:a s/Str 12 | :b s/Int 13 | :s (s/maybe s/Str)}] 14 | (is (= s (sw/walk identity identity s))))) 15 | 16 | (testing "inner is called with the MapEntries" 17 | (let [k (atom [])] 18 | (sw/walk (fn [x] 19 | (swap! k conj x) 20 | x) 21 | identity 22 | {:a s/Str :b s/Str}) 23 | (is (= [[:a s/Str] [:b s/Str]] @k)))) 24 | 25 | (testing "elements can be replaced" 26 | (is (= {:a (s/maybe s/Str) 27 | :b (s/maybe s/Str)} 28 | (sw/walk (fn [[k v]] 29 | [k (s/maybe v)]) 30 | identity 31 | {:a s/Str :b s/Str})))) 32 | 33 | (testing "Insides of schemas are walked and can be replaced" 34 | (letfn [(replace-str [s] 35 | (sw/walk (fn [x] 36 | (if (= x s/Str) 37 | s/Int 38 | (replace-str x))) 39 | identity 40 | s))] 41 | (is (= {:a (s/maybe s/Int)} 42 | (replace-str {:a (s/maybe s/Str)})))))) 43 | 44 | (defn map-entry? [x] 45 | #?(:clj (instance? clojure.lang.IMapEntry x) 46 | :cljs (satisfies? IMapEntry x))) 47 | 48 | (defn name-schemas [names schema] 49 | (sw/walk (fn [x] 50 | (if (map-entry? x) 51 | [(key x) (name-schemas (conj names (s/explicit-schema-key (key x))) (val x))] 52 | (name-schemas names x))) 53 | (fn [x] 54 | (if (map? x) 55 | (if-not (s/schema-name x) 56 | (with-meta x {:name names}) 57 | x) 58 | x)) 59 | schema)) 60 | 61 | (deftest name-schemas-test 62 | (let [named (name-schemas [:root] {:a {:b s/Str} 63 | :b {:c {:d s/Int}}})] 64 | (is (= [:root :a] (-> named :a meta :name))) 65 | (is (= [:root :b] (-> named :b meta :name))) 66 | (is (= [:root :b :c] (-> named :b :c meta :name)))) 67 | 68 | (let [named (name-schemas [:root] {:a {:b (with-meta [s/Str s/Int s/Bool] {:foo "bar"})}})] 69 | (is (= [:root :a] (-> named :a meta :name))) 70 | (is (= [s/Str s/Int s/Bool] (-> named :a :b)) 71 | "IMapEntry walk doesn't lose entries for other vectors") 72 | (is (= "bar" (-> named :a :b meta :foo)) 73 | "IMapEntry walk keeps metadata"))) 74 | 75 | (deftest leaf-schema-test 76 | (are [schema] 77 | (testing (pr-str schema) 78 | (let [fail (atom false) 79 | success (atom false)] 80 | (sw/walk (fn [x] (reset! fail true) x) (fn [x] (reset! success true) x) schema) 81 | (is (not @fail)) 82 | (is @success))) 83 | 84 | s/Any 85 | (s/eq 5) 86 | (s/isa ::parent) 87 | (s/enum :parent :child) 88 | (s/pred odd? 'odd) 89 | (s/protocol schema.core.Schema))) 90 | 91 | ; Records 92 | 93 | (defrecord Test [a b]) 94 | 95 | (deftest walk-record-test 96 | (let [named (name-schemas [:root] (Test. {:a s/Str} {:c {:d s/Int}}))] 97 | (is (= [:root :a] (-> named .-a meta :name))) 98 | (is (= [:root :b] (-> named .-b meta :name))) 99 | (is (instance? Test named)))) 100 | 101 | (deftest conditional-test 102 | (let [k (atom [])] 103 | (sw/walk (fn [x] (swap! k conj x) x) 104 | identity 105 | (s/conditional :a {:a s/Str} :b {:b s/Num} (constantly true) {:c s/Bool})) 106 | (is (= [{:a s/Str} {:b s/Num} {:c s/Bool}] @k)))) 107 | 108 | (deftest condpre-test 109 | (let [k (atom [])] 110 | (sw/walk (fn [x] (swap! k conj x) x) 111 | identity 112 | (s/cond-pre [s/Str] s/Str)) 113 | (is (= [[s/Str] s/Str] @k)))) 114 | 115 | (deftest constrained-test 116 | (let [k (atom [])] 117 | (sw/walk (fn [x] (swap! k conj x) x) 118 | identity 119 | (s/constrained s/Int even?)) 120 | (is (= [s/Int] @k)))) 121 | 122 | (defn recursive-optional-keys [m] 123 | (sw/postwalk (fn [s] 124 | ; FIXME: Should a helper fn be provided to check if value is a map schema? 125 | (if (and (map? s) (not (record? s))) 126 | (st/optional-keys s) 127 | s)) 128 | m)) 129 | 130 | (deftest recursive-optional-keys-test 131 | (is (= {(s/optional-key :a) s/Str 132 | (s/optional-key :b) {(s/optional-key :c) s/Str}} 133 | (recursive-optional-keys {:a s/Str 134 | :b {:c s/Str}}))) 135 | 136 | (is (= (s/constrained {(s/optional-key :a) s/Str 137 | (s/optional-key :b) {(s/optional-key :c) s/Str}} 138 | map?) 139 | (recursive-optional-keys (s/constrained {:a s/Str 140 | :b {:c s/Str}} 141 | map?))))) 142 | -------------------------------------------------------------------------------- /test/cljs/schema_tools/doo_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns schema-tools.doo-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | schema-tools.core-test 4 | schema-tools.walk-test 5 | schema-tools.select-schema-test 6 | schema-tools.coerce-test 7 | schema-tools.swagger.core-test 8 | schema-tools.openapi.core-test 9 | schema-tools.experimental.walk-test)) 10 | 11 | (enable-console-print!) 12 | 13 | (doo-tests 'schema-tools.core-test 14 | 'schema-tools.walk-test 15 | 'schema-tools.select-schema-test 16 | 'schema-tools.coerce-test 17 | 'schema-tools.swagger.core-test 18 | 'schema-tools.openapi.core-test 19 | 'schema-tools.experimental.walk-test) 20 | --------------------------------------------------------------------------------