├── .circleci └── config.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── kaocha ├── deps.edn ├── doc ├── SUMMARY.md ├── babashka.md ├── cljdoc.edn ├── data_generation.md ├── data_parsing.md ├── data_validation.md ├── design_choices.md ├── introduction.md ├── model_anatomy.md └── model_builder.md ├── package-lock.json ├── package.json ├── pom.xml ├── src └── minimallist │ ├── core.cljc │ ├── generator.cljc │ ├── helper.cljc │ ├── minicup.cljc │ ├── minimap.cljc │ └── util.cljc ├── test └── minimallist │ ├── core_test.cljc │ ├── generator_test.cljc │ └── util_test.cljc └── tests.edn /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/clojure:tools-deps-1.10.0.442-node 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - 'clj-v1-{{ checksum "deps.edn" }}-{{ checksum "package-lock.json" }}' 11 | - 'clj-v1' 12 | - run: npm ci 13 | - run: mkdir -p test-results 14 | - run: bin/kaocha --plugin kaocha.plugin/junit-xml --junit-xml-file test-results/kaocha/results.xml 15 | - store_test_results: 16 | path: test-results 17 | - save_cache: 18 | key: 'clj-v1-{{checksum "deps.edn"}}-{{ checksum "package-lock.json" }}' 19 | paths: 20 | - ~/.m2 21 | - ~/.cljs/.aot_cache 22 | - ~/node_modules 23 | - ~/.gitlibs 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | /out 5 | profiles.clj 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .idea/ 12 | .cpcache/ 13 | .cljs_node_repl/ 14 | node_modules/ 15 | *.iml 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | 6 | Versions prior to v0.1.0 are considered experimental, their API may change. 7 | 8 | ## [Unreleased] 9 | 10 | ### Added 11 | 12 | - Added one more arity to `h/repeat`, for a fixed number of repeat where min and max are the same. 13 | 14 | ## [0.0.10] - 2020-12-05 15 | 16 | ### Added 17 | 18 | - String are now treated as first class sequences. 19 | - Added some helper functions for specifying string-based grammars. 20 | - Updated the documentation about strings, char-cat and char-set. 21 | 22 | ## [0.0.9] - 2020-11-29 23 | 24 | ### Fixed 25 | 26 | - A bug which selected the wrong context in recursive `let` nodes. 27 | - A bug on the generator of optional map entries - Thx to Andrew Foltz-Morrison for the bug report and the fix. 28 | 29 | ### Changed 30 | 31 | - `describe` on a set now returns a vector instead of a set - it makes it easier to work with 32 | because its elements are ordered. 33 | 34 | ## [0.0.8] - 2020-09-26 35 | 36 | ### Changed 37 | 38 | - `describe` on a tuple where at least one entry has a defined :key will return 39 | a map of the entries with a key instead of a vector of all the entries. 40 | - :map-of no longer work with a key model and/or a value model. It works with an entry model instead. 41 | `describe` on a :map-of returns a vector of the descriptions of its entries. 42 | 43 | ## [0.0.7] - 2020-09-20 44 | 45 | ### Fixed 46 | - A link in the documentation. 47 | - :fn with-condition in the `describe` function. 48 | Updated the minimap model to reflect that with-condition is valid for the :fn nodes. 49 | - Throw an error when a reference cannot be resolved in the model, instead of just let 50 | minimallist crash somewhere else with an unexpected `nil` model. 51 | 52 | ## [0.0.6] - 2020-08-09 53 | ## [0.0.5] - 2020-08-09 54 | 55 | ### Fixed 56 | - A Cljdoc problem. 57 | 58 | ## [0.0.4] - 2020-08-09 59 | 60 | ### Added 61 | - this changelog file. 62 | - the models `gen/fn-simple-symbol`, `gen/fn-qualified-symbol`, 63 | `gen/fn-simple-keyword` and `gen/fn-qualified-keyword`. 64 | - the `describe` function, for parsing data. 65 | 66 | ### Changed 67 | - the models `gen/fn-symbol`, `gen/fn-keyword` now generate qualified symbols and keywords, 68 | 69 | ## [0.0.3] - 2020-07-26 70 | 71 | ### Added 72 | - the setup for a nice cljdoc documentation. 73 | 74 | ## [0.0.2] - 2020-07-26 75 | 76 | ### Added 77 | - the generator function in a separate namespace. 78 | - a documentation in `doc/`. 79 | 80 | ### Changed 81 | - some functions in the helpers. 82 | 83 | ## [0.0.1] - 2020-05-13 84 | 85 | ### Added 86 | - the `valid?` function, to validate data against a model. 87 | - the helper functions, to build the models. 88 | -------------------------------------------------------------------------------- /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. 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Minimallist [![CircleCI](https://circleci.com/gh/green-coder/minimallist.svg?style=svg)](https://circleci.com/gh/green-coder/minimallist) 2 | 3 | A minimalist data driven data model library, inspired by [Clojure Spec](https://clojure.org/guides/spec) and [Malli](https://github.com/metosin/malli). 4 | 5 | [![Clojars Project](https://img.shields.io/clojars/v/minimallist.svg)](https://clojars.org/minimallist) 6 | [![cljdoc badge](https://cljdoc.org/badge/minimallist/minimallist)](https://cljdoc.org/d/minimallist/minimallist/CURRENT) 7 | [![project chat](https://img.shields.io/badge/slack-join_chat-brightgreen.svg)](https://clojurians.slack.com/archives/C012HUX1VPC) 8 | [![cljdoc badge](https://img.shields.io/clojars/dt/minimallist?color=opal)](https://clojars.org/minimallist) 9 | 10 | ## Usage 11 | 12 | ```clojure 13 | (ns your-namespace 14 | (:require [minimallist.core :refer [valid? describe]] 15 | [minimallist.helper :as h])) 16 | 17 | (def hiccup-model 18 | (h/let ['hiccup (h/alt [:node (h/in-vector (h/cat [:name (h/fn keyword?)] 19 | [:props (h/? (h/map-of (h/vector (h/fn keyword?) (h/fn any?))))] 20 | [:children (h/* (h/not-inlined (h/ref 'hiccup)))]))] 21 | [:primitive (h/alt [:nil (h/fn nil?)] 22 | [:boolean (h/fn boolean?)] 23 | [:number (h/fn number?)] 24 | [:text (h/fn string?)])])] 25 | (h/ref 'hiccup))) 26 | 27 | (valid? hiccup-model [:div {:class [:foo :bar]} 28 | [:p "Hello, world of data"]]) 29 | ;=> true 30 | 31 | (describe hiccup-model [:div {:class [:foo :bar]} 32 | [:p "Hello, world of data"]]) 33 | ;=> [:node {:name :div 34 | ; :props [[[:class [:foo :bar]]]] 35 | ; :children [[:node {:name :p 36 | ; :props [] 37 | ; :children [[:primitive [:text "Hello, world of data"]]]}]]}] 38 | ``` 39 | 40 | ## Features 41 | 42 | - validates, parses and generates data, 43 | - fully data driven, models are hash-map based created via helpers, 44 | - support recursive definitions and sequence regex, 45 | - no macro, no static registry, pure functions, 46 | - relatively simple implementation, easy to read and modify, 47 | - cross platform (`.cljc`), 48 | - `valid?` and `describe` run in [Babashka](https://github.com/borkdude/babashka) 49 | 50 | ## Non-goals (for now) 51 | 52 | - does not integrate with anything else, 53 | - does not try hard to be performant 54 | 55 | ## Documentation 56 | 57 | See the [latest documentation on cljdoc](https://cljdoc.org/d/minimallist/minimallist/CURRENT) for: 58 | - A general description of the Minimallist project. 59 | - How to use the helpers to build your models. 60 | - How to validate, parse and generate your data. 61 | 62 | ## Status 63 | 64 | This is a work in progress, the API may change in the future. 65 | 66 | More functionalities will be added later, once the API design is more stable. 67 | 68 | If you find any bug or have comments, please create an issue in GitHub. 69 | 70 | ## License 71 | 72 | The Minimallist library is developed by Vincent Cantin. 73 | It is distributed under the terms of the Eclipse Public License version 2.0. 74 | -------------------------------------------------------------------------------- /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | clojure -A:test:test-check -m kaocha.runner "$@" 3 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.10.1"}} 3 | :aliases {:test-check {:extra-deps {org.clojure/test.check {:mvn/version "1.1.0"}}} 4 | :test {:extra-paths ["test"] 5 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.0.641"} 6 | lambdaisland/kaocha-cljs {:mvn/version "0.0-71"} 7 | lambdaisland/kaocha-junit-xml {:mvn/version "0.0.76"}}} 8 | :depstar {:extra-deps {seancorfield/depstar {:mvn/version "1.0.94"}}} 9 | 10 | ; clojure -A:outdated --write 11 | :outdated {:extra-deps {olical/depot {:mvn/version "2.0.1"}} 12 | :main-opts ["-m" "depot.outdated.main"]}}} 13 | 14 | ;; Memo for deploying a new release: 15 | ;; - change the version in pom.xml 16 | ;; - update the pom.xml's dependencies: 17 | ;; clj -Spom 18 | ;; - build the jar: 19 | ;; clojure -A:depstar -m hf.depstar.jar minimallist.jar -v 20 | ;; - add a tag "v0.0.x" to the latest commit and push to repo 21 | ;; - deploy: 22 | ;; mvn deploy:deploy-file -Dfile=minimallist.jar -DpomFile=pom.xml -DrepositoryId=clojars -Durl=https://clojars.org/repo/ 23 | -------------------------------------------------------------------------------- /doc/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | ## Introduction 4 | 5 | * [Introduction](introduction.md) 6 | * [Design Choices](design_choices.md) 7 | 8 | ## Model 9 | 10 | * [Model Anatomy](model_anatomy.md) 11 | * [Model Builder](model_builder.md) 12 | 13 | ## Data 14 | 15 | * [Data Validation](data_validation.md) 16 | * [Data Parsing](data_parsing.md) 17 | * [Data Generation](data_generation.md) 18 | 19 | ## Misc 20 | 21 | * [Usage in Babashka](babashka.md) 22 | -------------------------------------------------------------------------------- /doc/babashka.md: -------------------------------------------------------------------------------- 1 | As a babashka shell script: 2 | 3 | ```clojure 4 | #!/usr/bin/env bb 5 | 6 | (require '[babashka.classpath :refer [add-classpath]] 7 | '[clojure.java.shell :refer [sh]]) 8 | 9 | (def deps '{:deps {minimallist {:git/url "https://github.com/green-coder/minimallist" 10 | :sha "b373bb18b8868526243735c760bdc67a88dd1e9a"}}}) 11 | (def cp (:out (sh "clojure" "-Spath" "-Sdeps" (str deps)))) 12 | (add-classpath cp) 13 | 14 | 15 | (require '[minimallist.core :as m]) 16 | (require '[minimallist.helper :as h]) 17 | 18 | (m/valid? (h/fn int?) 1) ;=> true 19 | (m/valid? (h/fn int?) "1") ;=> false 20 | 21 | (m/describe (h/fn int?) 1) ;=> 1 22 | (m/describe (h/fn int?) "1") ;=> :invalid 23 | 24 | ;; Does not work for now. 25 | ;(require '[clojure.test.check.generators :as tcg]) 26 | ;(require '[minimallist.generator :as mg]) 27 | ;(tcg/sample (mg/gen (h/fn int?))) 28 | ``` 29 | -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc.doc/tree [["Readme" {:file "README.md"}] 2 | ["Changes" {:file "CHANGELOG.md"}] 3 | ["Introduction" {:file "doc/introduction.md"}] 4 | ["Design Choices" {:file "doc/design_choices.md"}] 5 | ["Model Anatomy" {:file "doc/model_anatomy.md"}] 6 | ["Model Builder" {:file "doc/model_builder.md"}] 7 | ["Data Validation" {:file "doc/data_validation.md"}] 8 | ["Data Parsing" {:file "doc/data_parsing.md"}] 9 | ["Data Generation" {:file "doc/data_generation.md"}] 10 | ["Usage in Babashka" {:file "doc/babashka.md"}]]} 11 | -------------------------------------------------------------------------------- /doc/data_generation.md: -------------------------------------------------------------------------------- 1 | ## Data Generation 2 | 3 | ```clojure 4 | (require '[minimallist.generator :as mg]) 5 | (require '[clojure.test.check.generators :as tcg]) 6 | 7 | ;; Implicit generation budget, varies with test.check's generator `size`. 8 | (tcg/sample (mg/gen (h/vector-of mg/fn-string?))) 9 | ;([] 10 | ; [] 11 | ; ["25"] 12 | ; ["525" "Eq8"] 13 | ; ["u" "IaDV" "JD"] 14 | ; ["69y" "sk"] 15 | ; ["0v" "" "M8" "rHD" "6qq0"] 16 | ; ["IWd8Ma" "6yF0" "9DK" "jIK"] 17 | ; ["HB6YheP" "tr3EQMj2" "j72Kw"] 18 | ; ["29gL" "GM5Q" "5mM" "Ju2y0Xs" "ywn248pp"]) 19 | 20 | ;; With an explicit generation budget of 20. 21 | (tcg/sample (mg/gen (h/vector-of mg/fn-string?) 22 | 20)) 23 | ;(["" "" "" "" "" "" "" "" "" "" ""] 24 | ; ["6" "" "B" "H" "" "" ""] 25 | ; ["3" "lO" "MU" "9W" "M" "G1" ""] 26 | ; ["D6" "Tw1" "Wu" "" "oS" "JW1" "N"] 27 | ; ["q" "vv8" "" "3" "" "H" "" "" "" "ZfC8"] 28 | ; ["7L" "42P34" "" "g7" "K22" "43" "c5WR"] 29 | ; ["qYWis" "Pbk6" "02W" "0e2aTz" "L" "3" "130B"] 30 | ; ["S2S" "GW3" "hFZ" "0G05wI" "a" "3HFyI"] 31 | ; ["8O" "jr55e" "" "qp" "DGR" "Ma9" ""] 32 | ; ["zsH" "N0" "MY49r3v" "i3F" "J0Ih6r1" "A0y" "PQlSm" "uv" "u3U" "1EVFN9u"]) 33 | 34 | 35 | ;; Recursive models 36 | (def hiccup-model 37 | (h/let ['hiccup (h/alt [:node (h/in-vector (h/cat mg/fn-keyword? 38 | (h/? (h/map)) 39 | (h/* (h/not-inlined (h/ref 'hiccup)))))] 40 | [:primitive (h/alt mg/fn-nil? 41 | mg/fn-boolean? 42 | mg/fn-number? 43 | mg/fn-string?)])] 44 | (h/ref 'hiccup))) 45 | 46 | ;; 20 Samples using a generator with budget of 50. 47 | (-> (mg/gen hiccup-model 50) 48 | (tcg/sample 20)) 49 | ;([:! {} nil [:P]] 50 | ; [:_D {} "" false] 51 | ; [:U {} true -2.0 "" 2 0 true nil -3.0 true false] 52 | ; false 53 | ; [:!V {} "m" 4 "s" "V" "L" true [:Lh] "" false nil] 54 | ; true 55 | ; [:?- {} 1 "D" -3.5 true false "06tV" "tL" -2.0 true "r3qWdG" true] 56 | ; nil 57 | ; [:rqm {} [:K {}] [:I] [:*-+. {}] true] 58 | ; [:W {} 8 3 "0F"] 59 | ; false 60 | ; [:-_3! {} [:TY8 {} false] [:-Op]] 61 | ; [:a {} nil true 5 nil 4 nil] 62 | ; [:EK_6 false -1.36328125 nil true "953r" -0.546875 nil "etj" false true true] 63 | ; "fyaty" 64 | ; 1.0 65 | ; [:.?G!Y {} "5idQ" [:+-7.*w] 12 [:F] false nil [:ixj+63] nil 8] 66 | ; 15 67 | ; true 68 | ; false) 69 | ``` 70 | 71 | ### Cat Ipsum generator 72 | 73 | ```clojure 74 | ;; Model builder 75 | (defn ipsum-model [subjects verbs pronouns objects] 76 | (h/let ['subject (h/enum subjects) 77 | 'verb (h/enum verbs) 78 | 'pronoun (h/enum pronouns) 79 | 'object (h/enum objects) 80 | 'group (h/alt (h/cat (h/ref 'pronoun) 81 | (h/ref 'object)) 82 | (h/cat (h/ref 'group) 83 | (h/enum #{"and" "with"}) 84 | (h/ref 'group))) 85 | 'sentence (h/cat (h/ref 'subject) 86 | (h/ref 'verb) 87 | (h/ref 'group))] 88 | (h/repeat 1 10 (h/not-inlined (h/ref 'sentence))))) 89 | 90 | ;; A model from the model builder 91 | (def cat-ipsum (ipsum-model #{"I" 92 | "My human" 93 | "The baby" 94 | "Bad mouses"} 95 | #{"slept on" 96 | "ate" 97 | "played with" 98 | "jumped over" 99 | "stared at" 100 | "scratched" 101 | "broke" 102 | "stretched nearby" 103 | "was singing for" 104 | "owned"} 105 | #{"the" "a" "my" "*MY*"} 106 | #{"fish" "dry food" 107 | "colorful balls" "table" 108 | "sofa" "bed" 109 | "toy" "trash" 110 | "apartment" 111 | "toilet"})) 112 | 113 | ;; A generator from the model 114 | (def cat-ipsum-generator 115 | (tcg/fmap (fn [sentences] 116 | (->> sentences 117 | (map (fn [sentence] 118 | (-> (str/join " " sentence) 119 | (str ".")))) 120 | (str/join " "))) 121 | (mg/gen cat-ipsum 50))) 122 | 123 | ;; Generate data from the generator, and have fun 124 | (tcg/sample cat-ipsum-generator) 125 | ;("Bad mouses broke the toilet. I scratched *MY* sofa. I slept on *MY* toilet. The baby jumped over the apartment. The baby stared at my toy. Bad mouses ate *MY* bed." 126 | ; "My human was singing for a toilet. My human slept on *MY* sofa with *MY* fish. My human owned a toilet." 127 | ; "My human was singing for a toilet. The baby ate *MY* trash. I owned the toy. I broke a trash. The baby jumped over my dry food. Bad mouses jumped over my sofa. I scratched the dry food." 128 | ; "My human ate the colorful balls. The baby played with the sofa. My human played with the toy. The baby played with a fish. My human was singing for my toilet. The baby stared at my colorful balls. Bad mouses scratched a sofa." 129 | ; "The baby stared at the fish. I broke my fish. My human ate my toilet. Bad mouses slept on a trash. The baby stretched nearby the apartment. The baby slept on my fish. I scratched *MY* sofa. My human was singing for a toy. Bad mouses ate *MY* colorful balls." 130 | ; "The baby was singing for *MY* toy. Bad mouses stared at my bed. I owned the apartment. My human played with *MY* fish. Bad mouses owned my apartment. Bad mouses stretched nearby my toy. I slept on *MY* apartment. My human was singing for my dry food. Bad mouses ate a dry food." 131 | ; "I broke a fish. I slept on a apartment. The baby broke the toilet. My human owned *MY* apartment. Bad mouses ate *MY* trash and my toilet. My human was singing for *MY* bed." 132 | ; "I ate my bed. My human was singing for a toy. The baby owned the toilet. The baby slept on *MY* fish." 133 | ; "The baby owned my bed. The baby played with a toilet. Bad mouses ate *MY* colorful balls. I scratched the sofa." 134 | ; "Bad mouses stared at my fish. My human ate *MY* toilet. My human stretched nearby the toilet. Bad mouses sleeped on my fish. My human scratched a fish. I broke the dry food.") 135 | ``` 136 | 137 | -------------------------------------------------------------------------------- /doc/data_parsing.md: -------------------------------------------------------------------------------- 1 | ## Data Parsing 2 | 3 | ```clojure 4 | (require '[minimallist.core :refer [describe]]) 5 | (require '[minimallist.helper :as h]) 6 | 7 | (describe (h/fn string?) "Hello, world!") 8 | ;=> "Hello, world" 9 | 10 | (describe (h/cat (h/fn int?) 11 | (h/alt [:option1 (h/fn string?)] 12 | [:option2 (h/fn keyword?)] 13 | [:option3 (h/cat (h/fn string?) 14 | (h/fn keyword?))]) 15 | (h/fn int?)) 16 | [1 "a" :b 3]) 17 | ;=> [1 [:option3 ["a" :b]] 3] 18 | ``` 19 | 20 | A more complete documentation will come later. 21 | 22 | In the mean time, please take a look at the test files 23 | for more examples. 24 | -------------------------------------------------------------------------------- /doc/data_validation.md: -------------------------------------------------------------------------------- 1 | ## Data Validation 2 | 3 | ```clojure 4 | (require '[minimallist.core :refer [valid?]]) 5 | (require '[minimallist.helper :as h]) 6 | 7 | (valid? (h/fn string?) "Hello, world!") 8 | ;=> true 9 | ``` 10 | -------------------------------------------------------------------------------- /doc/design_choices.md: -------------------------------------------------------------------------------- 1 | 2 | ## Making syntax someone else's choice 3 | 4 | Minimallist was designed in a minimalist way. Its implementation is using hashmaps to 5 | represent the models instead of other alternative like hiccup. 6 | The goal was to avoid having any kind of positional parsing in the implementation. 7 | 8 | The user can still use Hiccup for representing models and can convert them to hashmaps 9 | prior to using Minimallist's functions. 10 | 11 | Using hashmaps in the library avoids having to worry about making syntax choices that 12 | may break later. 13 | 14 | ## Decoupling data structure and data properties in models 15 | 16 | Clojure Spec was used as reference at the early stages of Minimallist's design. 17 | Using `s/and` and `s/or` for validating data was convenient and simple, but 18 | was problematic for generating data: 19 | In some cases, it was mixing together the structure of the data and the logical properties 20 | of those data, making it difficult for the library to know which one is which. 21 | 22 | In Minimallist, data structure and data properties have been separated, so that 23 | when we want to generate data, the *data structure* is generated while the 24 | *data properties* validate them. 25 | 26 | ## Or vs. Alt 27 | 28 | Choice is a problem. Having the choice on how to describe a choice is also a problem. 29 | 30 | In Minimallist, there is only one unique node `:alt` (for "alternative") which is used for choices. 31 | The same `:alt` node is used in both sequence and non-sequence modes. 32 | 33 | The `:or` node should only be used for describing the relations between data properties. 34 | If you use it in the same way that it works in Clojure Spec, it will work fine for validating 35 | data, but won't work well for generating data. 36 | 37 | Similarly, `:and` should be used only on relations between data properties. 38 | Minimallist was designed to minimize having to use them to represent data structure. 39 | See the examples for more information. 40 | 41 | -------------------------------------------------------------------------------- /doc/introduction.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Minimallist is a minimalist data driven data model library. 4 | 5 | It tries to address some of the usability problems of [Clojure Spec](https://clojure.org/guides/spec) 6 | while being data-driven, similarly to [Malli](https://github.com/metosin/malli) but differently. 7 | 8 | The name `Minimallist` was chosen from the words `minimalist` and `malli`, in honor to the `Malli` project. 9 | `Minimallist` is not related to `Malli`, its implementation and design choices are different. 10 | 11 | There is a [#minimallist](https://clojurians.slack.com/archives/C012HUX1VPC) channel in Clojurians Slack for discussion & help. 12 | -------------------------------------------------------------------------------- /doc/model_anatomy.md: -------------------------------------------------------------------------------- 1 | ## Node hierarchy 2 | 3 | In Minimallist, models are a node hierarchy where each node is a hashmap. 4 | 5 | Each node have a `:type` attribute to indicate what they represent. 6 | The node types currently supported are: 7 | 8 | ```clojure 9 | #{:fn :enum 10 | :and :or 11 | :set-of :map-of :map :sequence-of :sequence 12 | :alt :cat :repeat 13 | :let :ref} 14 | ``` 15 | 16 | ## Model of the models 17 | 18 | A data model is currently in progress to represent the data models. 19 | It is written using Minimallist and can be found in the source code, in the 20 | namespace [`minimallist.minimap`](../src/minimallist/minimap.cljc). 21 | 22 | You can use Minimallist and this model to verify that your models are 23 | valid: 24 | 25 | ```clojure 26 | (require '[minimallist.core :as m]) 27 | (require '[minimallist.minimap :as mm]) 28 | 29 | (m/valid? mm/minimap-model my-model) 30 | ``` 31 | 32 | ## What a model looks like 33 | 34 | Example: 35 | 36 | ```clojure 37 | {:type :map, 38 | :entries [{:key :name 39 | :model {:type :fn, :fn string?}} 40 | {:key :age 41 | :model {:type :fn, :fn int?}} 42 | {:key :gender 43 | :optional true 44 | :model {:type :enum, :values #{:female :male :other}}}]} 45 | ``` 46 | 47 | Models can be pretty verbose to write. 48 | Minimallist provides a small set of helper functions to express them 49 | in a more concise way (see the section [Model Builder](model_builder.md)). 50 | 51 | ## Local definitions 52 | 53 | A model can contain local definitions using a `:let` node, which can be referenced 54 | from an inner model specified in its `:body` attribute. 55 | 56 | Before using the `:let` node: 57 | 58 | ```clojure 59 | {:type :map, 60 | :entries [{:key :me, 61 | :model {:type :map, 62 | :entries [{:key :name, 63 | :model {:type :fn, :fn string?}} 64 | {:key :age, 65 | :model {:type :fn, :fn int?}}]}} 66 | {:key :best-friend, 67 | :model {:type :map, 68 | :entries [{:key :name, 69 | :model {:type :fn, :fn string?}} 70 | {:key :age, 71 | :model {:type :fn, :fn int?}}]}}]} 72 | ``` 73 | 74 | After using the `:let` node: 75 | 76 | ```clojure 77 | {:type :let, 78 | :bindings {'person {:type :map, 79 | :entries [{:key :name, 80 | :model {:type :fn, :fn string?}} 81 | {:key :age, 82 | :model {:type :fn, :fn int?}}]}}, 83 | :body {:type :map, 84 | :entries [{:key :me, 85 | :model {:type :ref, :key 'person}} 86 | {:key :best-friend, 87 | :model {:type :ref, :key 'person}}]}} 88 | ``` 89 | 90 | Local definition are also the key to unlock recursive data models. 91 | 92 | Example: 93 | 94 | ```clojure 95 | {:type :let, 96 | :bindings {'person {:type :map, 97 | :entries [{:key :name, 98 | :model {:type :fn, :fn string?}} 99 | {:key :friends, 100 | :model {:type :sequence-of, 101 | :elements-model {:type :ref, :key 'person}, 102 | :coll-type :vector}}]}}, 103 | :body {:type :ref, :key 'person}} 104 | ``` 105 | -------------------------------------------------------------------------------- /doc/model_builder.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | There are many ways to create Minimallist's model. 4 | 5 | The namespace `minimallist.helper` provides one way to do it via 6 | a small set of functions, hopefully a smaller way to write 7 | (and read) them: 8 | 9 | ```clojure 10 | (require '[minimallist.helper :as h]) 11 | 12 | (h/map [:me (h/map [:name (h/fn string?)] 13 | [:age (h/fn int?)])] 14 | [:best-friend (h/map [:name (h/fn string?)] 15 | [:age (h/fn int?)])]) 16 | 17 | (h/let ['person (h/map [:name (h/fn string?)] 18 | [:age (h/fn int?)])] 19 | (h/map [:me (h/ref 'person)] 20 | [:best-friend (h/ref 'person)])) 21 | 22 | (h/let ['person (h/map [:name (h/fn string?)] 23 | [:friends (h/vector-of (h/ref 'person))])] 24 | (h/ref 'person)) 25 | ``` 26 | 27 | ## Usage 28 | 29 | The helper function are very simple to use and combine. 30 | They are also documented in the source code. 31 | 32 | ### Custom predicate 33 | 34 | Useful for anything not directly supported by Minimallist. 35 | 36 | ```clojure 37 | ;; A string 38 | (h/fn string?) 39 | 40 | ;; An odd number 41 | (-> (h/fn int?) 42 | (h/with-condition (h/fn odd?))) 43 | 44 | ;; An odd number between 300 and 309 45 | (require '[clojure.test.check.generators :as tcg]) 46 | (-> (h/fn int?) 47 | (h/with-condition (h/and (h/fn #(<= 300 % 309)) 48 | (h/fn odd?))) 49 | (h/with-test-check-gen (tcg/fmap (fn [n] (+ 300 n n)) 50 | (tcg/choose 0 4)))) 51 | ``` 52 | 53 | `h/fn` is the interface between Minimallist and the rest of the world. 54 | Any integration with a 3rd party library should be based on this node. 55 | 56 | ### Fixed value 57 | 58 | This data model represents a fixed value. 59 | 60 | ```clojure 61 | (h/val 7) 62 | 63 | ;; Can be used on any Clojure value. 64 | (let [initial-inventory #{:sword :shield}] 65 | (h/val initial-inventory)) 66 | ``` 67 | 68 | ### One value out of a set 69 | 70 | Represents a value which belongs to a set of pre-defined values. 71 | 72 | ```clojure 73 | (h/enum #{:water :fire :earth :wind}) 74 | ``` 75 | 76 | ### And / Or 77 | 78 | Data model used for validation using conjunctions or disjunctions of 79 | other logical predicates. 80 | 81 | ```clojure 82 | (-> (h/fn int?) 83 | (h/with-condition (h/or (h/fn #(<= 0 % 9)) 84 | (h/val 42)))) 85 | ``` 86 | 87 | Those models are not supported by Minimallist's generator. If you want to use it 88 | outside of a `:condition-model` field, you need to add your own generator to their nodes. 89 | 90 | ```clojure 91 | ;; Minimallist's generator can use this model to generate data. 92 | (-> (h/or (h/fn #(<= 0 % 9)) 93 | (h/val 42)) 94 | (h/with-test-check-gen (tcg/one-of [(tcg/choose 0 9) 95 | (tcg/return 42)]))) 96 | ``` 97 | 98 | ### Collections `-of` 99 | 100 | `set-of`, `map-of`, `sequence-of` represent collections of items of the same model. 101 | 102 | ```clojure 103 | ;; A set of keywords. 104 | (h/set-of (h/fn keyword?)) 105 | 106 | ;; Map of id->name. 107 | (h/map-of (h/vector (h/fn int?) (h/fn string?))) 108 | 109 | ;; Sequence of numbers, either in a list or in a vector. 110 | (h/sequence-of (h/fn int?)) 111 | ``` 112 | 113 | `list-of`, `vector-of` and `string-of` are shortcuts to define at the same time a `:sequence-of` node 114 | with a `:coll-type` set to `:list`, `:vector` or `:string`. 115 | 116 | ```clojure 117 | ;; A list of numbers. 118 | (h/list-of (h/fn int?)) 119 | 120 | ;; A vector of numbers. 121 | (h/vector-of (h/fn int?)) 122 | 123 | ;; A string of chars (i.e. the only thing a string can contain) 124 | (h/string-of (h/fn char?)) 125 | ``` 126 | 127 | ### Collections with entries 128 | 129 | `map` and `tuple` represent collections where each item has its own model 130 | specified using entries. 131 | 132 | During data parsing, map entries are identified via their key, while tuple entries are either 133 | identified via their key or their index in the sequence. 134 | 135 | ```clojure 136 | (h/map [:name (h/fn string?)] 137 | [:age (h/fn int?)] 138 | [:gender {:optional true} (h/fn any?)]) 139 | 140 | (h/tuple [:first (h/fn int?)] 141 | (h/fn string?)) 142 | ``` 143 | 144 | `h/list`, `h/vector` and `h/string-tuple` are shortcuts to define at the same time a `:sequence` node 145 | (i.e. a `h/tuple`) with a `:coll-type` set to `:list`, `:vector` or `:string`. 146 | 147 | ```clojure 148 | ;; A list containing an integer followed by a string. 149 | (h/list [:first (h/fn int?)] 150 | (h/fn string?)) 151 | 152 | ;; A vector containing an integer followed by a string. 153 | (h/vector [:first (h/fn int?)] 154 | (h/fn string?)) 155 | 156 | ;; A string containing an operator char followed by a digit char. 157 | ;; Notice the convenient char-set helper. 158 | (h/string-tuple [:first (h/enum #{\* \+ \- \/})] 159 | #_(h/enum #{\0 \1 \2 \3 \4 \5 \6 \7 \8 \9}) 160 | (h/char-set "0123456789")) 161 | ``` 162 | 163 | ### Alt 164 | 165 | If your model can be either `A` or `B`, use the `:alt` node using `h/alt`. 166 | 167 | ```clojure 168 | ;; Entries will be identified using their index. 169 | (h/alt (h/fn nil?) 170 | (h/fn boolean?) 171 | (h/fn number?) 172 | (h/fn string?)) 173 | 174 | ;; Entries will be identified using their key. 175 | (h/alt [:nil (h/fn nil?)] 176 | [:boolean (h/fn boolean?)] 177 | [:number (h/fn number?)] 178 | [:string (h/fn string?)]) 179 | ``` 180 | 181 | ### Cat / Repeat 182 | 183 | `:cat` and `:repeat` nodes represent sequences of models. 184 | 185 | ```clojure 186 | ;; Sequential collection (list or vector) which contains 187 | ;; 1 to 3 booleans, followed by 188 | ;; 0 or 1 time integers, followed by 189 | ;; at least 1 string, followed by 190 | ;; any number of keywords. 191 | (h/cat (h/repeat 1 3 (h/fn boolean?)) 192 | (h/repeat 0 1 (h/fn int?)) 193 | (h/repeat 1 ##Inf (h/fn string?)) 194 | (h/repeat 0 ##Inf (h/fn keyword?))) 195 | 196 | ;; The same model, but written using shortcuts. 197 | (h/cat (h/repeat 1 3 (h/fn boolean?)) 198 | (h/? (h/fn int?)) 199 | (h/+ (h/fn string?)) 200 | (h/* (h/fn keyword?))) 201 | ``` 202 | 203 | We can specify the type of collection used to contain a sequence. 204 | 205 | ```clojure 206 | ;; A sequence of things which has to be inside a vector. 207 | (-> (h/cat (h/? (h/fn int?)) 208 | (h/fn string?)) 209 | h/in-vector) 210 | 211 | ;; Same model, but which has to be inside a list. 212 | (-> (h/cat (h/? (h/fn int?)) 213 | (h/fn string?)) 214 | h/in-list) 215 | 216 | ;; A model for chars inside a string, an interesting alternative to regex. 217 | ;; Notice the convenient char-cat and char-set helper functions. 218 | (-> (h/cat (h/char-cat "My favorite number is ") 219 | (h/char-set "123456789") 220 | (h/repeat 0 2 (h/char-set "0123456789")) 221 | (h/char-cat " and my favorite color is ") 222 | (h/alt (h/char-cat "red") 223 | (h/char-cat "green") 224 | (h/char-cat "blue")) 225 | (h/val \.)) 226 | h/in-string) 227 | ``` 228 | 229 | When a sequence is inside another sequence, it is considered inlined by default. 230 | With `h/not-inlined`, it will be contained in its own a collection (list or vector). 231 | 232 | ```clojure 233 | ;; Matches '(1 2 3 "Soleil") 234 | (h/cat (h/+ (h/fn int?)) 235 | (h/fn string?)) 236 | 237 | ;; Matches '([1 2 3] "Soleil") 238 | (h/cat (-> (h/+ (h/fn int?)) 239 | h/not-inlined) 240 | (h/fn string?)) 241 | ``` 242 | 243 | ### Let / Ref 244 | 245 | `h/let` creates a model where some local models are defined. 246 | `h/ref` refers to those local models. 247 | 248 | - Any value can be used as a key. 249 | - Inner local models are shadowing outer local models when using `h/ref`. 250 | 251 | ```clojure 252 | ;; Avoids repeating one-self 253 | (h/let ['person (h/map [:name (h/fn string?)] 254 | [:age (h/fn int?)])] 255 | (h/map [:me (h/ref 'person)] 256 | [:best-friend (h/ref 'person)])) 257 | 258 | ;; Allows definition of recursive data structures 259 | (h/let ['person (h/map [:name (h/fn string?)] 260 | [:friends (h/vector-of (h/ref 'person))])] 261 | (h/ref 'person)) 262 | ``` 263 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "isomorphic-ws": { 6 | "version": "4.0.1", 7 | "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", 8 | "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", 9 | "dev": true 10 | }, 11 | "ws": { 12 | "version": "7.2.3", 13 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz", 14 | "integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==", 15 | "dev": true 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "isomorphic-ws": "^4.0.1", 4 | "ws": "^7.0.1" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | minimallist 5 | minimallist 6 | 0.0.10 7 | Minimallist 8 | A minimalist data driven data model library, inspired by Spec and Malli. 9 | https://github.com/green-coder/minimallist 10 | 11 | https://github.com/green-coder/minimallist 12 | scm:git:git://github.com/green-coder/minimallist.git 13 | scm:git:ssh://git@github.com/green-coder/minimallist.git 14 | all-work-and-no-play 15 | 16 | 17 | 18 | Eclipse Public License v2.0 19 | http://www.eclipse.org/legal/epl-v20.html 20 | 21 | 22 | 23 | 24 | green-coder 25 | Vincent Cantin 26 | 27 | 28 | 29 | 30 | org.clojure 31 | clojure 32 | 1.10.1 33 | 34 | 35 | org.clojure 36 | test.check 37 | 1.1.0 38 | provided 39 | 40 | 41 | 42 | src 43 | test 44 | 45 | 46 | 47 | clojars 48 | https://repo.clojars.org/ 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/minimallist/core.cljc: -------------------------------------------------------------------------------- 1 | (ns minimallist.core 2 | #?(:cljs [:require-macros [minimallist.core :refer [implies]]])) 3 | 4 | (comment 5 | ;; Format which *may* be used by the end user later 6 | [:map [:x int?]] 7 | 8 | ;; Format used in the library's code 9 | {:type :map 10 | :entries [{:key :x 11 | :model {:type :fn 12 | :name int?}}]} 13 | 14 | ;; Supported node types 15 | [:fn :enum 16 | :and :or 17 | :set-of :map-of :map :sequence-of :sequence 18 | :alt :cat :repeat 19 | :let :ref]) 20 | 21 | ;; There are 2 kinds of predicates: 22 | ;; - structural (they test the existence of a structure), 23 | ;; - logical (they test properties on values and lead to booleans, they are not used to describe their structure). 24 | 25 | ;; Structural predicates can be predefined extensively: 26 | ;; [:set-of :map-of :map :sequence-of :sequence :alt :cat :repeat] 27 | 28 | ;; Logical predicates are everything else, for instance: 29 | ;; [:fn :enum :and :or] 30 | 31 | ;; Re-design of the :or and :alt : 32 | ;; - :or is only for non-structural tests, (e.g. [:or even? prime?]) 33 | ;; - :alt is used for any kind of branching, structural or logical. 34 | 35 | ;;--- 36 | 37 | (defmacro ^:no-doc implies 38 | "Logically equivalent to `(or (not condition) expression)`" 39 | [cause consequence] 40 | `(if ~cause 41 | ~consequence 42 | true)) 43 | 44 | (defn- comp-bindings [context bindings] 45 | (conj context bindings)) 46 | 47 | (defn- resolve-ref [context key] 48 | (loop [bindings-vector context] 49 | (if (seq bindings-vector) 50 | (let [bindings (peek bindings-vector)] 51 | (if (contains? bindings key) 52 | [bindings-vector (get bindings key)] 53 | (recur (pop bindings-vector)))) 54 | (throw (ex-info (str "Cannot resolve reference " key) 55 | {:context context, :key key}))))) 56 | 57 | (defn- s-sequential? [x] 58 | (or (sequential? x) 59 | (string? x))) 60 | 61 | (declare -valid?) 62 | 63 | (defn- left-overs 64 | "Returns a sequence of possible left-overs from the seq-data after matching the model with it." 65 | [context model seq-data] 66 | (if (and (#{:alt :cat :repeat :let :ref} (:type model)) 67 | (:inlined model true)) 68 | (case (:type model) 69 | :alt (mapcat (fn [entry] 70 | (left-overs context (:model entry) seq-data)) 71 | (:entries model)) 72 | :cat (reduce (fn [seqs-data entry] 73 | (mapcat (fn [seq-data] 74 | (left-overs context (:model entry) seq-data)) 75 | seqs-data)) 76 | [seq-data] 77 | (:entries model)) 78 | :repeat (->> (iterate (fn [seqs-data] 79 | (mapcat (fn [seq-data] 80 | (left-overs context (:elements-model model) seq-data)) 81 | seqs-data)) 82 | [seq-data]) 83 | (take-while seq) 84 | (take (inc (:max model))) ; inc because it includes the "match zero times" 85 | (drop (:min model)) 86 | (apply concat)) 87 | :let (left-overs (comp-bindings context (:bindings model)) (:body model) seq-data) 88 | :ref (let [[context model] (resolve-ref context (:key model))] 89 | (left-overs context model seq-data))) 90 | (if (and seq-data 91 | (-valid? context (dissoc model :inlined) (first seq-data))) 92 | [(next seq-data)] 93 | []))) 94 | 95 | (defn- -valid? [context model data] 96 | (case (:type model) 97 | :fn (and ((:fn model) data) 98 | (implies (contains? model :condition-model) 99 | (-valid? context (:condition-model model) data))) 100 | :enum (contains? (:values model) data) 101 | :and (every? (fn [entry] 102 | (-valid? context (:model entry) data)) 103 | (:entries model)) 104 | (:or :alt) (some (fn [entry] 105 | (-valid? context (:model entry) data)) 106 | (:entries model)) 107 | :set-of (and (set? data) 108 | (implies (contains? model :count-model) 109 | (-valid? context (:count-model model) (count data))) 110 | (implies (contains? model :elements-model) 111 | (every? (partial -valid? context (:elements-model model)) data)) 112 | (implies (contains? model :condition-model) 113 | (-valid? context (:condition-model model) data))) 114 | (:map-of :map) (and (map? data) 115 | (implies (contains? model :entries) 116 | (every? (fn [entry] 117 | (if (contains? data (:key entry)) 118 | (-valid? context (:model entry) (get data (:key entry))) 119 | (:optional entry))) 120 | (:entries model))) 121 | (implies (contains? model :entry-model) 122 | (every? (partial -valid? context (:entry-model model)) data)) 123 | (implies (contains? model :condition-model) 124 | (-valid? context (:condition-model model) data))) 125 | (:sequence-of :sequence) (and (s-sequential? data) 126 | ((-> (:coll-type model :any) {:any any? 127 | :list seq? 128 | :vector vector? 129 | :string string?}) data) 130 | (implies (contains? model :entries) 131 | (and (= (count (:entries model)) (count data)) 132 | (every? identity (map (fn [entry data-element] 133 | (-valid? context (:model entry) data-element)) 134 | (:entries model) 135 | data)))) 136 | (implies (contains? model :count-model) 137 | (-valid? context (:count-model model) (count data))) 138 | (implies (contains? model :elements-model) 139 | (every? (partial -valid? context (:elements-model model)) data)) 140 | (implies (contains? model :condition-model) 141 | (-valid? context (:condition-model model) data))) 142 | (:cat :repeat) (and (s-sequential? data) 143 | ((-> (:coll-type model :any) {:any any? 144 | :list seq? 145 | :vector vector? 146 | :string string?}) data) 147 | (some nil? (left-overs context (dissoc model :inlined) (seq data))) 148 | (implies (contains? model :count-model) 149 | (-valid? context (:count-model model) (count data))) 150 | (implies (contains? model :condition-model) 151 | (-valid? context (:condition-model model) data))) 152 | :let (-valid? (comp-bindings context (:bindings model)) (:body model) data) 153 | :ref (let [[context model] (resolve-ref context (:key model))] 154 | (-valid? context model data)))) 155 | 156 | (declare -describe) 157 | 158 | (defn- sequence-descriptions 159 | "Returns a sequence of possible descriptions from the seq-data matching a model." 160 | [context model seq-data] 161 | (if (and (#{:alt :cat :repeat :let :ref} (:type model)) 162 | (:inlined model true)) 163 | (case (:type model) 164 | :alt (mapcat (fn [index entry] 165 | (->> (sequence-descriptions context (:model entry) seq-data) 166 | (map (fn [seq-description] 167 | {:rest-seq (:rest-seq seq-description) 168 | :desc [(:key entry index) (:desc seq-description)]})))) 169 | (range) 170 | (:entries model)) 171 | :cat (if (every? #(not (contains? % :key)) (:entries model)) 172 | (reduce (fn [seq-descriptions entry] 173 | (mapcat (fn [acc] 174 | (->> (sequence-descriptions context (:model entry) (:rest-seq acc)) 175 | (map (fn [seq-description] 176 | {:rest-seq (:rest-seq seq-description) 177 | :desc (conj (:desc acc) (:desc seq-description))})))) 178 | seq-descriptions)) 179 | [{:rest-seq seq-data 180 | :desc []}] 181 | (:entries model)) 182 | (reduce-kv (fn [seq-descriptions index entry] 183 | (mapcat (fn [acc] 184 | (->> (sequence-descriptions context (:model entry) (:rest-seq acc)) 185 | (map (fn [seq-description] 186 | {:rest-seq (:rest-seq seq-description) 187 | :desc (assoc (:desc acc) (:key entry index) (:desc seq-description))})))) 188 | seq-descriptions)) 189 | [{:rest-seq seq-data 190 | :desc {}}] 191 | (:entries model))) 192 | :repeat (->> (iterate (fn [seq-descriptions] 193 | (mapcat (fn [acc] 194 | (->> (sequence-descriptions context (:elements-model model) (:rest-seq acc)) 195 | (map (fn [seq-description] 196 | {:rest-seq (:rest-seq seq-description) 197 | :desc (conj (:desc acc) (:desc seq-description))})))) 198 | seq-descriptions)) 199 | [{:rest-seq seq-data 200 | :desc []}]) 201 | (take-while seq) 202 | (take (inc (:max model))) ; inc because it includes the "match zero times" 203 | (drop (:min model)) 204 | (reverse) ; longest repetitions first 205 | (apply concat)) 206 | :let (sequence-descriptions (comp-bindings context (:bindings model)) (:body model) seq-data) 207 | :ref (let [[context model] (resolve-ref context (:key model))] 208 | (sequence-descriptions context model seq-data))) 209 | (if seq-data 210 | (let [description (-describe context (dissoc model :inlined) (first seq-data))] 211 | (if (:valid? description) 212 | [{:rest-seq (next seq-data) 213 | :desc (:desc description)}] 214 | [])) 215 | []))) 216 | 217 | (defn- -describe 218 | [context model data] 219 | (case (:type model) 220 | :fn {:valid? (and ((:fn model) data) 221 | (implies (contains? model :condition-model) 222 | (:valid? (-describe context (:condition-model model) data)))) 223 | :desc data} 224 | :enum {:valid? (contains? (:values model) data) 225 | :desc data} 226 | :and {:valid? (every? (fn [entry] 227 | (:valid? (-describe context (:model entry) data))) 228 | (:entries model)) 229 | :desc data} 230 | :or {:valid? (some (fn [entry] 231 | (:valid? (-describe context (:model entry) data))) 232 | (:entries model)) 233 | :desc data} 234 | :set-of (if (set? data) 235 | (let [entries (when (contains? model :elements-model) 236 | (mapv (partial -describe context (:elements-model model)) data)) 237 | valid? (and (implies (contains? model :elements-model) 238 | (every? :valid? entries)) 239 | (implies (contains? model :count-model) 240 | (:valid? (-describe context (:count-model model) (count data)))) 241 | (implies (contains? model :condition-model) 242 | (:valid? (-describe context (:condition-model model) data))))] 243 | {:valid? valid? 244 | :desc (mapv :desc entries)})) 245 | :map-of (if (map? data) 246 | (let [entries (mapv (partial -describe context (:entry-model model)) data) 247 | valid? (and (every? :valid? entries) 248 | (implies (contains? model :condition-model) 249 | (:valid? (-describe context (:condition-model model) data))))] 250 | {:valid? valid? 251 | :desc (mapv :desc entries)}) 252 | {:valid? false}) 253 | :map (if (map? data) 254 | (let [entries (into {} 255 | (keep (fn [entry] 256 | (if (contains? data (:key entry)) 257 | [(:key entry) (-describe context (:model entry) (get data (:key entry)))] 258 | (when-not (:optional entry) 259 | [(:key entry) {:missing? true}])))) 260 | (:entries model)) 261 | valid? (and (implies (contains? model :entries) 262 | (every? :valid? (vals entries))) 263 | (implies (contains? model :condition-model) 264 | (:valid? (-describe context (:condition-model model) data))))] 265 | {:valid? valid? 266 | :desc (into {} (map (fn [[k v]] [k (:desc v)])) entries)}) 267 | {:valid? false}) 268 | (:sequence-of :sequence) (if (s-sequential? data) 269 | (let [entries (into [] (cond 270 | (contains? model :elements-model) (map (fn [data-element] 271 | (-describe context (:elements-model model) data-element)) 272 | data) 273 | (contains? model :entries) (map (fn [entry data-element] 274 | (-describe context (:model entry) data-element)) 275 | (:entries model) 276 | data) 277 | :else (map (fn [x] {:desc x}) data))) 278 | valid? (and (({:any any? 279 | :list seq? 280 | :vector vector? 281 | :string string?} (:coll-type model :any)) data) 282 | (implies (contains? model :entries) 283 | (and (= (count (:entries model)) (count data)) 284 | (every? :valid? entries))) 285 | (implies (contains? model :count-model) 286 | (:valid? (-describe context (:count-model model) (count data)))) 287 | (implies (contains? model :elements-model) 288 | (every? :valid? entries)) 289 | (implies (contains? model :condition-model) 290 | (:valid? (-describe context (:condition-model model) data))))] 291 | {:valid? valid? 292 | :desc (if (and (= (:type model) :sequence) 293 | (some #(contains? % :key) (:entries model))) 294 | (->> (map (fn [model-entry described-entry] 295 | (when (contains? model-entry :key) 296 | [(:key model-entry) (:desc described-entry)])) 297 | (:entries model) 298 | entries) 299 | (filter some?) 300 | (into {})) 301 | (mapv :desc entries))}) 302 | {:valid? false}) 303 | :alt (let [[key entry] (first (into [] 304 | (comp (map-indexed (fn [index entry] 305 | [(:key entry index) 306 | (-describe context (:model entry) data)])) 307 | (filter (comp :valid? second)) 308 | (take 1)) 309 | (:entries model)))] 310 | (if (nil? entry) 311 | {:valid? false} 312 | {:valid? true 313 | :desc [key (:desc entry)]})) 314 | (:cat :repeat) (if (and (s-sequential? data) 315 | ((-> (:coll-type model :any) {:any any? 316 | :list seq? 317 | :vector vector? 318 | :string string?}) data) 319 | (implies (contains? model :count-model) 320 | (:valid? (-describe context (:count-model model) (count data))))) 321 | (let [seq-descriptions (filter (comp nil? :rest-seq) 322 | (sequence-descriptions context model (seq data)))] 323 | (if (seq seq-descriptions) 324 | {:desc (:desc (first seq-descriptions)) 325 | :valid? (implies (contains? model :condition-model) 326 | (:valid? (-describe context (:condition-model model) data)))} 327 | {:valid? false})) 328 | {:valid? false}) 329 | :let (-describe (comp-bindings context (:bindings model)) (:body model) data) 330 | :ref (let [[context model] (resolve-ref context (:key model))] 331 | (-describe context model data)))) 332 | 333 | ;; TODO: Treat the attributes independently of the type of the node in which they appear. 334 | ;; That's a kind of composition pattern a-la-unity. 335 | 336 | 337 | ;;-- 338 | 339 | ;; API 340 | 341 | (defn valid? 342 | "Return true if the data matches the model, false otherwise." 343 | [model data] 344 | (boolean (-valid? [] model data))) 345 | 346 | ;; WIP, do not use! 347 | (defn ^:no-doc explain 348 | "Returns a structure describing what parts of the data are not matching the model." 349 | [model data]) 350 | 351 | (defn describe 352 | "Returns a descriptions of the data's structure." 353 | ([model data] 354 | (describe model data {})) 355 | ([model data options] 356 | (let [description (-describe [] model data)] 357 | (if (:valid? description) 358 | (:desc description) 359 | (:invalid-result options :invalid))))) 360 | 361 | ;; WIP, do not use! 362 | (defn ^:no-doc undescribe 363 | "Returns a data which matches a description." 364 | [model description]) 365 | 366 | ;;--- 367 | 368 | (comment 369 | ;; Not in the core, not urgent. 370 | (defn visit [model travel-plan data]) 371 | 372 | ;; Not in the core, not urgent, maybe not needed. 373 | (defn transform [model transformer data])) 374 | -------------------------------------------------------------------------------- /src/minimallist/generator.cljc: -------------------------------------------------------------------------------- 1 | (ns minimallist.generator 2 | (:require [minimallist.core :as m] 3 | [minimallist.helper :as h] 4 | [minimallist.util :refer [reduce-update reduce-update-in reduce-mapv] :as util] 5 | [clojure.test.check.generators :as gen] 6 | [clojure.test.check.rose-tree :as rose] 7 | [clojure.test.check.random :as random])) 8 | 9 | ;; Helpers for generating non-structural data. 10 | ;; If you can't find what you need here, you can define your own helpers. 11 | 12 | (def ^{:doc "A model that matches and generates anything."} 13 | fn-any? (-> (h/fn any?) 14 | (h/with-test-check-gen gen/any))) 15 | 16 | (def ^{:doc "A model that matches anything and generates any scalar type."} 17 | fn-any-simple? (-> (h/fn any?) 18 | (h/with-test-check-gen gen/simple-type))) 19 | 20 | (def ^{:doc "A model that matches and generates the nil value."} 21 | fn-nil? (-> (h/fn nil?) 22 | (h/with-test-check-gen (gen/return nil)))) 23 | 24 | (def ^{:doc "A model that matches and generates booleans."} 25 | fn-boolean? (-> (h/fn boolean?) 26 | (h/with-test-check-gen (gen/elements [false true])))) 27 | 28 | (def ^{:doc "A model that matches and generates integers."} 29 | fn-int? (-> (h/fn int?) 30 | (h/with-test-check-gen gen/nat))) 31 | 32 | (def ^{:doc "A model that matches and generates doubles."} 33 | fn-double? (-> (h/fn double?) 34 | (h/with-test-check-gen gen/double))) 35 | 36 | (def ^{:doc "A model that matches any number and generates integers and doubles."} 37 | fn-number? (-> (h/fn number?) 38 | (h/with-test-check-gen (gen/one-of [gen/nat gen/double])))) 39 | 40 | (def ^{:doc "A model that matches strings and generates alphanumeric strings."} 41 | fn-string? (-> (h/fn string?) 42 | (h/with-test-check-gen gen/string-alphanumeric))) 43 | 44 | (def ^{:doc "A model that matches chars and generates an alphanumeric char."} 45 | fn-char? (-> (h/fn char?) 46 | (h/with-test-check-gen gen/char-alphanumeric))) 47 | 48 | (def ^{:doc "A model that matches and generates symbols with or without a namespace."} 49 | fn-symbol? (-> (h/fn symbol?) 50 | (h/with-test-check-gen (gen/one-of [gen/symbol gen/symbol-ns])))) 51 | 52 | (def ^{:doc "A model that matches and generates symbols without a namespace."} 53 | fn-simple-symbol? (-> (h/fn simple-symbol?) 54 | (h/with-test-check-gen gen/symbol))) 55 | 56 | (def ^{:doc "A model that matches and generates symbols with a namespace."} 57 | fn-qualified-symbol? (-> (h/fn qualified-symbol?) 58 | (h/with-test-check-gen gen/symbol-ns))) 59 | 60 | (def ^{:doc "A model that matches and generates keywords with or without a namespace."} 61 | fn-keyword? (-> (h/fn keyword?) 62 | (h/with-test-check-gen (gen/one-of [gen/keyword gen/keyword-ns])))) 63 | 64 | (def ^{:doc "A model that matches and generates keywords without a namespace."} 65 | fn-simple-keyword? (-> (h/fn simple-keyword?) 66 | (h/with-test-check-gen gen/keyword))) 67 | 68 | (def ^{:doc "A model that matches and generates keywords with a namespace."} 69 | fn-qualified-keyword? (-> (h/fn qualified-keyword?) 70 | (h/with-test-check-gen gen/keyword-ns))) 71 | 72 | 73 | 74 | (defn- find-stack-index [stack key] 75 | (loop [index (dec (count stack)) 76 | elements (rseq stack)] 77 | (when elements 78 | (let [elm (first elements)] 79 | (if (contains? (:bindings elm) key) 80 | index 81 | (recur (dec index) (next elements))))))) 82 | 83 | ;; TODO: walk on :count-model and :condition-model nodes 84 | (defn ^:no-doc postwalk [model visitor] 85 | (let [walk (fn walk [[stack walked-bindings] model path] 86 | (let [[[stack walked-bindings] model] 87 | (case (:type model) 88 | (:fn :enum) [[stack walked-bindings] model] 89 | (:set-of :sequence-of 90 | :repeat) (cond-> [[stack walked-bindings] model] 91 | (contains? model :elements-model) 92 | (reduce-update :elements-model walk (conj path :elements-model))) 93 | :map-of (-> [[stack walked-bindings] model] 94 | (reduce-update :entry-model walk (conj path :entry-model))) 95 | (:and :or 96 | :map :sequence 97 | :alt :cat) (cond-> [[stack walked-bindings] model] 98 | (contains? model :entries) 99 | (reduce-update :entries (fn [[stack walked-bindings] entries] 100 | (reduce-mapv (fn [[stack walked-bindings] [index entry]] 101 | (reduce-update [[stack walked-bindings] entry] :model 102 | walk (conj path :entries index :model))) 103 | [stack walked-bindings] 104 | (map-indexed vector entries))))) 105 | :let (let [[[stack' walked-bindings'] walked-body] (walk [(conj stack {:bindings (:bindings model) 106 | :path (conj path :bindings)}) 107 | walked-bindings] 108 | (:body model) 109 | (conj path :body))] 110 | [[(pop stack') walked-bindings'] (assoc model 111 | :bindings (:bindings (peek stack')) 112 | :body walked-body)]) 113 | :ref (let [key (:key model) 114 | index (find-stack-index stack key) 115 | binding-path (conj (get-in stack [index :path]) key)] 116 | (if (contains? walked-bindings binding-path) 117 | [[stack walked-bindings] model] 118 | (let [[[stack' walked-bindings'] walked-ref-model] (walk [(subvec stack 0 (inc index)) 119 | (conj walked-bindings binding-path)] 120 | (get-in stack [index :bindings key]) 121 | binding-path)] 122 | [[(-> stack' 123 | (assoc-in [index :bindings key] walked-ref-model) 124 | (into (subvec stack (inc index)))) walked-bindings'] model]))))] 125 | [[stack walked-bindings] (visitor model stack path)]))] 126 | (second (walk [[] #{}] model [])))) 127 | 128 | 129 | (defn- min-count-value [model] 130 | (if (= (:type model) :repeat) 131 | (:min model) 132 | (let [count-model (:count-model model)] 133 | (if (nil? count-model) 134 | 0 135 | (case (:type count-model) 136 | :enum (let [values (filter number? (:values count-model))] 137 | (when (seq values) 138 | (apply min values))) 139 | :fn (:min-value count-model) 140 | nil))))) 141 | 142 | (defn ^:no-doc assoc-leaf-distance-visitor 143 | "Associate an 'distance to leaf' measure to each node of the model. 144 | It is used as a hint on which path to choose when running out of budget 145 | in a budget-based data generation. It's very useful as well to avoid 146 | walking in infinite loops in the model." 147 | [model stack path] 148 | (let [distance (case (:type model) 149 | (:fn :enum) 0 150 | :map-of (let [entry-distance (-> model :entry-model ::leaf-distance)] 151 | (cond 152 | (zero? (min-count-value model)) 0 153 | (some? entry-distance) (inc entry-distance))) 154 | (:set-of 155 | :sequence-of 156 | :repeat) (if (or (not (contains? model :elements-model)) 157 | (zero? (min-count-value model))) 158 | 0 159 | (some-> (-> model :elements-model ::leaf-distance) inc)) 160 | (:or 161 | :alt) (let [distances (->> (:entries model) 162 | (map (comp ::leaf-distance :model)) 163 | (remove nil?))] 164 | (when (seq distances) 165 | (inc (reduce min distances)))) 166 | (:and 167 | :map 168 | :sequence 169 | :cat) (let [distances (->> (:entries model) 170 | (remove :optional) 171 | (map (comp ::leaf-distance :model)))] 172 | (when (every? some? distances) 173 | (inc (reduce max 0 distances)))) 174 | :let (some-> (-> model :body ::leaf-distance) inc) 175 | :ref (let [key (:key model) 176 | index (find-stack-index stack key) 177 | binding-distance (get-in stack [index :bindings key ::leaf-distance])] 178 | (some-> binding-distance inc)))] 179 | (cond-> model 180 | (some? distance) (assoc ::leaf-distance distance)))) 181 | 182 | (defn ^:no-doc assoc-min-cost-visitor 183 | "Associate an 'minimum cost' measure to each node of the model. 184 | It is used as a hint during the budget-based data generation." 185 | [model stack path] 186 | (let [type (:type model) 187 | min-cost (case type 188 | (:fn :enum) (::min-cost model 1) 189 | :map-of (let [container-cost 1 190 | min-count (min-count-value model) 191 | entry-min-cost (-> model :entry-model ::min-cost 192 | ; cancel the cost of the entry's vector 193 | (some-> dec)) 194 | content-cost (if (zero? min-count) 0 195 | (when (and min-count entry-min-cost) 196 | (* min-count entry-min-cost)))] 197 | (some-> content-cost (+ container-cost))) 198 | (:set-of 199 | :sequence-of 200 | :repeat) (let [container-cost 1 201 | min-count (min-count-value model) 202 | elements-model (:elements-model model) 203 | elements-model-min-cost (if elements-model 204 | (::min-cost elements-model) 205 | 1) ; the elements could be anything 206 | content-cost (if (zero? min-count) 0 207 | (when (and min-count elements-model-min-cost) 208 | (* elements-model-min-cost min-count)))] 209 | (some-> content-cost (+ container-cost))) 210 | (:or 211 | :alt) (let [existing-vals (->> (:entries model) 212 | (map (comp ::min-cost :model)) 213 | (filter some?))] 214 | (when (seq existing-vals) 215 | (reduce min existing-vals))) 216 | :and (let [vals (map (comp ::min-cost :model) (:entries model))] 217 | (when (and (seq vals) (every? some? vals)) 218 | (reduce max vals))) 219 | (:map 220 | :sequence 221 | :cat) (let [container-cost 1 222 | vals (->> (:entries model) 223 | (remove :optional) 224 | (map (comp ::min-cost :model))) 225 | content-cost (when (every? some? vals) (reduce + vals))] 226 | (some-> content-cost (+ container-cost))) 227 | :let (::min-cost (:body model)) 228 | :ref (let [key (:key model) 229 | index (find-stack-index stack key)] 230 | (get-in stack [index :bindings key ::min-cost])))] 231 | (cond-> model 232 | (some? min-cost) (assoc ::min-cost min-cost)))) 233 | 234 | (defn- rec-coll-size-gen 235 | "Returns a generator of numbers between 0 and max-size 236 | with a gaussian random distribution." 237 | [max-size] 238 | (if (pos? max-size) 239 | (gen/fmap (fn [[x y]] (+ x y 1)) 240 | (gen/tuple (gen/choose 0 (quot (dec max-size) 2)) 241 | (gen/choose 0 (quot max-size 2)))) 242 | (gen/return 0))) 243 | 244 | ;; Statistics about the distribution 245 | #_ (->> (gen/sample (rec-coll-size-gen 20) 10000) 246 | (frequencies) 247 | (sort-by first)) 248 | 249 | ;; maybe, use rec-coll-size-gen to pick up a size at each iteration 250 | (defn- decreasing-sizes-gen 251 | "Returns a generator of lazy sequence of decreasing sizes." 252 | [max-size] 253 | (#'gen/make-gen 254 | (fn [rng _] 255 | (let [f (fn f [rng max-size] 256 | (when-not (neg? max-size) 257 | (lazy-seq 258 | (let [[r1 r2] (random/split rng) 259 | size (#'gen/rand-range r1 0 max-size)] 260 | (cons size (f r2 (dec size)))))))] 261 | (rose/pure (f rng max-size)))))) 262 | 263 | #_(gen/sample (decreasing-sizes-gen 100) 1) 264 | 265 | (defn- budget-split-gen 266 | "Returns a generator which generates budget splits." 267 | [budget min-costs] 268 | (if (seq min-costs) 269 | (let [nb-elements (count min-costs) 270 | min-costs-sum (reduce + min-costs) 271 | budget-minus-min-costs (max 0 (- budget min-costs-sum))] 272 | (gen/fmap (fn [rates] 273 | (let [budget-factor (/ budget-minus-min-costs (reduce + rates))] 274 | (mapv (fn [min-cost rate] 275 | (+ min-cost (int (* rate budget-factor)))) 276 | min-costs 277 | rates))) 278 | (gen/vector (gen/choose 1 100) nb-elements))) 279 | (gen/return []))) 280 | 281 | 282 | (declare generator) 283 | 284 | (defn- sequence-generator 285 | "Returns a generator of a sequence." 286 | [context model budget] 287 | (if (and (#{:alt :cat :repeat :let :ref} (:type model)) 288 | (:inlined model true)) 289 | (or (:test.check/generator model) 290 | (case (:type model) 291 | :alt (let [possible-entries (filterv (comp ::leaf-distance :model) 292 | (:entries model)) 293 | affordable-entries (filterv (fn [entry] (<= (-> entry :model ::min-cost) budget)) 294 | possible-entries)] 295 | (if (seq affordable-entries) 296 | (gen/let [index (gen/choose 0 (dec (count affordable-entries)))] 297 | (sequence-generator context (:model (affordable-entries index)) budget)) 298 | (let [chosen-entry (first (sort-by (comp ::min-cost :model) possible-entries))] 299 | (sequence-generator context (:model chosen-entry) budget)))) 300 | 301 | :cat (let [budget (max 0 (dec budget)) ; the cat itself costs 1 302 | entries (:entries model) 303 | min-costs (mapv (comp ::min-cost :model) entries)] 304 | (gen/let [budgets (budget-split-gen budget min-costs) 305 | sequences (apply gen/tuple 306 | (mapv (fn [entry budget] 307 | (sequence-generator context (:model entry) budget)) 308 | entries 309 | budgets))] 310 | (into [] cat sequences))) 311 | 312 | :repeat (let [budget (max 0 (dec budget)) ; the repeat itself costs 1 313 | min-repeat (:min model) 314 | max-repeat (:max model) 315 | elements-model (:elements-model model) 316 | elm-min-cost (::min-cost elements-model) 317 | coll-max-size (-> (int (/ budget elm-min-cost)) 318 | (min max-repeat))] 319 | (gen/let [n-repeat (gen/fmap (fn [size] (+ min-repeat size)) 320 | (rec-coll-size-gen (- coll-max-size min-repeat))) 321 | budgets (let [min-costs (repeat n-repeat elm-min-cost)] 322 | (budget-split-gen budget min-costs)) 323 | sequences (apply gen/tuple 324 | (mapv (fn [budget] 325 | (sequence-generator context elements-model budget)) 326 | budgets))] 327 | (into [] cat sequences))) 328 | 329 | :let (sequence-generator (#'m/comp-bindings context (:bindings model)) (:body model) budget) 330 | :ref (let [[context model] (#'m/resolve-ref context (:key model))] 331 | (sequence-generator context model budget)))) 332 | (gen/fmap vector 333 | (generator context (dissoc model :inlined) budget)))) 334 | 335 | (defn- generator 336 | "Returns a generator of a data structure." 337 | [context model budget] 338 | (or (:test.check/generator model) 339 | (case (:type model) 340 | 341 | :fn nil ;; a generator is supposed to be provided for those nodes 342 | 343 | ;; TODO: there "might be" an problem a enumeration order from the set. 344 | :enum (gen/elements (:values model)) 345 | 346 | (:and :or) nil ;; a generator is supposed to be provided for those nodes 347 | 348 | :alt (let [possible-entries (filterv (comp ::leaf-distance :model) 349 | (:entries model)) 350 | affordable-entries (filterv (fn [entry] (<= (-> entry :model ::min-cost) budget)) 351 | possible-entries)] 352 | (if (seq affordable-entries) 353 | (gen/let [index (gen/choose 0 (dec (count affordable-entries)))] 354 | (generator context (:model (affordable-entries index)) budget)) 355 | (let [chosen-entry (first (sort-by (comp ::min-cost :model) possible-entries))] 356 | (generator context (:model chosen-entry) budget)))) 357 | 358 | :set-of (let [budget (max 0 (dec budget)) ; the collection itself costs 1 359 | elements-model (:elements-model model) 360 | count-model (:count-model model) 361 | elm-min-cost (::min-cost elements-model) 362 | coll-sizes-gen (if count-model 363 | (if (= (:type count-model) :enum) 364 | (gen/shuffle (sort (:values count-model))) 365 | (gen/vector-distinct (generator context count-model 0) 366 | {:min-elements 1})) 367 | (decreasing-sizes-gen (int (/ budget elm-min-cost)))) 368 | set-gen (gen/bind coll-sizes-gen 369 | (fn [coll-sizes] 370 | (#'gen/make-gen 371 | (fn [rng gen-size] 372 | (loop [rng rng 373 | coll-sizes coll-sizes] 374 | (when-not (seq coll-sizes) 375 | (throw (ex-info "Couldn't generate a set." {:elements-model elements-model 376 | :coll-sizes coll-sizes}))) 377 | (let [[r1 r2 next-rng] (random/split-n rng 3) 378 | coll-size (first coll-sizes) 379 | elements-gen (if elements-model 380 | (let [min-costs (repeat coll-size elm-min-cost) 381 | budgets (-> (budget-split-gen budget min-costs) 382 | (gen/call-gen r1 gen-size) 383 | (rose/root))] 384 | (apply gen/tuple 385 | (mapv (partial generator context elements-model) 386 | budgets))) 387 | (gen/vector gen/any coll-size)) 388 | elements (-> (gen/call-gen elements-gen r2 gen-size) 389 | (rose/root)) 390 | elements-in-set (into #{} elements)] 391 | (if (= (count elements) (count elements-in-set)) 392 | (rose/pure elements-in-set) 393 | (recur next-rng (rest coll-sizes)))))))))] 394 | (cond->> set-gen 395 | (contains? model :condition-model) (gen/such-that (partial #'m/-valid? context (:condition-model model))))) 396 | 397 | :map-of (cond->> (let [budget (max 0 (dec budget)) ; the collection itself costs 1 398 | count-model (:count-model model) 399 | entry-model (:entry-model model) 400 | entry-min-cost (::min-cost entry-model) 401 | coll-max-size (int (/ budget entry-min-cost)) 402 | coll-size-gen (if count-model 403 | (generator context count-model 0) 404 | (rec-coll-size-gen coll-max-size)) 405 | budgets-gen (gen/bind coll-size-gen 406 | (fn [coll-size] 407 | (let [min-costs (repeat coll-size entry-min-cost)] 408 | (budget-split-gen budget min-costs)))) 409 | entries-gen (gen/bind budgets-gen 410 | (fn [entry-budgets] 411 | (apply gen/tuple 412 | (mapv (partial generator context entry-model) 413 | entry-budgets)))) 414 | map-gen (gen/fmap (fn [entries] 415 | (into {} entries)) 416 | entries-gen)] 417 | (cond->> map-gen 418 | (contains? model :condition-model) (gen/such-that (partial #'m/-valid? context (:condition-model model)))))) 419 | 420 | :map (let [budget (max 0 (dec budget)) ; the collection itself costs 1 421 | possible-entries (filterv (comp ::min-cost :model) (:entries model)) 422 | {required-entries false, optional-entries true} (group-by (comp true? :optional) possible-entries) 423 | required-min-cost (transduce (map (comp ::min-cost :model)) + required-entries) 424 | map-gen (gen/let [coll-size (if (< required-min-cost budget) 425 | (gen/choose (count required-entries) (count possible-entries)) 426 | (gen/return (count required-entries))) 427 | selected-entries (gen/fmap (fn [shuffled-optional-entries] 428 | (->> (concat required-entries shuffled-optional-entries) 429 | (take coll-size))) 430 | (gen/shuffle optional-entries)) 431 | entry-budgets (let [min-costs (mapv (comp ::min-cost :model) selected-entries)] 432 | (budget-split-gen budget min-costs))] 433 | (apply gen/hash-map 434 | (mapcat (fn [entry budget] 435 | [(:key entry) (generator context (:model entry) budget)]) 436 | selected-entries 437 | entry-budgets)))] 438 | (cond->> map-gen 439 | (contains? model :condition-model) (gen/such-that (partial #'m/-valid? context (:condition-model model))))) 440 | 441 | (:sequence-of :sequence) (let [budget (max 0 (dec budget)) ; the collection itself costs 1 442 | entries (:entries model) 443 | seq-gen (if entries 444 | ; :sequence ... count-model is not used 445 | (gen/bind (budget-split-gen budget (mapv (comp ::min-cost :model) entries)) 446 | (fn [budgets] 447 | (apply gen/tuple (mapv (fn [entry budget] 448 | (generator context (:model entry) budget)) 449 | entries budgets)))) 450 | ; :sequence-of ... count-model and/or elements-model might be used 451 | (let [count-model (:count-model model) 452 | elements-model (:elements-model model) 453 | elm-min-cost (::min-cost elements-model) 454 | coll-max-size (int (/ budget elm-min-cost)) 455 | coll-size-gen (if count-model 456 | (generator context count-model 0) 457 | (rec-coll-size-gen coll-max-size))] 458 | (if elements-model 459 | (let [budgets-gen (gen/bind coll-size-gen 460 | (fn [coll-size] 461 | (let [min-costs (repeat coll-size elm-min-cost)] 462 | (budget-split-gen budget min-costs))))] 463 | (gen/bind budgets-gen 464 | (fn [budgets] 465 | (apply gen/tuple 466 | (mapv (partial generator context elements-model) 467 | budgets))))) 468 | (gen/bind coll-size-gen 469 | (fn [coll-size] 470 | (gen/vector gen/any coll-size)))))) 471 | contained-seq-gen (gen/fmap (fn [[coll inside-list?]] 472 | (case (:coll-type model) 473 | :list (apply list coll) 474 | :vector coll 475 | :string (apply str coll) 476 | (cond->> coll 477 | inside-list? (apply list)))) 478 | (gen/tuple seq-gen (gen/no-shrink gen/boolean)))] 479 | (cond->> contained-seq-gen 480 | (contains? model :condition-model) (gen/such-that (partial #'m/-valid? context (:condition-model model))))) 481 | 482 | (:cat :repeat) (let [seq-gen (sequence-generator context (dissoc model :inlined) budget) 483 | contained-seq-gen (gen/fmap (fn [[coll inside-list?]] 484 | (case (:coll-type model) 485 | :list (apply list coll) 486 | :vector coll 487 | :string (apply str coll) 488 | (cond->> coll 489 | inside-list? (apply list)))) 490 | (gen/tuple seq-gen (gen/no-shrink gen/boolean)))] 491 | (cond->> contained-seq-gen 492 | (contains? model :condition-model) (gen/such-that (partial #'m/-valid? context (:condition-model model))))) 493 | 494 | :let (generator (#'m/comp-bindings context (:bindings model)) (:body model) budget) 495 | 496 | :ref (let [[context model] (#'m/resolve-ref context (:key model))] 497 | (generator context model budget))))) 498 | 499 | (defn decorate-model 500 | "Analyzes and decorates the model with information regarding 'distance to leaf' and 'minimal cost'." 501 | [model] 502 | (let [visitor (fn [model stack path] 503 | (-> model 504 | (assoc-leaf-distance-visitor stack path) 505 | (assoc-min-cost-visitor stack path))) 506 | walker (fn [model] 507 | (postwalk model visitor))] 508 | (util/iterate-while-different walker model 100))) 509 | 510 | (defn gen 511 | "Returns a test.check generator derived from the model." 512 | ([model] 513 | (gen model nil)) 514 | ([model budget] 515 | (let [decorated-model (decorate-model model)] 516 | (when-not (::min-cost decorated-model) 517 | (throw (ex-info "The model cannot be generated as it contains infinite structures that cannot be avoided during generation." 518 | {:model model 519 | :decorated-model decorated-model}))) 520 | (if budget 521 | (generator [] decorated-model budget) 522 | (gen/sized (fn [size] ; size varies between 0 and 200 523 | (generator [] decorated-model size))))))) 524 | -------------------------------------------------------------------------------- /src/minimallist/helper.cljc: -------------------------------------------------------------------------------- 1 | (ns minimallist.helper 2 | (:refer-clojure :exclude [fn val and or set map sequence vector-of list vector cat repeat ? * + let ref]) 3 | (:require [clojure.core :as clj])) 4 | 5 | ;; 6 | ;; Some helper functions to compose hash-map based models 7 | ;; 8 | 9 | 10 | ; Modifiers, can be used with the -> macro 11 | 12 | (defn- -entry 13 | "Parses an entry from one of the following formats: 14 | - model 15 | - [key model] 16 | - [key options model] 17 | 18 | When specified, options is a hashmap." 19 | [entry] 20 | (clj/let [[key options model] (if (vector? entry) 21 | (case (count entry) 22 | 2 [(first entry) nil (second entry)] 23 | 3 entry 24 | (throw (ex-info "wrong entry format" entry))) 25 | [nil nil entry])] 26 | (cond-> options 27 | key (assoc :key key) 28 | model (assoc :model model)))) 29 | 30 | 31 | (defn with-count 32 | "Specifies a :count-model to a collection model with varying size, 33 | namely :set-of, :map-of and :sequence-of." 34 | [collection-model count-model] 35 | (assoc collection-model :count-model count-model)) 36 | 37 | (defn with-entries 38 | "Adds entries to a :map model." 39 | [map-model & entries] 40 | (assoc map-model 41 | :entries (into (:entries map-model []) 42 | (clj/map -entry) 43 | entries))) 44 | 45 | (defn with-optional-entries 46 | "Adds optional entries to a :map model." 47 | [map-model & entries] 48 | (assoc map-model 49 | :entries (into (:entries map-model []) 50 | (clj/map (clj/fn [entry] 51 | (-> (-entry entry) 52 | (assoc :optional true)))) 53 | entries))) 54 | 55 | (defn with-condition 56 | "Specifies a :condition-model to a model" 57 | [model condition-model] 58 | (assoc model :condition-model condition-model)) 59 | 60 | ;; For any node, mostly for :fn 61 | (defn with-test-check-gen 62 | "Specifies a test-check generator to a model." 63 | [model generator] 64 | (assoc model :test.check/generator generator)) 65 | 66 | (defn in-list 67 | "Specifies the :coll-type to be a list. 68 | To be used on sequence model nodes, namely :sequence-of, :sequence, :cat and :repeat." 69 | [sequence-model] 70 | (assoc sequence-model :coll-type :list)) 71 | 72 | (defn in-vector 73 | "Specifies the :coll-type to be a vector. 74 | To be used on sequence model nodes, namely :sequence-of, :sequence, :cat and :repeat." 75 | [sequence-model] 76 | (assoc sequence-model :coll-type :vector)) 77 | 78 | (defn in-string 79 | "Specifies the :coll-type to be a string. 80 | To be used on sequence model nodes, namely :sequence-of, :sequence, :cat and :repeat." 81 | [sequence-model] 82 | (assoc sequence-model :coll-type :string)) 83 | 84 | (defn not-inlined 85 | "Specify that this sequence model should not be inlined within its parent model. 86 | Useful on nodes :alt, :cat and :repeat." 87 | [sequence-model] 88 | (assoc sequence-model :inlined false)) 89 | 90 | 91 | ;; The main functions 92 | 93 | (defn fn 94 | "Model for values verifying custom predicates." 95 | [predicate] 96 | {:type :fn 97 | :fn predicate}) 98 | 99 | (defn val 100 | "Model for a fixed value." 101 | [value] 102 | {:type :enum 103 | :values #{value}}) 104 | 105 | (defn enum 106 | "Model of one value from a set." 107 | [values-set] 108 | {:type :enum 109 | :values values-set}) 110 | 111 | (defn and 112 | "Model for validating a value against a conjunction of models." 113 | [& conditions] 114 | {:type :and 115 | :entries (mapv -entry conditions)}) 116 | 117 | (defn or 118 | "Model for validating a value against a disjunction of models." 119 | [& conditions] 120 | {:type :or 121 | :entries (mapv -entry conditions)}) 122 | 123 | (defn set-of 124 | "Model of a set of values matching a specific model." 125 | [elements-model] 126 | {:type :set-of 127 | :elements-model elements-model}) 128 | 129 | (defn map-of 130 | "Model of a hashmap made of entries (2-vector) of a specific model." 131 | [entry-model] 132 | {:type :map-of 133 | :entry-model entry-model}) 134 | 135 | (defn sequence-of 136 | "Model of a sequence of values, all matching a specific model." 137 | [elements-model] 138 | {:type :sequence-of 139 | :elements-model elements-model}) 140 | 141 | (defn list-of 142 | "Same as sequence-of, but inside a list." 143 | [elements-model] 144 | (-> (sequence-of elements-model) in-list)) 145 | 146 | (defn vector-of 147 | "Same as sequence-of, but inside a vector." 148 | [elements-model] 149 | (-> (sequence-of elements-model) in-vector)) 150 | 151 | (defn string-of 152 | "Same as sequence-of, but inside a string." 153 | [elements-model] 154 | (-> (sequence-of elements-model) in-string)) 155 | 156 | (defn map 157 | "Model of a hashmap with specified models for each of its entries." 158 | ([] 159 | {:type :map}) 160 | ([& entries] 161 | (apply with-entries (map) entries))) 162 | 163 | (defn tuple 164 | "A sequence of values, each matching their specific model." 165 | [& entries] 166 | {:type :sequence 167 | :entries (mapv -entry entries)}) 168 | 169 | (defn list 170 | "Same as tuple, but inside a list." 171 | [& entries] 172 | (-> (apply tuple entries) in-list)) 173 | 174 | (defn vector 175 | "Same as tuple, but inside a vector." 176 | [& entries] 177 | (-> (apply tuple entries) in-vector)) 178 | 179 | (defn string-tuple 180 | "Same as tuple, but inside a string." 181 | [& entries] 182 | (-> (apply tuple entries) in-string)) 183 | 184 | (defn alt 185 | "Model of a choice (alternative) between different possible entries." 186 | [& entries] 187 | {:type :alt 188 | :entries (mapv -entry entries)}) 189 | 190 | (defn cat 191 | "Sequence model of a concatenation of models. 192 | The sequence models in the entries are inlined by default." 193 | [& entries] 194 | {:type :cat 195 | :entries (mapv -entry entries)}) 196 | 197 | (defn repeat 198 | "Sequence model of a repetition of a model. 199 | If elements-model is a sequence model, it is inlined by default." 200 | ([times elements-model] 201 | (repeat times times elements-model)) 202 | ([min max elements-model] 203 | {:type :repeat 204 | :min min 205 | :max max 206 | :elements-model elements-model})) 207 | 208 | (defn ? 209 | "Sequence model of a model being either absent or present." 210 | [model] 211 | (repeat 0 1 model)) 212 | 213 | (defn * 214 | "Sequence model of a model being either absent or repeated an 215 | arbitrary number of times." 216 | [model] 217 | (repeat 0 ##Inf model)) 218 | 219 | (defn + 220 | "Sequence model of a model being repeated an 221 | arbitrary number of times." 222 | [model] 223 | (repeat 1 ##Inf model)) 224 | 225 | (defn let 226 | "Model with local model definitions." 227 | [bindings body] 228 | {:type :let 229 | :bindings (apply hash-map bindings) 230 | :body body}) 231 | 232 | (defn ref 233 | "A reference to a model locally defined." 234 | [key] 235 | {:type :ref 236 | :key key}) 237 | 238 | (defn char-cat 239 | "A cat sequence of chars, built from a string which contains them." 240 | [s] 241 | (apply cat (clj/map val s))) 242 | 243 | (defn char-set 244 | "A set of chars, built from a string which contains them." 245 | [s] 246 | (enum (clj/set s))) 247 | -------------------------------------------------------------------------------- /src/minimallist/minicup.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc minimallist.minicup 2 | (:require [minimallist.core :as m] 3 | [minimallist.helper :as h])) 4 | 5 | ;; 6 | ;; In this namespace, I am testing the library's usability from the user's perspective. 7 | ;; 8 | 9 | ;; Reference samples for minicup format, a hiccup-like form of Minimallist's models. 10 | (def minicup-reference-samples 11 | [int? ; shorthand format 12 | [:fn int?] 13 | 14 | #(= 1 %) ; shorthand format 15 | [:fn {:gen #{1}} #(= 1 %)] ; hiccup format, with a place for the properties 16 | 17 | #{1 "2" :3} 18 | [:enum #{1 "2" :3}] 19 | 20 | [:and pos-int? even?] 21 | 22 | [:or pos-int? even?] 23 | 24 | [:set-of {:count #{2 3}} int?] 25 | 26 | [:map 27 | [:a int?] 28 | [:b {:optional true} string?] 29 | [(list 1 2 3) string?]] 30 | 31 | [:map-of keyword? int?] 32 | 33 | [:sequence-of {:count #{2 3}} int?] ; list or vector 34 | [:list-of {:count #{2 3}} int?] ; list only 35 | [:vector-of {:count #{2 3}} int?] ;vector only 36 | 37 | [:tuple int? int? int?] ; list or vector 38 | 39 | (list int? int? int?) ; maybe not a shorthand? 40 | [:list int? int? int?] ; list only 41 | 42 | [int? int? int?] ; maybe not a shorthand? 43 | [:vector int? int? int?] ; vector only 44 | 45 | [:alt 46 | [:int int?] 47 | [:keyword keyword?] 48 | [:ints [:repeat 0 2 int?]] 49 | [:keywords [:repeat 0 2 keyword?]]] 50 | 51 | [:cat string? [:+ pos-int?] [:+ int?]] 52 | 53 | [:? int?] 54 | [:* int?] 55 | [:+ int?] 56 | [:repeat 0 2 int?] 57 | 58 | [:let ['foo int? 59 | :bar string?] 60 | [:vector [:ref 'foo] [:ref :bar]]]]) 61 | 62 | 63 | ;; Model of the minicup format 64 | 65 | (def minicup-model 66 | (let [anything-model (h/fn any?) 67 | entry-of (fn [entry-model] 68 | (h/not-inlined (h/alt [:vector-entry (h/in-vector (h/cat [:key anything-model] 69 | [:properties (h/? (h/map))] 70 | [:model (h/not-inlined entry-model)]))] 71 | [:entry-model entry-model])))] 72 | (h/let ['model (h/alt 73 | [:immediate-fn (h/fn fn?)] 74 | [:fn (h/in-vector (h/cat (h/val :fn) 75 | (h/? (h/map [:gen {:optional true} anything-model])) 76 | (h/fn fn?)))] 77 | [:immediate-enum (h/set-of anything-model)] 78 | [:enum (h/in-vector (h/cat (h/val :enum) 79 | (h/set-of anything-model)))] 80 | [:and (h/in-vector (h/cat (h/val :and) 81 | (h/+ (entry-of (h/ref 'model)))))] 82 | [:or (h/in-vector (h/cat (h/val :or) 83 | (h/+ (entry-of (h/ref 'model)))))] 84 | [:set-of (h/in-vector (h/cat (h/val :set-of) 85 | (h/? (h/map [:count {:optional true} (h/ref 'model)])) 86 | (h/not-inlined (h/ref 'model))))] 87 | [:map (h/in-vector (h/cat (h/val :map) 88 | (h/+ (entry-of (h/ref 'model)))))] 89 | [:map-of (h/vector (h/val :map-of) 90 | (h/ref 'model))] 91 | [:sequence-of (h/in-vector (h/cat (h/val :sequence-of) 92 | (h/? (h/map [:count {:optional true} (h/ref 'model)])) 93 | (h/not-inlined (h/ref 'model))))] 94 | [:list-of (h/in-vector (h/cat (h/val :list-of) 95 | (h/? (h/map [:count {:optional true} (h/ref 'model)])) 96 | (h/not-inlined (h/ref 'model))))] 97 | [:vector-of (h/in-vector (h/cat (h/val :vector-of) 98 | (h/? (h/map [:count {:optional true} (h/ref 'model)])) 99 | (h/not-inlined (h/ref 'model))))] 100 | [:tuple (h/in-vector (h/cat (h/val :tuple) 101 | (h/+ (entry-of (h/ref 'model)))))] 102 | [:immediate-list (h/in-list (h/+ (entry-of (h/ref 'model))))] 103 | [:list (h/in-vector (h/cat (h/val :list) 104 | (h/+ (entry-of (h/ref 'model)))))] 105 | [:immediate-vector (h/in-vector (h/+ (entry-of (h/ref 'model))))] 106 | [:vector (h/in-vector (h/cat (h/val :vector) 107 | (h/+ (entry-of (h/ref 'model)))))] 108 | [:alt (h/in-vector (h/cat (h/val :alt) 109 | (h/+ (entry-of (h/ref 'model)))))] 110 | [:cat (h/in-vector (h/cat (h/val :cat) 111 | (h/+ (entry-of (h/ref 'model)))))] 112 | [:? (h/vector (h/val :?) 113 | (h/ref 'model))] 114 | [:* (h/vector (h/val :*) 115 | (h/ref 'model))] 116 | [:+ (h/vector (h/val :+) 117 | (h/ref 'model))] 118 | [:repeat (h/vector (h/val :repeat) 119 | (h/fn int?) 120 | (h/fn int?) 121 | (h/ref 'model))] 122 | [:let (h/vector (h/val :let) 123 | (h/in-vector (h/+ (h/cat anything-model 124 | (h/not-inlined (h/ref 'model))))) 125 | (h/ref 'model))] 126 | [:ref (h/vector (h/val :ref) anything-model)])] 127 | (h/ref 'model)))) 128 | 129 | (comment 130 | (m/valid? minicup-model [:and int? int?]) 131 | (m/describe minicup-model [:and int? int?])) 132 | -------------------------------------------------------------------------------- /src/minimallist/minimap.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc minimallist.minimap 2 | (:require [minimallist.core :as m] 3 | [minimallist.helper :as h])) 4 | 5 | ;; A model of the model format accepted by Minimallist's functions. 6 | ;; Use it to validate your models when you have problems which you don't understand, just in case. 7 | (def minimap-model 8 | (h/let ['model (h/alt [:fn (-> (h/map [:type (h/val :fn)] 9 | [:fn (h/fn fn?)]) 10 | (h/with-optional-entries [:condition-model (h/ref 'model)]))] 11 | [:enum (h/map [:type (h/val :enum)] 12 | [:values (h/set-of (h/fn any?))])] 13 | [:and-or (h/map [:type (h/enum #{:and :or})] 14 | [:entries (h/vector-of (h/map [:model (h/ref 'model)]))])] 15 | [:set-of (-> (h/map [:type (h/val :set-of)]) 16 | (h/with-optional-entries [:count-model (h/ref 'model)] 17 | [:elements-model (h/ref 'model)] 18 | [:condition-model (h/ref 'model)]))] 19 | [:map-of (-> (h/map [:type (h/val :map-of)] 20 | [:entry-model (h/ref 'model)]) 21 | (h/with-optional-entries [:condition-model (h/ref 'model)]))] 22 | [:map (-> (h/map [:type (h/val :map)]) 23 | (h/with-optional-entries [:entries (h/vector-of (-> (h/map [:key (h/fn any?)] 24 | [:model (h/ref 'model)]) 25 | (h/with-optional-entries [:optional (h/fn boolean?)])))] 26 | [:condition-model (h/ref 'model)]))] 27 | [:sequence-of (-> (h/map [:type (h/val :sequence-of)]) 28 | (h/with-optional-entries [:coll-type (h/enum #{:any :list :vector})] 29 | [:count-model (h/ref 'model)] 30 | [:elements-model (h/ref 'model)] 31 | [:condition-model (h/ref 'model)]) 32 | (h/with-condition (h/fn #(not (and (or (contains? % :count-model) 33 | (contains? % :elements-model)) 34 | (contains? % :entries))))))] 35 | [:sequence (-> (h/map [:type (h/val :sequence)]) 36 | (h/with-optional-entries [:coll-type (h/enum #{:any :list :vector})] 37 | [:entries (h/vector-of (h/map [:model (h/ref 'model)]))] 38 | [:condition-model (h/ref 'model)]) 39 | (h/with-condition (h/fn #(not (and (or (contains? % :count-model) 40 | (contains? % :elements-model)) 41 | (contains? % :entries))))))] 42 | [:alt (-> (h/map [:type (h/val :alt)] 43 | [:entries (h/vector-of (h/map [:key (h/fn any?)] 44 | [:model (h/ref 'model)]))]) 45 | (h/with-optional-entries [:inlined (h/fn boolean?)]))] 46 | [:cat (-> (h/map [:type (h/val :cat)] 47 | [:entries (h/vector-of (h/map [:model (h/ref 'model)]))]) 48 | (h/with-optional-entries [:coll-type (h/enum #{:any :list :vector})] 49 | [:count-model (h/ref 'model)] 50 | [:inlined (h/fn boolean?)] 51 | [:condition-model (h/ref 'model)]))] 52 | [:repeat (-> (h/map [:type (h/val :repeat)] 53 | [:min (h/or (h/val 0) 54 | (h/fn pos-int?))] 55 | [:max (h/or (h/fn pos-int?) 56 | (h/val ##Inf))] 57 | [:elements-model (h/ref 'model)]) 58 | (h/with-optional-entries [:coll-type (h/enum #{:any :list :vector})] 59 | [:count-model (h/ref 'model)] 60 | [:inlined (h/fn boolean?)] 61 | [:condition-model (h/ref 'model)]) 62 | (h/with-condition (h/fn #(<= (:min %) (:max %)))))] 63 | [:let (h/map [:type (h/val :let)] 64 | [:bindings (h/map-of (h/vector (h/fn any?) (h/ref 'model)))] 65 | [:body (h/ref 'model)])] 66 | [:ref (h/map [:type (h/val :ref)] 67 | [:key (h/fn any?)])])] 68 | (h/ref 'model))) 69 | 70 | (comment 71 | ;; Just for fun 72 | (m/valid? minimap-model minimap-model) 73 | 74 | ;; This is how you normally use it 75 | ;; (This will be a lot more useful after the explain function is implemented) 76 | (m/valid? minimap-model (h/fn int?))) 77 | -------------------------------------------------------------------------------- /src/minimallist/util.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc minimallist.util 2 | (:require [clojure.set :as set] 3 | [clojure.walk :as walk])) 4 | 5 | (defn lazy-map 6 | "Similar to the map function, except that it evaluates 1 element at a time." 7 | [f & colls] 8 | (lazy-seq 9 | (let [coll-seqs (mapv seq colls)] 10 | (when (every? some? coll-seqs) 11 | (cons (apply f (mapv first coll-seqs)) 12 | (apply lazy-map f (mapv rest coll-seqs))))))) 13 | 14 | (defn reduce-update [[acc data] key f & args] 15 | (let [elm (get data key) 16 | [updated-acc updated-elm] (apply f acc elm args) 17 | updated-data (assoc data key updated-elm)] 18 | [updated-acc updated-data])) 19 | 20 | (defn reduce-update-in [[acc data] path f & args] 21 | (let [elm (get-in data path) 22 | [updated-acc updated-elm] (apply f acc elm args) 23 | updated-data (assoc-in data path updated-elm)] 24 | [updated-acc updated-data])) 25 | 26 | (defn reduce-mapv [f acc coll] 27 | (reduce (fn [[acc v] elm] 28 | (let [[updated-acc updated-elm] (f acc elm)] 29 | [updated-acc (conj v updated-elm)])) 30 | [acc []] 31 | coll)) 32 | 33 | (defn walk-map-select-keys [expr keys-to-select] 34 | (walk/postwalk (fn [expr] 35 | (cond-> expr 36 | (map? expr) (select-keys keys-to-select))) 37 | expr)) 38 | 39 | (defn walk-map-dissoc [expr & keys-to-dissoc] 40 | (walk/postwalk (fn [expr] 41 | (if (map? expr) 42 | (apply dissoc expr keys-to-dissoc) 43 | expr)) 44 | expr)) 45 | 46 | (defn iterate-while-different 47 | "Iterates f on val until the (= val (f val)) up to maximum number of iterations, 48 | then returns val." 49 | [f val max-iterations] 50 | (->> (iterate f val) 51 | (partition 2 1) 52 | (take max-iterations) 53 | (take-while (fn [[x y]] (not= x y))) 54 | (cons (list nil val)) 55 | last 56 | second)) 57 | -------------------------------------------------------------------------------- /test/minimallist/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns minimallist.core-test 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [minimallist.core :refer [valid? explain describe undescribe] :as m] 4 | [minimallist.helper :as h] 5 | [minimallist.util :as util])) 6 | 7 | (comment 8 | (#'m/sequence-descriptions {} 9 | ; [:cat [:+ pos-int?] 10 | ; [:+ int?]] 11 | (h/cat (h/+ (h/fn pos-int?)) 12 | (h/+ (h/fn int?))) 13 | [3 4 0 2]) 14 | 15 | (#'m/sequence-descriptions {} 16 | ; [:repeat {:min 0, :max 2} int?] 17 | (h/repeat 0 2 (h/fn int?)) 18 | (seq [1 2])) 19 | 20 | (#'m/sequence-descriptions {} 21 | ; [:alt [:ints [:repeat {:min 0, :max 2} int?]] 22 | ; [:keywords [:repeat {:min 0, :max 2} keyword?]] 23 | (h/alt [:ints (h/repeat 0 2 (h/fn int?))] 24 | [:keywords (h/repeat 0 2 (h/fn keyword?))]) 25 | (seq [1 2])) 26 | 27 | (#'m/sequence-descriptions {} 28 | ; [:* int?] 29 | (h/* (h/fn int?)) 30 | (seq [1 :2])) 31 | 32 | (#'m/sequence-descriptions {} 33 | ; [:+ int?] 34 | (h/+ (h/fn int?)) 35 | (seq [1 2 3]))) 36 | 37 | (deftest valid?-test 38 | (let [test-data [;; fn 39 | (h/fn #(= 1 %)) 40 | [1] 41 | [2] 42 | 43 | (-> (h/fn int?) 44 | (h/with-condition (h/fn odd?))) 45 | [1] 46 | [2] 47 | 48 | (-> (h/fn symbol?) 49 | (h/with-condition (h/fn (complement #{'if 'val})))) 50 | ['a] 51 | ['if] 52 | 53 | ;; enum 54 | (h/enum #{1 "2" :3}) 55 | [1 "2" :3] 56 | [[1] 2 true false nil] 57 | 58 | (h/enum #{nil false}) 59 | [nil false] 60 | [true '()] 61 | 62 | ;; and 63 | (h/and (h/fn pos-int?) 64 | (h/fn even?)) 65 | [2 4 6] 66 | [0 :a -1 1 3] 67 | 68 | ;; or 69 | (h/or (h/fn pos-int?) 70 | (h/fn even?)) 71 | [-2 0 1 2 3] 72 | [-3 -1] 73 | 74 | ;; set 75 | (-> (h/set-of (h/fn int?)) 76 | (h/with-count (h/enum #{2 3}))) 77 | [#{1 2} #{1 2 3}] 78 | [#{1 :a} [1 2 3] '(1 2) `(1 ~2) #{1} #{1 2 3 4}] 79 | 80 | ;; map, entries 81 | (h/map [:a (h/fn int?)] 82 | [:b {:optional true} (h/fn string?)] 83 | [(list 1 2 3) (h/fn string?)]) 84 | [{:a 1, :b "foo", (list 1 2 3) "you can count on me like ..."} 85 | {:a 1, :b "bar", [1 2 3] "soleil !"} 86 | {:a 1, [1 2 3] "soleil !"}] 87 | [{:a 1, :b "foo"} 88 | {:a 1, :b "foo", #{1 2 3} "bar"} 89 | {:a 1, :b 'bar, [1 2 3] "soleil !"}] 90 | 91 | ;; map, keys and values 92 | (h/map-of (h/vector (h/fn keyword?) (h/fn int?))) 93 | [{} {:a 1, :b 2}] 94 | [{:a 1, :b "2"} [[:a 1] [:b 2]] {true 1, false 2}] 95 | 96 | ;; sequence, no collection type specified 97 | (h/sequence-of (h/fn int?)) 98 | ['(1 2 3) [1 2 3] `(1 2 ~3)] 99 | ['(1 :a) #{1 2 3} {:a 1, :b 2, :c 3}] 100 | 101 | (h/sequence-of (h/fn char?)) 102 | ["" "hi" "hello"] 103 | [[1 2 3]] 104 | 105 | ;; sequence, with condition 106 | (-> (h/sequence-of (h/fn int?)) 107 | (h/with-condition (h/fn (fn [coll] (= coll (reverse coll)))))) 108 | [[1] '(1 1) '[1 2 1]] 109 | ['(1 2) '(1 2 3)] 110 | 111 | ;; sequence as a list 112 | (h/list-of (h/fn int?)) 113 | ['(1 2 3) `(1 2 ~3)] 114 | ['(1 :a) [1 2 3] #{1 2 3}] 115 | 116 | ;; sequence as a vector 117 | (h/vector-of (h/fn int?)) 118 | [[1 2 3]] 119 | [[1 :a] '(1 2 3) #{1 2 3} `(1 2 ~3)] 120 | 121 | ;; sequence as a string 122 | (h/string-of (h/enum (set "0123456789abcdef"))) 123 | ["03ab4c" "cafe"] 124 | ["coffee" [1 :a] '(1 2 3) #{1 2 3} `(1 2 ~3)] 125 | 126 | ;; sequence with size specified using a model 127 | (-> (h/sequence-of (h/fn any?)) 128 | (h/with-count (h/enum #{2 3}))) 129 | ['(1 2) [1 "2"] `(1 ~"2") [1 "2" :3] "hi"] 130 | [#{1 "a"} [1 "2" :3 :4] "hello"] 131 | 132 | ;; sequence with entries (fixed size is implied) 133 | (h/tuple (h/fn int?) (h/fn string?)) 134 | ['(1 "2") [1 "2"] `(1 ~"2")] 135 | [#{1 "a"} [1 "2" :3]] 136 | 137 | ;; sequence with entries in a string 138 | (h/string-tuple (h/val \a) (h/enum #{\b \c})) 139 | ["ab" "ac"] 140 | [[\a \b] #{\a \b}] 141 | 142 | ;; alt 143 | (h/alt [:int (h/fn int?)] 144 | [:strings (h/vector-of (h/fn string?))]) 145 | [1 ["1"]] 146 | [[1] "1" :1 [:1]] 147 | 148 | ;; alt - inside a cat 149 | (h/cat (h/fn int?) 150 | (h/alt [:string (h/fn string?)] 151 | [:keyword (h/fn keyword?)] 152 | [:string-keyword (h/cat (h/fn string?) 153 | (h/fn keyword?))]) 154 | (h/fn int?)) 155 | [[1 "2" 3] [1 :2 3] [1 "a" :b 3]] 156 | [[1 ["a" :b] 3]] 157 | 158 | ;; alt - inside a cat, but with :inline false on its cat entry 159 | (h/cat (h/fn int?) 160 | (h/alt [:string (h/fn string?)] 161 | [:keyword (h/fn keyword?)] 162 | [:string-keyword (-> (h/cat (h/fn string?) 163 | (h/fn keyword?)) 164 | (h/not-inlined))]) 165 | (h/fn int?)) 166 | [[1 "2" 3] [1 :2 3] [1 ["a" :b] 3]] 167 | [[1 "a" :b 3]] 168 | 169 | ;; cat & repeat - a color string 170 | (-> (h/cat (h/val \#) 171 | (h/repeat 6 6 (h/enum (set "0123456789abcdefABCDEF"))))) 172 | ["#000000" "#af4Ea5"] 173 | ["000000" "#cafe" "#coffee"] 174 | 175 | ;; cat of cat, the inner cat is implicitly inlined 176 | (-> (h/cat (h/fn int?) 177 | (h/cat (h/fn int?))) 178 | (h/in-vector)) 179 | [[1 2]] 180 | [[1] [1 [2]] [1 2 3] '(1) '(1 2) '(1 (2)) '(1 2 3)] 181 | 182 | ;; cat of cat, the inner cat is explicitly not inlined 183 | (-> (h/cat (h/fn int?) 184 | (-> (h/cat (h/fn int?)) 185 | (h/not-inlined)))) 186 | [[1 [2]] '[1 (2)] '(1 (2))] 187 | [[1] [1 2] [1 [2] 3]] 188 | 189 | ;; repeat - no collection type specified 190 | (h/repeat 0 2 (h/fn int?)) 191 | [[] [1] [1 2] '() '(1) '(2 3)] 192 | [[1 2 3] '(1 2 3)] 193 | 194 | ;; repeat - inside a list 195 | (h/in-list (h/repeat 0 2 (h/fn int?))) 196 | ['() '(1) '(2 3)] 197 | [[] [1] [1 2] [1 2 3] '(1 2 3)] 198 | 199 | ;; repeat - inside a vector 200 | (h/in-vector (h/repeat 0 2 (h/fn int?))) 201 | [[] [1] [1 2]] 202 | [[1 2 3] '() '(1) '(2 3) '(1 2 3)] 203 | 204 | ;; repeat - inside a string 205 | (h/in-string (h/repeat 4 6 (h/fn char?))) 206 | ["hello"] 207 | ["" "hi" [] [1] '(1 2 3)] 208 | 209 | ;; repeat - min > 0 210 | (h/repeat 2 3 (h/fn int?)) 211 | [[1 2] [1 2 3]] 212 | [[] [1] [1 2 3 4]] 213 | 214 | ;; repeat - max = +Infinity 215 | (h/repeat 2 ##Inf (h/fn int?)) 216 | [[1 2] [1 2 3] [1 2 3 4]] 217 | [[] [1]] 218 | 219 | ;; repeat - of a cat 220 | (h/repeat 1 2 (h/cat (h/fn int?) 221 | (h/fn string?))) 222 | [[1 "a"] [1 "a" 2 "b"]] 223 | [[] [1] [1 2] [1 "a" 2 "b" 3 "c"]] 224 | 225 | ;; repeat - of a cat with :inlined false 226 | (h/repeat 1 2 (-> (h/cat (h/fn int?) 227 | (h/fn string?)) 228 | (h/not-inlined))) 229 | [[[1 "a"]] [[1 "a"] [2 "b"]] ['(1 "a") [2 "b"]]] 230 | [[] [1] [1 2] [1 "a"] [1 "a" 2 "b"] [1 "a" 2 "b" 3 "c"]] 231 | 232 | ;; char-cat & char-set 233 | (-> (h/cat (h/char-cat "good") 234 | (h/val \space) 235 | (h/alt (h/char-cat "morning") 236 | (h/char-cat "afternoon") 237 | (h/repeat 3 10 (h/char-set "#?!@_*+%")))) 238 | (h/in-string)) 239 | ["good morning" "good afternoon" "good #@*+?!"] 240 | ["good" "good " "good day"] 241 | 242 | ;; let / ref 243 | (h/let ['pos-even? (h/and (h/fn pos-int?) 244 | (h/fn even?))] 245 | (h/ref 'pos-even?)) 246 | [2 4] 247 | [-2 -1 0 1 3] 248 | 249 | ;; let / ref - with structural recursion 250 | (h/let ['hiccup (h/alt 251 | [:node (h/in-vector (h/cat (h/fn keyword?) 252 | (h/? (h/map)) 253 | (h/* (h/not-inlined (h/ref 'hiccup)))))] 254 | [:primitive (h/alt (h/fn nil?) 255 | (h/fn boolean?) 256 | (h/fn number?) 257 | (h/fn string?))])] 258 | (h/ref 'hiccup)) 259 | [nil 260 | false 261 | 1 262 | "hi" 263 | [:div] 264 | [:div {}] 265 | [:div "hei" [:p "bonjour"]] 266 | [:div {:a 1} "hei" [:p "bonjour"]]] 267 | [{} 268 | {:a 1} 269 | ['div] 270 | [:div {:a 1} "hei" [:p {} {} "bonjour"]]] 271 | 272 | ;; let / ref - with recursion within a sequence 273 | (h/let ['foo (h/cat (h/fn int?) 274 | (h/? (h/ref 'foo)) 275 | (h/fn string?))] 276 | (h/ref 'foo)) 277 | [[1 "hi"] 278 | [1 1 "hi" "hi"] 279 | [1 1 1 "hi" "hi" "hi"]] 280 | [[1 1 "hi"] 281 | [1 "hi" "hi"] 282 | [1 1 :no "hi" "hi"]] 283 | 284 | ; let / ref - with shadowed local model 285 | (h/let ['foo (h/ref 'bar) 286 | 'bar (h/fn int?)] 287 | (h/let ['bar (h/fn string?)] 288 | (h/ref 'foo))) 289 | [1] 290 | ["hi"]]] 291 | 292 | (doseq [[model valid-coll invalid-coll] (partition 3 test-data)] 293 | (doseq [data valid-coll] 294 | (is (valid? model data))) 295 | (doseq [data invalid-coll] 296 | (is (not (valid? model data)))))) 297 | 298 | (is (thrown? #?(:clj Exception :cljs js/Object) 299 | (valid? (h/let [] (h/ref 'foo)) 'bar)))) 300 | 301 | 302 | (deftest describe-test 303 | (let [test-data [;; fn 304 | (h/fn #(= 1 %)) 305 | [1 1 306 | 2 :invalid] 307 | 308 | (-> (h/fn int?) 309 | (h/with-condition (h/fn odd?))) 310 | [1 1 311 | 2 :invalid] 312 | 313 | (-> (h/fn symbol?) 314 | (h/with-condition (h/fn (complement #{'if 'val})))) 315 | ['a 'a 316 | 'if :invalid] 317 | 318 | ;; enum 319 | (h/enum #{1 "2" false nil}) 320 | [1 1 321 | "2" "2" 322 | false false 323 | nil nil 324 | true :invalid] 325 | 326 | ;; and 327 | (h/and (h/fn pos-int?) 328 | (h/fn even?)) 329 | [0 :invalid 330 | 1 :invalid 331 | 2 2 332 | 3 :invalid 333 | 4 4] 334 | 335 | ;; or 336 | (h/or (h/fn int?) 337 | (h/fn string?)) 338 | [1 1 339 | "a" "a" 340 | :a :invalid] 341 | 342 | ;; set 343 | (h/set-of (h/fn int?)) 344 | [#{1 2} [1 2]] 345 | 346 | ;; map 347 | (h/map [:a {:optional true} (h/fn int?)] 348 | [:b (h/or (h/fn int?) 349 | (h/fn string?))]) 350 | [{:a 1, :b 2} {:a 1, :b 2} 351 | {:a 1, :b "foo"} {:a 1, :b "foo"} 352 | {:a 1, :b [1 2]} :invalid 353 | ; missing optional entry 354 | {:b 2} {:b 2} 355 | ; missing entry 356 | {:a 1} :invalid 357 | ; extra entry 358 | {:a 1, :b 2, :c 3} {:a 1, :b 2}] 359 | 360 | ;; map-of - entry-model 361 | (h/map-of (h/vector (h/fn keyword?) (h/fn int?))) 362 | [{:a 1, :b 2} [[:a 1] [:b 2]] 363 | {"a" 1} :invalid] 364 | 365 | ;; map-of - real world use case 366 | (h/map-of (h/alt [:symbol (h/vector (h/fn simple-symbol?) (h/fn keyword?))] 367 | [:keys (h/vector (h/val :keys) (h/vector-of (h/fn symbol?)))] 368 | [:as (h/vector (h/val :as) (h/fn simple-symbol?))])) 369 | '[{first-name :first-name 370 | last-name :last-name 371 | :keys [foo bar] 372 | :as foobar} 373 | [[:symbol [first-name :first-name]] 374 | [:symbol [last-name :last-name]] 375 | [:keys [:keys [foo bar]]] 376 | [:as [:as foobar]]]] 377 | 378 | ;; sequence - :elements-model 379 | (h/sequence-of (h/fn int?)) 380 | [[1 2 3] [1 2 3] 381 | '(1 2 3) '(1 2 3) 382 | `(1 2 3) '(1 2 3) 383 | [1 "2" 3] :invalid] 384 | 385 | ;; sequence - :elements-model with condition 386 | (-> (h/sequence-of (h/fn int?)) 387 | (h/with-condition (h/fn (fn [coll] (= coll (reverse coll)))))) 388 | [[1 2 1] [1 2 1] 389 | '(1 2 3) :invalid] 390 | 391 | ;; sequence - :coll-type vector 392 | (h/vector-of (h/fn any?)) 393 | [[1 2 3] [1 2 3] 394 | '(1 2 3) :invalid 395 | `(1 2 3) :invalid] 396 | 397 | ;; sequence - :coll-type list 398 | (h/list-of (h/fn any?)) 399 | [[1 2 3] :invalid 400 | '(1 2 3) '(1 2 3) 401 | `(1 2 3) '(1 2 3)] 402 | 403 | ;; sequence - :entries 404 | (h/tuple (h/fn int?) (h/fn string?)) 405 | [[1 "a"] [1 "a"] 406 | [1 2] :invalid 407 | [1] :invalid] 408 | 409 | (h/tuple (h/fn int?) 410 | [:text (h/fn string?)]) 411 | [[1 "a"] {:text "a"}] 412 | 413 | (h/tuple [:number (h/fn int?)] 414 | [:text (h/fn string?)]) 415 | [[1 "a"] {:number 1, :text "a"}] 416 | 417 | ;; sequence - :count-model 418 | (-> (h/sequence-of (h/fn any?)) 419 | (h/with-count (h/val 3))) 420 | [[1 2] :invalid 421 | [1 2 3] [1 2 3] 422 | [1 2 3 4] :invalid 423 | "12" :invalid 424 | "123" (into [] "123") 425 | "1234" :invalid] 426 | 427 | ;; alt - not inside a sequence 428 | (h/alt [:number (h/fn int?)] 429 | [:sequence (h/vector-of (h/fn string?))]) 430 | [1 [:number 1] 431 | ["1"] [:sequence ["1"]] 432 | [1] :invalid 433 | "1" :invalid] 434 | 435 | ;; alt - inside a cat 436 | (h/cat (h/fn int?) 437 | (h/alt [:option1 (h/fn string?)] 438 | [:option2 (h/fn keyword?)] 439 | [:option3 (h/cat (h/fn string?) 440 | (h/fn keyword?))]) 441 | (h/fn int?)) 442 | [[1 "2" 3] [1 [:option1 "2"] 3] 443 | [1 :2 3] [1 [:option2 :2] 3] 444 | [1 "a" :b 3] [1 [:option3 ["a" :b]] 3] 445 | [1 ["a" :b] 3] :invalid] 446 | 447 | ;; alt - inside a cat, but with :inline false on its cat entry 448 | (h/cat (h/fn int?) 449 | (h/alt [:option1 (h/fn string?)] 450 | [:option2 (h/fn keyword?)] 451 | [:option3 (h/not-inlined (h/cat (h/fn string?) 452 | (h/fn keyword?)))]) 453 | (h/fn int?)) 454 | [[1 "2" 3] [1 [:option1 "2"] 3] 455 | [1 :2 3] [1 [:option2 :2] 3] 456 | [1 "a" :b 3] :invalid 457 | [1 ["a" :b] 3] [1 [:option3 ["a" :b]] 3]] 458 | 459 | ;; cat of cat, the inner cat is implicitly inlined 460 | (h/cat (h/fn int?) 461 | (h/cat (h/fn int?))) 462 | [[1 2] [1 [2]] 463 | [1] :invalid 464 | [1 [2]] :invalid 465 | [1 2 3] :invalid] 466 | 467 | ;; cat of cat, the inner cat is explicitly not inlined 468 | (h/cat (h/fn int?) 469 | (h/not-inlined (h/cat (h/fn int?)))) 470 | [[1 [2]] [1 [2]] 471 | [1 '(2)] [1 [2]] 472 | [1] :invalid 473 | [1 2] :invalid 474 | [1 [2] 3] :invalid] 475 | 476 | ;; repeat - no collection type specified 477 | (h/repeat 0 2 (h/fn int?)) 478 | [[] [] 479 | [1] [1] 480 | [1 2] [1 2] 481 | '() [] 482 | '(1) [1] 483 | '(2 3) [2 3] 484 | [1 2 3] :invalid 485 | '(1 2 3) :invalid] 486 | 487 | ;; repeat - inside a vector 488 | (-> (h/repeat 0 2 (h/fn int?)) 489 | (h/in-vector)) 490 | [[1] [1] 491 | '(1) :invalid] 492 | 493 | ;; repeat - inside a list 494 | (-> (h/repeat 0 2 (h/fn int?)) 495 | (h/in-list)) 496 | [[1] :invalid 497 | '(1) [1]] 498 | 499 | ;; repeat - min > 0 500 | (h/repeat 2 3 (h/fn int?)) 501 | [[] :invalid 502 | [1] :invalid 503 | [1 2] [1 2] 504 | [1 2 3] [1 2 3] 505 | [1 2 3 4] :invalid] 506 | 507 | ;; repeat - max = +Infinity 508 | (h/repeat 2 ##Inf (h/fn int?)) 509 | [[] :invalid 510 | [1] :invalid 511 | [1 2] [1 2] 512 | [1 2 3] [1 2 3]] 513 | 514 | ;; repeat - of a cat 515 | (h/repeat 1 2 (h/cat (h/fn int?) 516 | (h/fn string?))) 517 | [[1 "a"] [[1 "a"]] 518 | [1 "a" 2 "b"] [[1 "a"] [2 "b"]] 519 | [] :invalid 520 | [1] :invalid 521 | [1 2] :invalid 522 | [1 "a" 2 "b" 3 "c"] :invalid] 523 | 524 | ;; repeat - of a cat with :inlined false 525 | (h/repeat 1 2 (h/not-inlined (h/cat (h/fn int?) 526 | (h/fn string?)))) 527 | [[[1 "a"]] [[1 "a"]] 528 | [[1 "a"] [2 "b"]] [[1 "a"] [2 "b"]] 529 | ['(1 "a") [2 "b"]] [[1 "a"] [2 "b"]] 530 | [] :invalid 531 | [1] :invalid 532 | [1 2] :invalid 533 | [1 "a"] :invalid 534 | [1 "a" 2 "b"] :invalid 535 | [1 "a" 2 "b" 3 "c"] :invalid] 536 | 537 | ;; let / ref 538 | (h/let ['pos-even? (h/and (h/fn pos-int?) 539 | (h/fn even?))] 540 | (h/ref 'pos-even?)) 541 | [0 :invalid 542 | 1 :invalid 543 | 2 2 544 | 3 :invalid 545 | 4 4]]] 546 | 547 | (doseq [[model data-description-pairs] (partition 2 test-data)] 548 | (doseq [[data description] (partition 2 data-description-pairs)] 549 | (is (= [data (describe model data)] 550 | [data description]))))) 551 | 552 | (is (thrown? #?(:clj Exception :cljs js/Object) 553 | (describe (h/let [] (h/ref 'foo)) 'bar)))) 554 | -------------------------------------------------------------------------------- /test/minimallist/generator_test.cljc: -------------------------------------------------------------------------------- 1 | (ns minimallist.generator-test 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [clojure.test.check.generators :as tcg] 4 | [clojure.string :as str] 5 | [minimallist.core :refer [valid?]] 6 | [minimallist.helper :as h] 7 | [minimallist.util :as util] 8 | [minimallist.generator :as mg :refer [gen fn-any? fn-int? fn-string? fn-char? 9 | fn-symbol? fn-simple-symbol? fn-qualified-symbol? 10 | fn-keyword? fn-simple-keyword? fn-qualified-keyword?]])) 11 | 12 | (defn- path-test-visitor [] 13 | ;; Testing using side effects. 14 | ;; A little ugly, but good enough for tests. 15 | (let [paths (atom [])] 16 | (fn 17 | ([] @paths) 18 | ([model stack path] 19 | (swap! paths conj path) 20 | model)))) 21 | 22 | (deftest postwalk-visit-order-test 23 | (are [model expected-paths] 24 | (let [visitor (path-test-visitor)] 25 | (mg/postwalk model visitor) ; Create side effects 26 | (= (visitor) expected-paths)) ; Collect and compare the side effects 27 | 28 | (h/let ['leaf (h/fn int?) 29 | 'tree (h/ref 'leaf)] 30 | (h/ref 'tree)) 31 | [[:bindings 'leaf] 32 | [:bindings 'tree] 33 | [:body] 34 | []] 35 | 36 | (h/let ['root (h/let ['leaf (h/fn int?) 37 | 'tree (h/ref 'leaf)] 38 | (h/ref 'tree))] 39 | (h/ref 'root)) 40 | [[:bindings 'root :bindings 'leaf] 41 | [:bindings 'root :bindings 'tree] 42 | [:bindings 'root :body] 43 | [:bindings 'root] 44 | [:body] 45 | []] 46 | 47 | (h/let ['leaf (h/fn int?) 48 | 'root (h/let ['tree (h/ref 'leaf)] 49 | (h/ref 'tree))] 50 | (h/ref 'root)) 51 | [[:bindings 'leaf] 52 | [:bindings 'root :bindings 'tree] 53 | [:bindings 'root :body] 54 | [:bindings 'root] 55 | [:body] 56 | []] 57 | 58 | ; test of no visit more than once 59 | (h/let ['leaf (h/fn int?) 60 | 'tree (h/tuple (h/ref 'leaf) (h/ref 'leaf))] 61 | (h/ref 'tree)) 62 | [[:bindings 'leaf] 63 | [:bindings 'tree :entries 0 :model] 64 | [:bindings 'tree :entries 1 :model] 65 | [:bindings 'tree] 66 | [:body] 67 | []] 68 | 69 | ; test of no visit more than once, infinite loop otherwise 70 | (h/let ['leaf (h/fn int?) 71 | 'tree (h/tuple (h/ref 'tree) (h/ref 'leaf))] 72 | (h/ref 'tree)) 73 | [[:bindings 'tree :entries 0 :model] 74 | [:bindings 'leaf] 75 | [:bindings 'tree :entries 1 :model] 76 | [:bindings 'tree] 77 | [:body] 78 | []] 79 | 80 | #__)) 81 | 82 | (deftest assoc-leaf-distance-visitor-test 83 | (are [model expected-walked-model] 84 | (= (-> model 85 | (mg/postwalk mg/assoc-leaf-distance-visitor) 86 | (util/walk-map-dissoc :fn)) 87 | expected-walked-model) 88 | 89 | ; Recursive data-structure impossible to generate 90 | ; This one is trying to bring the generator function in an infinite loop. 91 | (h/let ['loop (h/ref 'loop)] 92 | (h/ref 'loop)) 93 | {:type :let 94 | :bindings {'loop {:type :ref 95 | :key 'loop}} 96 | :body {:type :ref 97 | :key 'loop}} 98 | 99 | ; Recursive data-structure impossible to generate 100 | (h/let ['leaf (h/fn int?) 101 | 'tree (h/tuple (h/ref 'tree) (h/ref 'leaf))] 102 | (h/ref 'tree)) 103 | {:type :let 104 | :bindings {'leaf {:type :fn 105 | ::mg/leaf-distance 0} 106 | 'tree {:type :sequence 107 | :entries [{:model {:type :ref 108 | :key 'tree}} 109 | {:model {:type :ref 110 | :key 'leaf 111 | ::mg/leaf-distance 1}}]}} 112 | :body {:type :ref 113 | :key 'tree}} 114 | 115 | ; Recursive data-structure impossible to generate 116 | (h/let ['rec-map (h/map [:a (h/fn int?)] 117 | [:b (h/ref 'rec-map)])] 118 | (h/ref 'rec-map)) 119 | {:type :let 120 | :bindings {'rec-map {:type :map 121 | :entries [{:key :a 122 | :model {:type :fn 123 | ::mg/leaf-distance 0}} 124 | {:key :b 125 | :model {:type :ref 126 | :key 'rec-map}}]}} 127 | :body {:type :ref 128 | :key 'rec-map}} 129 | 130 | ; Recursive data-structure which can be generated 131 | (h/let ['leaf (h/fn int?) 132 | 'tree (h/alt (h/ref 'tree) (h/ref 'leaf))] 133 | (h/ref 'tree)) 134 | {:type :let 135 | :bindings {'leaf {:type :fn 136 | ::mg/leaf-distance 0} 137 | 'tree {:type :alt 138 | :entries [{:model {:type :ref 139 | :key 'tree}} 140 | {:model {:type :ref 141 | :key 'leaf 142 | ::mg/leaf-distance 1}}] 143 | ::mg/leaf-distance 2}} 144 | :body {:type :ref 145 | :key 'tree 146 | ::mg/leaf-distance 3} 147 | ::mg/leaf-distance 4} 148 | 149 | (h/let ['rec-map (h/map [:a (h/fn int?)] 150 | [:b {:optional true} (h/ref 'rec-map)])] 151 | (h/ref 'rec-map)) 152 | {:type :let 153 | :bindings {'rec-map {:type :map 154 | :entries [{:key :a 155 | :model {:type :fn 156 | ::mg/leaf-distance 0}} 157 | {:key :b 158 | :optional true 159 | :model {:type :ref 160 | :key 'rec-map}}] 161 | ::mg/leaf-distance 1}} 162 | :body {:type :ref 163 | :key 'rec-map 164 | ::mg/leaf-distance 2} 165 | ::mg/leaf-distance 3} 166 | 167 | #__)) 168 | 169 | 170 | (deftest assoc-min-cost-visitor-test 171 | (are [model expected-walked-model] 172 | (= (-> model 173 | (mg/postwalk mg/assoc-min-cost-visitor) 174 | (util/walk-map-dissoc :fn)) 175 | expected-walked-model) 176 | 177 | (h/tuple (h/fn int?) (h/fn string?)) 178 | {:type :sequence 179 | :entries [{:model {:type :fn 180 | ::mg/min-cost 1}} 181 | {:model {:type :fn 182 | ::mg/min-cost 1}}] 183 | ::mg/min-cost 3} 184 | 185 | (h/cat (h/fn int?) (h/fn string?)) 186 | {:type :cat 187 | :entries [{:model {:type :fn 188 | ::mg/min-cost 1}} 189 | {:model {:type :fn 190 | ::mg/min-cost 1}}] 191 | ::mg/min-cost 3} 192 | 193 | (h/in-vector (h/cat (h/fn int?) (h/fn string?))) 194 | {:type :cat 195 | :coll-type :vector 196 | :entries [{:model {:type :fn 197 | ::mg/min-cost 1}} 198 | {:model {:type :fn 199 | ::mg/min-cost 1}}] 200 | ::mg/min-cost 3} 201 | 202 | (h/not-inlined (h/cat (h/fn int?) (h/fn string?))) 203 | {:type :cat 204 | :inlined false 205 | :entries [{:model {:type :fn 206 | ::mg/min-cost 1}} 207 | {:model {:type :fn 208 | ::mg/min-cost 1}}] 209 | ::mg/min-cost 3} 210 | 211 | (h/map [:a (h/fn int?)] 212 | [:b {:optional true} (h/fn int?)]) 213 | {:type :map 214 | :entries [{:key :a 215 | :model {:type :fn 216 | ::mg/min-cost 1}} 217 | {:key :b 218 | :optional true 219 | :model {:type :fn 220 | ::mg/min-cost 1}}] 221 | ::mg/min-cost 2} 222 | 223 | (h/map-of (h/vector (h/fn keyword?) (h/fn int?))) 224 | {:type :map-of 225 | :entry-model {:type :sequence 226 | :coll-type :vector 227 | :entries [{:model {:type :fn 228 | ::mg/min-cost 1}} 229 | {:model {:type :fn 230 | ::mg/min-cost 1}}] 231 | ::mg/min-cost 3} 232 | ::mg/min-cost 1} 233 | 234 | (-> (h/map-of (h/vector (h/fn keyword?) (h/fn int?))) 235 | (h/with-count (h/enum #{3 4}))) 236 | {:type :map-of 237 | :entry-model {:type :sequence 238 | :coll-type :vector 239 | :entries [{:model {:type :fn 240 | ::mg/min-cost 1}} 241 | {:model {:type :fn 242 | ::mg/min-cost 1}}] 243 | ::mg/min-cost 3} 244 | :count-model {:type :enum 245 | :values #{3 4}} 246 | ::mg/min-cost 7} 247 | 248 | (h/set-of (h/fn any?)) 249 | {:type :set-of 250 | :elements-model {:type :fn 251 | ::mg/min-cost 1} 252 | ::mg/min-cost 1} 253 | 254 | (-> (h/set-of (h/fn any?)) 255 | (h/with-count (h/val 3))) 256 | {:type :set-of 257 | :elements-model {:type :fn 258 | ::mg/min-cost 1} 259 | :count-model {:type :enum 260 | :values #{3}} 261 | ::mg/min-cost 4} 262 | 263 | (h/let ['foo (-> (h/set-of (h/fn int?)) 264 | (h/with-count (h/val 3)))] 265 | (h/ref 'foo)) 266 | {:type :let 267 | :bindings {'foo {:type :set-of 268 | :count-model {:type :enum 269 | :values #{3}} 270 | :elements-model {:type :fn 271 | ::mg/min-cost 1} 272 | ::mg/min-cost 4}} 273 | :body {:type :ref 274 | :key 'foo 275 | ::mg/min-cost 4} 276 | ::mg/min-cost 4} 277 | 278 | #__)) 279 | 280 | (deftest budget-split-gen-test 281 | (is (every? (fn [[a b c]] 282 | (and (<= 0 a 5) 283 | (<= 5 b 10) 284 | (<= 10 c 15))) 285 | (-> (#'mg/budget-split-gen 20.0 [0 5 10]) 286 | tcg/sample))) 287 | (is (every? #(= % [5 10 10]) 288 | (-> (#'mg/budget-split-gen 20.0 [5 10 10]) 289 | tcg/sample))) 290 | (is (every? empty? 291 | (-> (#'mg/budget-split-gen 10.0 []) 292 | tcg/sample)))) 293 | 294 | (comment 295 | ;; For occasional hand testing 296 | 297 | (tcg/sample (gen (-> (h/set-of fn-any?) 298 | (h/with-count (h/enum #{1 2 3 10})) 299 | (h/with-condition (h/fn (comp #{1 2 3} count)))))) 300 | 301 | (tcg/sample (gen (h/map-of (h/vector fn-int? fn-simple-symbol?)))) 302 | 303 | (tcg/sample (gen (-> (h/map [:a fn-int?]) 304 | (h/with-optional-entries [:b fn-string?])))) 305 | 306 | (tcg/sample (gen (h/sequence-of fn-int?))) 307 | 308 | (tcg/sample (gen (h/tuple fn-int? fn-string?))) 309 | 310 | (tcg/sample (gen (h/cat fn-int? fn-string?))) 311 | 312 | (tcg/sample (gen (h/repeat 2 3 fn-int?))) 313 | 314 | (tcg/sample (gen (h/repeat 2 3 (h/cat fn-int? fn-string?)))) 315 | 316 | (tcg/sample (gen (h/let ['int? fn-int? 317 | 'string? fn-string? 318 | 'int-string? (h/cat (h/ref 'int?) (h/ref 'string?))] 319 | (h/repeat 2 3 (h/ref 'int-string?))))) 320 | 321 | (tcg/sample (gen (-> (h/set-of fn-int?) 322 | (h/with-condition (h/fn (fn [coll] 323 | (or (empty? coll) 324 | (some even? coll)))))))) 325 | 326 | (tcg/sample (gen (-> (h/set-of fn-any?) 327 | (h/with-count (h/enum #{1 2 3 10})) 328 | (h/with-condition (h/fn (comp #{1 2 3} count)))))) 329 | 330 | (tcg/sample (gen (h/let ['node (h/set-of (h/ref 'node))] 331 | (h/ref 'node)))) 332 | 333 | (tcg/sample (gen (h/let ['node (h/map-of (h/vector fn-int? (h/ref 'node)))] 334 | (h/ref 'node)) 50)) 335 | 336 | (tcg/sample (gen (h/let ['node (h/map-of (h/vector fn-keyword? (h/ref 'node)))] 337 | (h/ref 'node)) 100) 1) 338 | 339 | (tcg/sample (gen (h/map [:a fn-int?]))) 340 | 341 | (tcg/sample (gen (-> (h/map [:a fn-int?]) 342 | (h/with-optional-entries [:b fn-string?])))) 343 | 344 | (tcg/sample (gen (h/cat (h/vector-of fn-int?) 345 | (h/vector-of fn-int?)) 20)) 346 | 347 | (tcg/sample (gen (h/repeat 5 10 fn-int?))) 348 | 349 | (tcg/sample (gen fn-symbol?)) 350 | (tcg/sample (gen fn-simple-symbol?)) 351 | (tcg/sample (gen fn-qualified-symbol?)) 352 | 353 | (tcg/sample (gen fn-keyword?)) 354 | (tcg/sample (gen fn-simple-keyword?)) 355 | (tcg/sample (gen fn-qualified-keyword?)) 356 | 357 | (tcg/sample (gen (-> (h/cat (h/char-cat "good") 358 | (h/val \space) 359 | (h/alt (h/char-cat "morning") 360 | (h/char-cat "afternoon") 361 | (h/repeat 3 10 (h/char-set "#?!@_*+%")))) 362 | (h/in-string))) 363 | 100) 364 | 365 | 366 | #__) 367 | 368 | (deftest gen-test 369 | (let [model fn-string?] 370 | (is (every? (partial valid? model) 371 | (tcg/sample (gen model))))) 372 | 373 | (let [model (h/enum #{:1 2 "3"})] 374 | (is (every? (partial valid? model) 375 | (tcg/sample (gen model))))) 376 | 377 | (let [model (-> (h/set-of fn-int?) 378 | (h/with-condition (h/fn (fn [coll] 379 | (or (empty? coll) 380 | (some even? coll))))))] 381 | (is (every? (partial valid? model) 382 | (tcg/sample (gen model))))) 383 | 384 | (let [model (-> (h/set-of fn-any?) 385 | (h/with-count (h/enum #{1 2 3 10})) 386 | (h/with-condition (h/fn (comp #{1 2 3} count))))] 387 | (is (every? (partial valid? model) 388 | (tcg/sample (gen model))))) 389 | 390 | (let [model (h/map-of (h/vector fn-int? fn-string?))] 391 | (is (every? (partial valid? model) 392 | (tcg/sample (gen model))))) 393 | 394 | (let [model (-> (h/map [:a fn-int?]) 395 | (h/with-optional-entries [:b fn-string?]) 396 | (h/with-entries [:c fn-int?]) 397 | (h/with-optional-entries [:d fn-string?])) 398 | sample (tcg/sample (gen model) 100)] 399 | (is (and (every? (partial valid? model) sample) 400 | (every? (fn [element] (contains? element :a)) sample) 401 | (some (fn [element] (contains? element :b)) sample) 402 | (some (fn [element] (not (contains? element :b))) sample) 403 | (every? (fn [element] (contains? element :c)) sample) 404 | (some (fn [element] (contains? element :d)) sample) 405 | (some (fn [element] (not (contains? element :d))) sample)))) 406 | 407 | (let [model (h/sequence-of fn-int?)] 408 | (is (every? (partial valid? model) 409 | (tcg/sample (gen model))))) 410 | 411 | (let [model (h/tuple fn-int? fn-string?) 412 | sample (tcg/sample (gen model) 100)] 413 | (is (and (every? (partial valid? model) sample) 414 | (some list? sample) 415 | (some vector? sample)))) 416 | 417 | (let [model (h/list fn-int? fn-string?) 418 | sample (tcg/sample (gen model))] 419 | (is (and (every? (partial valid? model) sample) 420 | (every? list? sample)))) 421 | 422 | (let [model (h/vector fn-int? fn-string?) 423 | sample (tcg/sample (gen model))] 424 | (is (and (every? (partial valid? model) sample) 425 | (every? vector? sample)))) 426 | 427 | (let [model (h/string-tuple fn-char? fn-char?) 428 | sample (tcg/sample (gen model))] 429 | (is (and (every? (partial valid? model) sample) 430 | (every? string? sample)))) 431 | 432 | (let [model (h/in-list (h/cat fn-int? fn-string?)) 433 | sample (tcg/sample (gen model))] 434 | (is (and (every? (partial valid? model) sample) 435 | (every? list? sample)))) 436 | 437 | (let [model (h/in-vector (h/cat fn-int? fn-string?)) 438 | sample (tcg/sample (gen model))] 439 | (is (and (every? (partial valid? model) sample) 440 | (every? vector? sample)))) 441 | 442 | (let [model (h/in-string (h/cat fn-char? fn-char?)) 443 | sample (tcg/sample (gen model))] 444 | (is (and (every? (partial valid? model) sample) 445 | (every? string? sample)))) 446 | 447 | (let [model (h/alt fn-int? fn-string?)] 448 | (is (every? (partial valid? model) 449 | (tcg/sample (gen model))))) 450 | 451 | (let [model (h/cat fn-int? fn-string?)] 452 | (is (every? (partial valid? model) 453 | (tcg/sample (gen model))))) 454 | 455 | (let [model (h/repeat 2 3 fn-int?)] 456 | (is (every? (partial valid? model) 457 | (tcg/sample (gen model))))) 458 | 459 | (let [model (h/repeat 2 3 (h/cat fn-int? fn-string?))] 460 | (is (every? (partial valid? model) 461 | (tcg/sample (gen model))))) 462 | 463 | (let [model (h/not-inlined (h/cat fn-int?))] 464 | (is (every? (partial valid? model) 465 | (tcg/sample (gen model))))) 466 | 467 | (let [model (h/not-inlined (h/repeat 1 2 fn-int?))] 468 | (is (every? (partial valid? model) 469 | (tcg/sample (gen model))))) 470 | 471 | (let [model (h/let ['int? fn-int? 472 | 'string? fn-string? 473 | 'int-string? (h/cat (h/ref 'int?) (h/ref 'string?))] 474 | (h/repeat 2 3 (h/ref 'int-string?)))] 475 | (is (every? (partial valid? model) 476 | (tcg/sample (gen model))))) 477 | 478 | ;; Budget-based limit on model choice. 479 | (let [model (h/let ['tree (h/alt [:leaf fn-int?] 480 | [:branch (h/vector (h/ref 'tree) 481 | (h/ref 'tree))])] 482 | (h/ref 'tree))] 483 | (is (every? (partial valid? model) 484 | (tcg/sample (gen model))))) 485 | 486 | ;; Budget-based limit on variable set size. 487 | (let [model (h/let ['node (h/set-of (h/ref 'node))] 488 | (h/ref 'node))] 489 | (is (every? (partial valid? model) 490 | (tcg/sample (gen model))))) 491 | 492 | ;; Budget-based limit on variable sequence size. 493 | (let [model (h/let ['node (h/vector-of (h/ref 'node))] 494 | (h/ref 'node))] 495 | (is (every? (partial valid? model) 496 | (tcg/sample (gen model))))) 497 | 498 | ;; Budget-based limit on variable map size. 499 | (let [model (h/let ['node (h/map-of (h/vector fn-int? (h/ref 'node)))] 500 | (h/ref 'node))] 501 | (is (every? (partial valid? model) 502 | (tcg/sample (gen model))))) 503 | 504 | ;; Budget-based limit on optional entries in a map. 505 | (let [model (h/let ['node (-> (h/map [:a fn-int?]) 506 | (h/with-optional-entries [:x (h/ref 'node)] 507 | [:y (h/ref 'node)] 508 | [:z (h/ref 'node)]))] 509 | (h/ref 'node))] 510 | (is (every? (partial valid? model) 511 | (tcg/sample (gen model))))) 512 | 513 | ;;; Budget-based limit on number of occurrences in a repeat. 514 | ;(let [model (h/let ['node (h/repeat 0 1 (h/ref 'node))] 515 | ; (h/ref 'node))] 516 | ; (is (every? (partial valid? model) 517 | ; (tcg/sample (gen model))))) 518 | 519 | ;; Model impossible to generate. 520 | (let [model (h/let ['node (h/map [:a (h/ref 'node)])] 521 | (h/ref 'node))] 522 | (is (thrown? #?(:clj Exception :cljs js/Object) (tcg/sample (gen model))))) 523 | 524 | ;; Model impossible to generate. 525 | (let [model (h/let ['node (h/tuple (h/ref 'node))] 526 | (h/ref 'node))] 527 | (is (thrown? #?(:clj Exception :cljs js/Object) (tcg/sample (gen model))))) 528 | 529 | ;; Model impossible to generate. 530 | (let [model (h/let ['node (h/cat (h/ref 'node))] 531 | (h/ref 'node))] 532 | (is (thrown? #?(:clj Exception :cljs js/Object) (tcg/sample (gen model))))) 533 | 534 | ;; Model impossible to generate. 535 | (let [model (h/let ['node (h/cat (h/ref 'node))] 536 | (h/ref 'node))] 537 | (is (thrown? #?(:clj Exception :cljs js/Object) (tcg/sample (gen model))))) 538 | 539 | (let [model (h/let ['node (h/repeat 1 2 (h/ref 'node))] 540 | (h/ref 'node))] 541 | (is (thrown? #?(:clj Exception :cljs js/Object) (tcg/sample (gen model)))))) 542 | 543 | ;; TODO: [later] reuse the cat-ipsum model for parsing the output. 544 | 545 | ;; TODO: in the :alt node, introduce a property :occurrence for the generator. 546 | ;; TODO: generate models, use them to generate data, should not stack overflow. 547 | -------------------------------------------------------------------------------- /test/minimallist/util_test.cljc: -------------------------------------------------------------------------------- 1 | (ns minimallist.util-test 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [minimallist.util :as util] 4 | [minimallist.helper :as h])) 5 | 6 | (deftest reduce-update-test 7 | (let [m {:a 1 8 | :b 5} 9 | f (fn [acc elm] 10 | (let [elm10 (* elm 10)] 11 | [(conj acc elm10) elm10]))] 12 | (is (= (-> [[] m] 13 | (util/reduce-update :a f) 14 | (util/reduce-update :b f)) 15 | [[10 50] {:a 10, :b 50}])))) 16 | 17 | (deftest reduce-update-in-test 18 | (let [m {:a {:x 1, :y 2} 19 | :b [3 4 5]} 20 | f (fn [acc elm] 21 | (let [elm10 (* elm 10)] 22 | [(conj acc elm10) elm10]))] 23 | (is (= (-> [[] m] 24 | (util/reduce-update-in [:a :x] f) 25 | (util/reduce-update-in [:b 2] f)) 26 | [[10 50] {:a {:x 10, :y 2}, :b [3 4 50]}])))) 27 | 28 | (deftest reduce-mapv 29 | (let [m {:a {:x 1, :y 2} 30 | :b [3 4 5]} 31 | f (fn [acc elm] 32 | (let [elm10 (* elm 10)] 33 | [(conj acc elm10) elm10]))] 34 | (is (= (util/reduce-update [[] m] :b (partial util/reduce-mapv f)) 35 | [[30 40 50] {:a {:x 1, :y 2}, :b [30 40 50]}])))) 36 | 37 | (deftest iterate-while-different-test 38 | (let [inc-up-to-10 (fn [x] (cond-> x (< x 10) inc))] 39 | (is (= (util/iterate-while-different inc-up-to-10 0 0) 0)) 40 | (is (= (util/iterate-while-different inc-up-to-10 0 5) 5)) 41 | (is (= (util/iterate-while-different inc-up-to-10 0 10) 10)) 42 | (is (= (util/iterate-while-different inc-up-to-10 0 15) 10)) 43 | 44 | (is (= (util/iterate-while-different inc-up-to-10 7 2) 9)) 45 | (is (= (util/iterate-while-different inc-up-to-10 7 3) 10)) 46 | (is (= (util/iterate-while-different inc-up-to-10 7 4) 10)) 47 | 48 | (is (= (util/iterate-while-different inc-up-to-10 12 0) 12)) 49 | (is (= (util/iterate-while-different inc-up-to-10 12 3) 12)) 50 | 51 | (is (= (util/iterate-while-different inc-up-to-10 0 ##Inf) 10)) 52 | (is (= (util/iterate-while-different inc-up-to-10 10 ##Inf) 10)) 53 | (is (= (util/iterate-while-different inc-up-to-10 15 ##Inf) 15)))) 54 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :unit 3 | :type :kaocha.type/clojure.test} 4 | {:id :unit-cljs 5 | :type :kaocha.type/cljs}]} 6 | --------------------------------------------------------------------------------