├── .clj-kondo └── config.edn ├── .gitignore ├── LICENSE ├── README.md ├── deps.edn ├── pom.xml ├── src └── ws │ └── web │ └── spec.clj └── test └── ws └── web └── spec_expectations.clj /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters 2 | {:missing-docstring {:level :warning} 3 | 4 | :single-key-in {:level :warning} 5 | :unsorted-required-namespaces {:level :error}} 6 | 7 | :lint-as 8 | {expectations.clojure.test/defexpect clojure.test/deftest, 9 | expectations.clojure.test/from-each clojure.core/for, 10 | expectations.clojure.test/more-of clj-kondo.lint-as/def-catch-all,}} 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.clj-kondo/.cache 2 | /.cpcache 3 | *.jar 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-specs [![Clojars Project](http://clojars.org/worldsingles/web-specs/latest-version.svg)](http://clojars.org/worldsingles/web-specs) [![cljdoc badge](https://cljdoc.org/badge/worldsingles/web-specs?0.1.1)](https://cljdoc.org/d/worldsingles/web-specs/CURRENT) 2 | 3 | Low-level Specs for web APIs/form fields that coerce string->type and also generate strings. 4 | 5 | _[Updated 2021-04-22: coercing specs are a less than good idea and we are moving away from these, instead adopting [coax](https://github.com/exoscale/coax) for the coercions -- which it can derive from specs -- followed by standard validation or confirmation via specs.]_ 6 | 7 | ## Usage 8 | 9 | `deps.edn`: 10 | 11 | ``` clojure 12 | clj -Sdeps '{:deps {worldsingles/web-specs {:mvn/version "0.1.1"}}}' 13 | ``` 14 | 15 | Leiningen / Boot Dependency: 16 | 17 | ``` clojure 18 | [worldsingles/web-specs "0.1.1"] 19 | ``` 20 | 21 | All the specs defined here accept strings that coerce to a given type, as well as values of that type, and will generate strings that can be coerced to that type. 22 | 23 | * `:ws.web.spec/boolean` -- a spec for `Boolean` values 24 | * `:ws.web.spec/long` -- a spec for `long` values 25 | * `:ws.web.spec/opt-long` -- a spec for "optional" `long` values (`nil` and `""` are accepted, `nil` will be generated -- but not `""`) 26 | * `:ws.web.spec/pos-int` -- a spec for positive integer values (based on `pos-int?`) 27 | * `:ws.web.spec/age` -- a spec for positive integer values in the range 18..120 28 | * `:ws.web.spec/double` -- a spec for `double` values 29 | * `:ws.web.spec/opt-double` -- a spec for "optional" `double` values (`nil` and `""` are accepted, `nil` will be generated -- but not `""`) 30 | * `:ws.web.spec/date` -- a spec for date values 31 | * `:ws.web.spec/opt-date` -- a spec for "optional" date values (`nil` and `""` are accepted, `nil` will be generated -- but not `""`) 32 | * `:ws.web.spec/date-time` -- a spec for date time values 33 | * `:ws.web.spec/opt-date-time` -- a spec for "optional" date time values (`nil` and `""` are accepted, `nil` will be generated -- but not `""`) 34 | 35 | For dates, the following formats are accepted: 36 | * `yyyy/M/d` 37 | * `M/d/yyyy` 38 | * `yyyy-M-d` 39 | * `M-d-yyyy` 40 | * `M/d/yy` -- short years are considered 20xx 41 | * `M-d-yy` -- short years are considered 20xx 42 | * `EEE MMM dd HH:mm:ss zzz yyyy` -- e.g., `"Fri Sep 20 13:02:00 PDT 2019"` 43 | 44 | For date times, the format corresponds to ISO 8601: `yyyy-MM-ddTHH:mm:ss.SSSZ` 45 | -- e.g., `"2020-04-05T22:28:12.000Z"` 46 | 47 | All generated dates are strings of the form `MM/dd/yyyy`. 48 | 49 | These specs are all built on two macros that wrap low-level coercions and predicates to produce string->type coercing specs: 50 | * `param-spec`, `opt-param-spec` 51 | 52 | These accept a "coercion function", an optional "stringify" function, and a spec: 53 | * Coercion: accept a value of the target type, or a string representation of such a value and either convert it to the target type or produce `:clojure.spec.alpha/invalid` 54 | * Stringify: accept a value of the target type and produce a string representation of it (that can be coerced back to the original value) -- this defaults to `str` 55 | * Spec: a spec for the target type that will successfully generate values of that type 56 | 57 | In addition, the following "coercions" are defined that accept strings or a given type, and produce `:clojure.spec.alpha/invalid` on bad input: 58 | * `->boolean`, `->long`, `->double`, and `->date` 59 | 60 | For `->long`, `->double`, and `->date` there are utility `coerce->...` functions that accept the given type or a string that they will attempt to coerce to the given type, throwing exceptions on bad input. These are used to build the `->...` coercions but are left public in case they are useful in other contexts. 61 | 62 | Finally, there is a convenience function that accepts a comma-separated list of numbers and will coerce that to a vector of long values, or produce `:clojure.spec.alpha/invalid` on bad input: 63 | * `split->longs` 64 | 65 | ## Releases 66 | 67 | * 0.1.1 -- Jun 09, 2020 -- adds `date-time` spec and support functions (PR #2 @juan-ignacio-848) 68 | * 0.1.0 -- Sep 20, 2019 -- first public release. 69 | 70 | ## License 71 | 72 | Copyright © 2017-2019 [World Singles Networks llc](https://worldsinglesnetworks.com/). 73 | 74 | Distributed under the Eclipse Public License version 1.0. 75 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {clojure.java-time {:mvn/version "0.3.2"}} 2 | :aliases 3 | {:test 4 | {:extra-paths ["test"] 5 | :extra-deps {expectations/clojure-test {:mvn/version "1.2.1"} 6 | org.clojure/test.check {:mvn/version "1.0.0"}}} 7 | :runner 8 | {:extra-deps {com.cognitect/test-runner 9 | {:git/url "https://github.com/cognitect-labs/test-runner" 10 | :sha "f7ef16dc3b8332b0d77bc0274578ad5270fbfedd"}} 11 | :main-opts ["-m" "cognitect.test-runner" 12 | "-d" "test" 13 | "-r" ".*expectations$"]} 14 | :jar 15 | {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}} 16 | :main-opts ["-m" "hf.depstar.jar" "web-specs.jar"]} 17 | :deploy 18 | {:extra-deps {deps-deploy {:mvn/version "0.0.9"}} 19 | :main-opts ["-m" "deps-deploy.deps-deploy" "deploy" "web-specs.jar"]}}} 20 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | worldsingles 5 | web-specs 6 | 0.1.1 7 | web-specs 8 | Common utility functions and "extensions" to Clojure. 9 | https://github.com/worldsingles/web-specs 10 | 11 | 12 | Eclipse Public License 13 | http://www.eclipse.org/legal/epl-v10.html 14 | 15 | 16 | 17 | 18 | Sean Corfield 19 | 20 | 21 | 22 | scm:git:git@github.com:worldsingles/web-specs.git 23 | scm:git:git@github.com:worldsingles/web-specs.git 24 | https://github.com/worldsingles/web-specs 25 | v0.1.1 26 | 27 | 28 | 29 | org.clojure 30 | clojure 31 | 1.10.1 32 | 33 | 34 | clojure.java-time 35 | clojure.java-time 36 | 0.3.2 37 | 38 | 39 | 40 | src 41 | 42 | 43 | 44 | clojars 45 | https://repo.clojars.org/ 46 | 47 | 48 | sonatype 49 | https://oss.sonatype.org/content/repositories/snapshots/ 50 | 51 | 52 | 53 | 54 | clojars 55 | Clojars repository 56 | https://clojars.org/repo 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/ws/web/spec.clj: -------------------------------------------------------------------------------- 1 | ;; copyright (c) 2017-2019 world singles llc 2 | 3 | (ns ws.web.spec 4 | "Reusable specs and spec-related utilities, common to web apps." 5 | (:require [clojure.spec.alpha :as s] 6 | [clojure.spec.gen.alpha :as g] 7 | [clojure.string :as str] 8 | [java-time :as jt])) 9 | 10 | ;; generic specs to conform from strings 11 | 12 | (defn ->boolean 13 | "Spec predicate: conform to Boolean else invalid." 14 | [s] 15 | (if (boolean? s) 16 | s 17 | ({"true" true "false" false} (str/lower-case s) ::s/invalid))) 18 | 19 | (defn coerce->long 20 | "Given a string or number, produce a long, or throw an exception. 21 | Low level utility used by spec predicates to accept either a 22 | number or a string that can be converted to a number." 23 | [s] 24 | (if (number? s) s (Long/parseLong s))) 25 | 26 | (defn ->long 27 | "Spec predicate: conform to Long else invalid." 28 | [s] 29 | (try (coerce->long s) 30 | (catch Exception _ ::s/invalid))) 31 | 32 | (defn coerce->double 33 | "Given a string or number, produce a double, or throw an exception. 34 | Low level utility used by spec predicates to accept either a 35 | number or a string that can be converted to a number." 36 | [s] 37 | (if (number? s) s (Double/parseDouble s))) 38 | 39 | (defn ->double 40 | "Spec predicate: conform to Double else invalid." 41 | [s] 42 | (try (coerce->double s) 43 | (catch Exception _ ::s/invalid))) 44 | 45 | (defn coerce->date 46 | "Given a string or date, produce a date, or throw an exception. 47 | Low level utility used by spec predicates to accept either a 48 | date or a string that can be converted to a date." 49 | [s] 50 | (if (instance? java.util.Date s) 51 | s 52 | (jt/sql-date 53 | (some #(try 54 | (jt/local-date % s) 55 | (catch Exception _)) 56 | ["yyyy/M/d" "M/d/yyyy" 57 | "yyyy-M-d" "M-d-yyyy" 58 | "M/d/yy" "M-d-yy" 59 | "EEE MMM dd HH:mm:ss zzz yyyy"])))) 60 | 61 | (defn ->date 62 | "Spec predicate: conform to Date else invalid." 63 | [s] 64 | (try (coerce->date s) 65 | (catch Exception _ ::s/invalid))) 66 | 67 | (defn coerce->date-time 68 | "Given a string or date, produce a date-time, or throw an exception. 69 | Low level utility used by spec predicates to accept either a 70 | date or a string that can be converted to a date-time." 71 | [s] 72 | (if (instance? java.util.Date s) 73 | s 74 | (jt/java-date s))) 75 | 76 | (defn ->date-time 77 | "Spec predicate: conform to Date else invalid." 78 | [s] 79 | (try (coerce->date-time s) 80 | (catch Exception _ ::s/invalid))) 81 | 82 | (defn split->longs 83 | "Spec predicate: conform string to collection of Long. 84 | Also accepts a collection of numbers." 85 | [s] 86 | (try (mapv coerce->long (if (string? s) (str/split s #",") s)) 87 | (catch Exception _ ::s/invalid))) 88 | 89 | (defmacro param-spec 90 | "Given a coercion function and a predicate / spec, produce a 91 | spec that accepts strings that can be coerced to a value that 92 | satisfies the predicate / spec, and will also generate strings 93 | that conform to the given spec." 94 | [coerce str-or-spec & [spec]] 95 | (let [[to-str spec] (if spec [str-or-spec spec] [str str-or-spec])] 96 | `(s/with-gen (s/and (s/conformer ~coerce) ~spec) 97 | (fn [] (g/fmap ~to-str (s/gen ~spec)))))) 98 | 99 | (defmacro opt-param-spec 100 | "Given a coercion function and a predicate / spec, produce a 101 | spec that accepts strings that can be coerced to a value that 102 | satisfies the predicate / spec, and will also generate strings 103 | that conform to the given spec. 104 | An empty string is coerced to nil; a generated nil produces the 105 | empty string." 106 | [coerce str-or-spec & [spec]] 107 | (let [[to-str spec] (if spec [str-or-spec spec] [str str-or-spec])] 108 | `(s/with-gen (s/and (s/or :n nil? 109 | :e (s/and string? empty? (s/conformer seq)) 110 | :s (s/and (s/conformer ~coerce) ~spec)) 111 | (s/conformer val)) 112 | (fn [] (g/frequency [[1 (s/gen nil?)] 113 | [5 (g/fmap ~to-str (s/gen ~spec))]]))))) 114 | 115 | (s/def ::boolean (param-spec ->boolean boolean?)) 116 | 117 | (s/def ::long (param-spec ->long int?)) 118 | 119 | (s/def ::opt-long (opt-param-spec ->long int?)) 120 | 121 | (s/def ::pos-int (param-spec ->long pos-int?)) 122 | 123 | (s/def ::age (param-spec ->long (s/int-in 18 121))) 124 | 125 | (s/def ::double (param-spec ->double double?)) 126 | 127 | (s/def ::opt-double (opt-param-spec ->double double?)) 128 | 129 | (s/def ::date (param-spec ->date 130 | #(jt/format "MM/dd/yyyy" (jt/offset-date-time % 0)) 131 | inst?)) 132 | 133 | (s/def ::opt-date (opt-param-spec ->date 134 | #(jt/format "MM/dd/yyyy" (jt/offset-date-time % 0)) 135 | inst?)) 136 | 137 | (s/def ::date-time (param-spec ->date-time 138 | #(jt/format 139 | "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" 140 | (jt/offset-date-time % 0)) 141 | inst?)) 142 | 143 | (s/def ::opt-date-time (opt-param-spec ->date-time 144 | #(jt/format 145 | "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" 146 | (jt/offset-date-time % 0)) 147 | inst?)) 148 | -------------------------------------------------------------------------------- /test/ws/web/spec_expectations.clj: -------------------------------------------------------------------------------- 1 | ;; copyright (c) 2018-2020 world singles networks llc 2 | 3 | (ns ws.web.spec-expectations 4 | (:require [clojure.spec.alpha :as s] 5 | [expectations.clojure.test :refer [approximately 6 | defexpect expect expecting 7 | from-each in more-of]] 8 | [java-time :as jt] 9 | [ws.web.spec 10 | :refer [->boolean ->date ->date-time ->double ->long]])) 11 | 12 | (defexpect boolean-tests 13 | (expecting "String conversions" 14 | (expect true (->boolean "true")) 15 | (expect true (->boolean "TRUE")) 16 | (expect false (->boolean "false")) 17 | (expect false (->boolean "FALSE"))) 18 | (expecting "Passthrough" 19 | (expect true (->boolean true)) 20 | (expect false (->boolean false))) 21 | (expecting "Invalid input" 22 | (expect s/invalid? (->boolean "not a boolean")) 23 | (expect s/invalid? (->boolean :not-a-boolean)) 24 | (expect s/invalid? (s/conform :ws.web.spec/boolean "not a boolean")) 25 | (expect s/invalid? (s/conform :ws.web.spec/boolean :not-a-boolean))) 26 | (expecting "Conformance" 27 | (expect :ws.web.spec/boolean "true") 28 | (expect :ws.web.spec/boolean "TRUE") 29 | (expect :ws.web.spec/boolean "false") 30 | (expect :ws.web.spec/boolean "FALSE") 31 | (expect :ws.web.spec/boolean true) 32 | (expect :ws.web.spec/boolean false)) 33 | (expecting "Exercise" 34 | (expect (more-of [s v] 35 | string? s 36 | boolean? v 37 | true (= s (str v)) 38 | s (in #{"true" "false"})) 39 | (from-each [pair (s/exercise :ws.web.spec/boolean)] 40 | pair)))) 41 | 42 | (defexpect long-tests 43 | (expecting "String conversions" 44 | (expect -123 (->long "-123")) 45 | (expect 456 (->long "456")) 46 | (expect 0 (->long "0"))) 47 | (expecting "Passthrough" 48 | (expect -333 (->long -333)) 49 | (expect 666 (->long 666))) 50 | (expecting "Invalid input" 51 | (expect s/invalid? (->long "not a long")) 52 | (expect s/invalid? (->long :not-a-long)) 53 | (expect s/invalid? (s/conform :ws.web.spec/long "")) 54 | (expect s/invalid? (s/conform :ws.web.spec/long nil)) 55 | (expect s/invalid? (s/conform :ws.web.spec/long "not a long")) 56 | (expect s/invalid? (s/conform :ws.web.spec/long :not-a-long))) 57 | (expecting "Conformance" 58 | (expect :ws.web.spec/long "-1") 59 | (expect :ws.web.spec/long "10203040") 60 | (expect :ws.web.spec/long "0") 61 | (expect :ws.web.spec/long "-999") 62 | (expect :ws.web.spec/long -1234) 63 | (expect :ws.web.spec/long 4321)) 64 | (expecting "Exercise" 65 | (expect (more-of [s v] 66 | string? s 67 | int? v 68 | true (= s (str v))) 69 | (from-each [pair (s/exercise :ws.web.spec/long)] 70 | pair)))) 71 | 72 | (defexpect pos-int-tests 73 | (expecting "Invalid input" 74 | (expect s/invalid? (s/conform :ws.web.spec/pos-int "0")) 75 | (expect s/invalid? (s/conform :ws.web.spec/pos-int "-100")) 76 | (expect s/invalid? (s/conform :ws.web.spec/pos-int 0)) 77 | (expect s/invalid? (s/conform :ws.web.spec/pos-int -123)) 78 | (expect s/invalid? (s/conform :ws.web.spec/pos-int "not a long")) 79 | (expect s/invalid? (s/conform :ws.web.spec/pos-int :not-a-long))) 80 | (expecting "Conformance" 81 | (expect :ws.web.spec/pos-int "10203040") 82 | (expect :ws.web.spec/pos-int 4321)) 83 | (expecting "Exercise" 84 | (expect (more-of [s v] 85 | string? s 86 | int? v 87 | pos-int? v 88 | true (= s (str v))) 89 | (from-each [pair (s/exercise :ws.web.spec/pos-int)] 90 | pair)))) 91 | 92 | (defexpect opt-long-tests 93 | (expecting "Invalid input" 94 | (expect s/invalid? (s/conform :ws.web.spec/opt-long "not a long")) 95 | (expect s/invalid? (s/conform :ws.web.spec/opt-long :not-a-long))) 96 | (expecting "Conformance" 97 | (expect :ws.web.spec/opt-long "") 98 | (expect :ws.web.spec/opt-long nil) 99 | (expect :ws.web.spec/opt-long "-1") 100 | (expect :ws.web.spec/opt-long "10203040") 101 | (expect :ws.web.spec/opt-long "0") 102 | (expect :ws.web.spec/opt-long "-999") 103 | (expect :ws.web.spec/opt-long -1234) 104 | (expect :ws.web.spec/opt-long 4321)) 105 | (expecting "Exercise" 106 | (expect (more-of [s v] 107 | true (or (string? s) (nil? s)) 108 | true (or (int? v) (nil? v)) 109 | true (or (= s (str v)) 110 | (= nil s v))) 111 | (from-each [pair (s/exercise :ws.web.spec/opt-long)] 112 | pair)))) 113 | 114 | (defexpect age-tests 115 | (expecting "Invalid input" 116 | (expect s/invalid? (s/conform :ws.web.spec/age "0")) 117 | (expect s/invalid? (s/conform :ws.web.spec/age "15")) 118 | (expect s/invalid? (s/conform :ws.web.spec/age "17")) 119 | (expect s/invalid? (s/conform :ws.web.spec/age "-100")) 120 | (expect s/invalid? (s/conform :ws.web.spec/age "121")) 121 | (expect s/invalid? (s/conform :ws.web.spec/age "200")) 122 | (expect s/invalid? (s/conform :ws.web.spec/age 0)) 123 | (expect s/invalid? (s/conform :ws.web.spec/age -123)) 124 | (expect s/invalid? (s/conform :ws.web.spec/age "not a long")) 125 | (expect s/invalid? (s/conform :ws.web.spec/age :not-a-long))) 126 | (expecting "Conformance" 127 | (expect :ws.web.spec/age "18") 128 | (expect :ws.web.spec/age "55") 129 | (expect :ws.web.spec/age "120") 130 | (expect :ws.web.spec/age 32)) 131 | (expecting "Exercise" 132 | (expect (more-of [s v] 133 | string? s 134 | int? v 135 | true (= s (str v)) 136 | true (<= 18 v 120)) 137 | (from-each [pair (s/exercise :ws.web.spec/age)] 138 | pair)))) 139 | 140 | (defexpect double-tests 141 | (expecting "String conversions" 142 | (expect (approximately -123) (->double "-123")) 143 | (expect (approximately 456.78) (->double "456.78")) 144 | (expect (approximately 0) (->double "0"))) 145 | (expecting "Passthrough" 146 | (expect (approximately -333.0) (->double -333.0)) 147 | (expect (approximately 666.666) (->double 666.666)) 148 | (expect (approximately 123.0) (->double 1.23e2))) 149 | (expecting "Invalid input" 150 | (expect s/invalid? (->double "not a double")) 151 | (expect s/invalid? (->double :not-a-double)) 152 | (expect s/invalid? (s/conform :ws.web.spec/double "")) 153 | (expect s/invalid? (s/conform :ws.web.spec/double nil)) 154 | (expect s/invalid? (s/conform :ws.web.spec/double "not a double")) 155 | (expect s/invalid? (s/conform :ws.web.spec/double :not-a-double))) 156 | (expecting "Conformance" 157 | (expect :ws.web.spec/double "-1") 158 | (expect :ws.web.spec/double "10203.040") 159 | (expect :ws.web.spec/double "0") 160 | (expect :ws.web.spec/double "-99.9") 161 | (expect :ws.web.spec/double -12.34) 162 | (expect :ws.web.spec/double 4.321)) 163 | (expecting "Exercise" 164 | (expect (more-of [s v] 165 | string? s 166 | double? v 167 | true (= s (str v))) 168 | (from-each [pair (s/exercise :ws.web.spec/double)] 169 | pair))) 170 | (expecting "Optional doubles" 171 | (expect :ws.web.spec/opt-double "") 172 | (expect :ws.web.spec/opt-double nil) 173 | (expect (more-of [s v] 174 | true (or (string? s) (nil? s)) 175 | true (or (double? v) (nil? v)) 176 | true (or (= s (str v)) 177 | (= nil s v))) 178 | (from-each [pair (s/exercise :ws.web.spec/opt-double)] 179 | pair)))) 180 | 181 | (defexpect date-tests 182 | (expecting "String conversions" 183 | (expect (jt/sql-date 1962 7 7) (->date "1962/7/7")) 184 | (expect (jt/sql-date 1962 7 7) (->date "1962-7-7")) 185 | (expect (jt/sql-date 1960 11 26) (->date "11/26/1960")) 186 | (expect (jt/sql-date 1960 11 26) (->date "11-26-1960")) 187 | ;; watch out for short year -- it will always be 2000+ 188 | (expect (jt/sql-date 2060 11 26) (->date "11/26/60"))) 189 | (expecting "Passthrough" 190 | (expect (jt/sql-date 2019 9 20) (->date (jt/sql-date 2019 9 20))) 191 | (let [now (java.util.Date.)] 192 | (expect now (->date now)))) 193 | (expecting "Invalid input" 194 | (expect s/invalid? (->date "not a date")) 195 | (expect s/invalid? (->date "99/99/99")) 196 | (expect s/invalid? (->date :not-a-date)) 197 | (expect s/invalid? (s/conform :ws.web.spec/date "")) 198 | (expect s/invalid? (s/conform :ws.web.spec/date nil)) 199 | (expect s/invalid? (s/conform :ws.web.spec/date "not a long")) 200 | (expect s/invalid? (s/conform :ws.web.spec/date :not-a-long))) 201 | (expecting "Conformance" 202 | (expect :ws.web.spec/date "2019/9/20") 203 | (expect :ws.web.spec/date "7/7/62") 204 | (expect :ws.web.spec/date "Fri Sep 20 12:34:56 PDT 2019") 205 | (expect :ws.web.spec/date (java.util.Date.)) 206 | (expect :ws.web.spec/date #inst "2000-01-01")) 207 | (expecting "Exercise" 208 | (expect (more-of [s v] 209 | string? s 210 | inst? v 211 | #"[01][0-9]/[0123][0-9]/[0-9]{4}" s) 212 | (from-each [pair (s/exercise :ws.web.spec/date)] 213 | pair))) 214 | (expecting "Optional dates" 215 | (expect nil? (s/conform :ws.web.spec/opt-date "")) 216 | (expect nil? (s/conform :ws.web.spec/opt-date nil)) 217 | (expect (more-of [s v] 218 | true (or (string? s) (nil? s)) 219 | true (or (inst? v) (nil? v)) 220 | some? (or (nil? s) 221 | (re-find #"[01][0-9]/[0123][0-9]/[0-9]{4}" s))) 222 | (from-each [pair (s/exercise :ws.web.spec/opt-date)] 223 | pair)))) 224 | 225 | (defexpect date-time-tests 226 | (expecting "String conversions" 227 | (expect #inst "2020-04-05T22:28:12.000" (->date-time "2020-04-05T22:28:12.000Z")) 228 | (expect #inst "2020-12-31T23:59:59.999" (->date-time "2020-12-31T23:59:59.999Z"))) 229 | (expecting "Invalid input" 230 | (expect s/invalid? (->date-time "2020/04/05T22:28:12.000Z")) 231 | (expect s/invalid? (->date-time "2020-4-5T22:28:12.000Z")) 232 | (expect s/invalid? (->date-time "2020/04/05")) 233 | (expect s/invalid? (->date-time "99/99/99T99:99:99.999Z")) 234 | (expect s/invalid? (->date-time "22:40:30.231Z")) 235 | (expect s/invalid? (->date-time "not a date time")) 236 | (expect s/invalid? (->date-time :not-a-date-time)) 237 | (expect s/invalid? (s/conform :ws.web.spec/date-time "")) 238 | (expect s/invalid? (s/conform :ws.web.spec/date-time nil))) 239 | (expecting "Conformance" 240 | (expect :ws.web.spec/date-time "2020-01-01T00:00:00.000Z") 241 | (expect :ws.web.spec/date-time (java.util.Date.)) 242 | (expect :ws.web.spec/date-time #inst "2010-11-03T15:34:22.033Z")) 243 | (expecting "Exercise" 244 | (expect (more-of [s v] 245 | string? s 246 | inst? v 247 | #"[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0-5][0-9]:[0-5][0-9].[0-9]{3}Z" s) 248 | (from-each [pair (s/exercise :ws.web.spec/date-time)] 249 | pair))) 250 | (expecting "Optional date times" 251 | (expect nil? (s/conform :ws.web.spec/opt-date-time "")) 252 | (expect nil? (s/conform :ws.web.spec/opt-date-time nil)) 253 | (expect (more-of [s v] 254 | true (or (string? s) (nil? s)) 255 | true (or (inst? v) (nil? v)) 256 | some? (or (nil? s) 257 | (re-find #"[0-9]{4}-[01][0-9]-[0123][0-9]T[012][0-9]:[0-5][0-9]:[0-5][0-9].[0-9]{3}Z" s))) 258 | (from-each [pair (s/exercise :ws.web.spec/opt-date-time)] 259 | pair)))) 260 | --------------------------------------------------------------------------------