{"id":13839979,"url":"https://github.com/bhauman/spell-spec","last_synced_at":"2025-04-05T11:12:06.889Z","repository":{"id":62431759,"uuid":"135056078","full_name":"bhauman/spell-spec","owner":"bhauman","description":"clojure.spec.alpha helpers that check for misspelled map keys, with expound integration","archived":false,"fork":false,"pushed_at":"2020-05-11T15:48:54.000Z","size":40,"stargazers_count":135,"open_issues_count":1,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-29T10:09:48.767Z","etag":null,"topics":["clojure","clojure-spec","clojurescript","validation"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bhauman.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-05-27T14:57:07.000Z","updated_at":"2025-03-23T14:54:38.000Z","dependencies_parsed_at":"2022-11-01T21:00:54.364Z","dependency_job_id":null,"html_url":"https://github.com/bhauman/spell-spec","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bhauman%2Fspell-spec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bhauman%2Fspell-spec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bhauman%2Fspell-spec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bhauman%2Fspell-spec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bhauman","download_url":"https://codeload.github.com/bhauman/spell-spec/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247325693,"owners_count":20920714,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojure-spec","clojurescript","validation"],"created_at":"2024-08-04T17:00:39.687Z","updated_at":"2025-04-05T11:12:01.876Z","avatar_url":"https://github.com/bhauman.png","language":"Clojure","funding_links":[],"categories":["Libraries"],"sub_categories":[],"readme":"# spell-spec\n\n[![Clojars Project](https://img.shields.io/clojars/v/com.bhauman/spell-spec.svg)](https://clojars.org/com.bhauman/spell-spec)\n\n\u003cimg src=\"https://s3.amazonaws.com/bhauman-blog-images/misspelled-key-error.png\"/\u003e\n\n\u003cimg src=\"https://s3.amazonaws.com/bhauman-blog-images/unknown-key-error.png\"/\u003e\n\n`spell-spec` is a Clojure/Script library that provides additional spec\nmacros that have the same signature as `clojure.spec.alpha/keys`\nmacro. `spell-spec` macros will also verify that unspecified map keys are\nnot misspellings of specified map keys. `spell-spec` also provides\n[expound](https://github.com/bhb/expound) integration for nicely\nformatted results.\n\nIf you are unfamiliar with\n[Clojure Spec](https://clojure.org/guides/spec) you can learn more\nfrom the official [guide to Clojure Spec](https://clojure.org/guides/spec).\n\nExample Specs and output:\n\n```clojure\n(s/explain \n  (spell-spec.alpha/keys :opt-un [::hello ::there]) \n  {:there 1 :helloo 1})\n;; In: [:helloo 0] val: :helloo fails at: [0] predicate: (not-misspelled #{:hello :there})\n;; \t :expound.spec.problem/type  :spell-spec.alpha/misspelled-key\n;; \t :spell-spec.alpha/misspelled-key  :helloo\n;; \t :spell-spec.alpha/likely-misspelling-of  :hello\n```\n\nDesigned to work well with [expound](https://github.com/bhb/expound):\n\n```clojure\n(expound/expound \n  (spell-spec.alpha/keys :opt-un [::hello ::there]) \n  {:there 1 :helloo 1})\n;; -- Misspelled map key -------------\n;;\n;;   {:there ..., :helloo ...}\n;;                ^^^^^^^\n;;\n;; should be spelled\n;;\n;;   :hello\n;;\n;; -------------------------\n;; Detected 1 error\n```\n\nMaps remain open for keys that aren't similar to the specified keys.\n\n```clojure\n(s/valid? \n  (spell-spec.alpha/keys :opt-un [::hello ::there]) \n  {:there 1 :hello 1 :barbara 1})\n=\u003e true\n```\n\nAlso provides warnings instead of spec failures by binding\n`spell-spec.alpha/*warn-only*` to `true`\n\n```clojure\n(binding [spell-spec.alpha/*warn-only* true]\n  (s/valid? \n    (spell-spec.alpha/keys :opt-un [::hello ::there]) \n    {:there 1 :helloo 1}))\n;; \u003c\u003c printed to *err* \u003e\u003e\n;; SPEC WARNING: possible misspelled map key :helloo should probably be :hello in {:there 1, :helloo 1}\n=\u003e true\n```\n\nor calling `spell-spec.alpha/warn-keys`\n\n```clojure\n(s/valid?\n  (spell-spec.alpha/warn-keys :opt-un [::hello ::there]) \n  {:there 1 :helloo 1})\n;; \u003c\u003c printed to *err* \u003e\u003e\n;; SPEC WARNING: possible misspelled map key :helloo should probably be :hello in {:there 1, :helloo 1}\n=\u003e true\n```\n\n## Why?\n\nIn certain situations there is a need to provide user feedback for\nmiss-typed map keys. This is true for tool configuration and possibly\nany external API where users are repeatedly stung by single character\nmishaps. `spell-spec` can provide valuable feedback for these\nsituations.\n\nThis library is an evolution of the library\n[strictly-specking](https://github.com/bhauman/strictly-specking),\nwhich I wrote to validate the complex configuration of\n[figwheel](https://github.com/bhauman/lein-figwheel).\n\nWhen I originally wrote\n[strictly-specking](https://github.com/bhauman/strictly-specking), I\nreally wanted to push the limits of what could be done to provide\nfeedback for configuration errors. As a result the code in\n`strictly-specking` is very complex and tailored to the problem domain\nof configuration specification for a tool like\n[figwheel](https://github.com/bhauman/lein-figwheel).\n\nWhen used with expound, `spell-spec` is a **good enough** approach\nwhich will provide good feedback for a much broader set of use\ncases. I am planning on using this approach instead of\n[strictly-specking](https://github.com/bhauman/strictly-specking) from\nnow on.\n\n`spell-spec` is much lighter as it has no dependencies other than\nof `clojure.spec` itself.\n\n## Usage\n\nAdd `spell-spec` as a dependency in your project config.\n\nFor **leiningen** in your `project.clj` `:dependencies` add:\n\n```clojure\n:dependencies [[com.bhauman/spell-spec \"0.1.1\"]\n               ;; if you want to use expound\n               [expound \"0.7.0\"]]\n```\n\nFor **clojure cli tools** in your `deps.edn` `:deps` key add:\n\n```clojure\n{:deps {com.bhauman/spell-spec {:mvn/version \"0.1.1\"}\n        ;; if you want to use expound\n        expound {:mvn/version \"0.7.0\"}}}\n```\n\n## Using with Expound\n\n`spell-spec` does not declare `expound` as a dependency and does not\nautomatically register its expound helpers.\n\nIf you want to use the `spell-spec`\n[expound](https://github.com/bhb/expound) integration, then after\n`expound.alpha` has been required you will need to require\n`spell-spec.expound` to register the expound helpers. You will want to\ndo this before you validate any `spell-spec` defined specs.\n\n### `spell-spec.alpha/keys`\n\n`keys` is likely the macro that you will use most often when using\n`spell-spec`.\n\nUse `spell-spec.alpha/keys` the same way that you would use\n`clojure.spec.alpha/keys` keeping in mind that the spec it creates\nwill fail for keys that are misspelled.\n\n`spell-spec.alpha/keys` is a spec macro that has the same signature\nand behavior as `clojure.spec.alpha/keys`. In addition to performing\nthe same checks that `clojure.spec.alpha/keys` does, it checks to see\nif there are unknown keys present which are also close misspellings of\nthe specified keys.\n\nAn important aspect of this behavior is that the map is left open to\nother keys that are not close misspellings of the specified\nkeys. Keeping maps open is an important pattern in Clojure which\nallows one to simply add behavior to a program by adding extra data to\nmaps that flow through functions. `spell-spec.alpha/keys` keeps this\nin mind and is fairly conservative in its spelling checks.\n\nAn example of using:\n\n```clojure\n(require '[clojure.spec.alpha :as s])\n(require '[spell-spec.alpha :as spell])\n\n(s/def ::name string?)\n(s/def ::use-history boolean?)\n\n(s/def ::config (spell/keys :opt-un [::name ::use-history]))\n\n(s/valid? ::config {:name \"John\" :use-hisory false :countr 1})\n;; =\u003e false\n\n(s/explain ::config {:name \"John\" :use-hisory false :countr 1})\n;; In: [:use-hisory 0] val: :use-hisory fails at: [0] predicate: (not-misspelled #{:name :use-history})\n;; \t :expound.spec.problem/type  :spell-spec.alpha/misspelled-key\n;; \t :spell-spec.alpha/misspelled-key  :use-hisory\n;; \t :spell-spec.alpha/likely-misspelling-of  :use-history\n\n;; to use with expound must first require expound\n(require '[expound.alpha :refer [expound]])\n\n;; and then the optional spell-spec expound helpers\n(require 'spell-spec.expound)\n\n(expound ::config {:name \"John\" :use-hisory false :countr 1})\n;; -- Misspelled map key -------------\n;;\n;;   {:name ..., :use-hisory ..., :counter ...}\n;;               ^^^^^^^^^^^\n;;\n;; should be spelled\n;;\n;;   :use-history\n;;\n;; -------------------------\n;; Detected 1 error\n```\n\n### `spell-spec.alpha/strict-keys`\n\n`strict-keys` is very similar to `spell-spec.alpha/keys` except that\nthe map is closed to keys that are not specified.\n\n`strict-keys` will produce two types of validation problems: one for\n**misspelled keys** and one for **unknown keys**.\n\n\u003e I really debated about whether I should add `strict-keys` to the\n\u003e library as it violates the Clojure idiom of keeping maps\n\u003e open. However, there are some situations where this behavior is\n\u003e warranted. I strongly advocate for the use of `spell-spec.alpha/keys`\n\u003e over `strict-keys`  ... don't say I didn't warn you.\n\nExample (continuation of the example session above):\n\n```clojure\n(s/def ::strict-config (spell/strict-keys :opt-un [::name ::use-history]))\n\n(s/valid? ::strict-config {:name \"John\" :use-hisory false :countr 1})\n;; =\u003e false\n\n(s/explain ::strict-config {:name \"John\" :use-hisory false :countr 1})\n;; In: [:use-hisory 0] val: :use-hisory fails at: [0] predicate: #{:name :use-history}\n;;   :expound.spec.problem/type  :spell-spec.alpha/misspelled-key\n;; \t :spell-spec.alpha/misspelled-key  :use-hisory\n;; \t :spell-spec.alpha/likely-misspelling-of  :use-history\n;; In: [:countr 0] val: :countr fails at: [0] predicate: #{:name :use-history}\n;; \t :expound.spec.problem/type  :spell-spec.alpha/unknown-key\n;;\t :spell-spec.alpha/unknown-key  :countr\n\n(s/expound ::strict-config {:name \"John\" :use-hisory false :countr 1})\n;; -- Misspelled map key -------------\n;;\n;;   {:name ..., :countr ..., :use-hisory ...}\n;;                            ^^^^^^^^^^^\n;;\n;; should be spelled\n;;\n;;   :use-history\n;;\n;; -- Unknown map key ----------------\n;;\n;;   {:name ..., :use-hisory ..., :countr ...}\n;;                                ^^^^^^^\n;;\n;; should be one of\n;;\n;;   :name, :use-history\n;;\n;; -------------------------\n;; Detected 2 errors\n```\n\n## Warnings only\n\nOne way to keep maps completely open is to simply warn when keys are\nmisspelled or unknown, helpful feedback is still provided but the spec\ndoesn't fail when these anomalies are detected.\n\nSpecs defined by `spell-spec.alpha/keys` and `spell-spec.alpha/strict-keys`\nwill issue warnings instead of failing when one binds\n`spell-spec.alpha/*warn-only*` to `true` around the calls that verify\nthe specs.\n\nOne can also use the following substitutions to get warnings instead of failures:\n\n* use `spell-spec.alpha/warn-keys` for `spell-spec.alpha/keys`\n* use `spell-spec.alpha/warn-strict-keys` for `spell-spec.alpha/strict-keys`\n\n## Handling warnings\n\nBy default warnings are printed to `clojure.core/*err*`. One can\ncontrol how `spell-spec` warnings are reported by binding\n`spell-spec.alpha/*warning-handler*` to a function of one argument.\n\nExample (continuing):\n\n```clojure\n(s/def ::warn-config (spell/warn-strict-keys :opt-un [::name ::use-history]))\n\n(binding [spell/*warning-handler* clojure.pprint/pprint]\n  (s/valid? ::warn-config {:name \"John\" :use-hisory false :countr 1}))\n;; \u003c\u003c prints out \u003e\u003e\n;; {:path [0],\n;;  :pred #{:name :use-history},\n;;  :val :use-hisory,\n;;  :via [],\n;;  :in [:use-hisory 0],\n;;  :expound.spec.problem/type :spell-spec.alpha/misspelled-key,\n;;  :spell-spec.alpha/misspelled-key :use-hisory,\n;;  :spell-spec.alpha/likely-misspelling-of :use-history,\n;;  :spell-spec.alpha/warning-message\n;;  \"possible misspelled map key :use-hisory should probably be :use-history in {:name \\\"John\\\", :use-hisory false, :countr 1}\"\n;;  :spell-spec.alpha/value {:name \"John\", :use-hisory false, :countr 1}}\n;; {:path [0],\n;;  :pred #{:name :use-history},\n;;  :val :countr,\n;;  :via [],\n;;  :in [:countr 0],\n;;  :expound.spec.problem/type :spell-spec.alpha/unknown-key,\n;;  :spell-spec.alpha/unknown-key :countr,\n;;  :spell-spec.alpha/warning-message\n;;  \"unknown map key :countr in {:name \\\"John\\\", :use-hisory false, :countr 1}\"\n;;  :spell-spec.alpha/value {:name \"John\", :use-hisory false, :countr 1}}\n;; =\u003e true\n```\n\n## Changing the threshold that detects misspelling \n\nA misspelling is detected when an unknown map key is within a certain\n`levenshtein` distance from a specified map key. If the size of this\ndistance is too big then the number of false positives goes up.\n\nYou can override the default behavior by binding the\n`spell-spec.alpha/*length-\u003ethreshold*` to a function that takes one\nargument, the length of the shortest keyword (of two compared\nkeywords) and returns an integer which is the threshold for the\nlevenshtein distance.\n\nExample (continuing):\n\n```clojure\n(s/def ::namer (spell/keys :opt-un [::name]))\n\n;; :namee one character off from :name an thus a detected misspelling\n;; with a threshold of 1\n(binding [spell/*length-\u003ethreshold* (fn [_] 1)]\n  (s/valid? ::namer {:namee \"John\"}))\n;; =\u003e false\n\n;; :nameee is two characters off from :name an thus an un-detected misspelling\n;; with a threshold of 1\n(binding [spell/*length-\u003ethreshold* (fn [_] 1)]\n  (s/valid? ::namer {:nameee \"John\"})) \n;; =\u003e true\n\n;; with a threshold of 2 we can detect both of the above misspellings\n(binding [spell/*length-\u003ethreshold* (fn [_] 2)]\n  (s/valid? ::namer {:namee \"John\"}))\n;; =\u003e false\n(binding [spell/*length-\u003ethreshold* (fn [_] 2)]\n  (s/valid? ::namer {:nameee \"John\"})) \n;; =\u003e false\n```\n\n## License\n\nCopyright © 2018 Bruce Hauman\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbhauman%2Fspell-spec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbhauman%2Fspell-spec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbhauman%2Fspell-spec/lists"}