Giter VIP home page Giter VIP logo

spell-spec's Introduction

spell-spec

Clojars Project

spell-spec is a Clojure/Script library that provides additional spec macros that have the same signature as clojure.spec.alpha/keys macro. spell-spec macros will also verify that unspecified map keys are not misspellings of specified map keys. spell-spec also provides expound integration for nicely formatted results.

If you are unfamiliar with Clojure Spec you can learn more from the official guide to Clojure Spec.

Example Specs and output:

(s/explain 
  (spell-spec.alpha/keys :opt-un [::hello ::there]) 
  {:there 1 :helloo 1})
;; In: [:helloo 0] val: :helloo fails at: [0] predicate: (not-misspelled #{:hello :there})
;; 	 :expound.spec.problem/type  :spell-spec.alpha/misspelled-key
;; 	 :spell-spec.alpha/misspelled-key  :helloo
;; 	 :spell-spec.alpha/likely-misspelling-of  :hello

Designed to work well with expound:

(expound/expound 
  (spell-spec.alpha/keys :opt-un [::hello ::there]) 
  {:there 1 :helloo 1})
;; -- Misspelled map key -------------
;;
;;   {:there ..., :helloo ...}
;;                ^^^^^^^
;;
;; should be spelled
;;
;;   :hello
;;
;; -------------------------
;; Detected 1 error

Maps remain open for keys that aren't similar to the specified keys.

(s/valid? 
  (spell-spec.alpha/keys :opt-un [::hello ::there]) 
  {:there 1 :hello 1 :barbara 1})
=> true

Also provides warnings instead of spec failures by binding spell-spec.alpha/*warn-only* to true

(binding [spell-spec.alpha/*warn-only* true]
  (s/valid? 
    (spell-spec.alpha/keys :opt-un [::hello ::there]) 
    {:there 1 :helloo 1}))
;; << printed to *err* >>
;; SPEC WARNING: possible misspelled map key :helloo should probably be :hello in {:there 1, :helloo 1}
=> true

or calling spell-spec.alpha/warn-keys

(s/valid?
  (spell-spec.alpha/warn-keys :opt-un [::hello ::there]) 
  {:there 1 :helloo 1})
;; << printed to *err* >>
;; SPEC WARNING: possible misspelled map key :helloo should probably be :hello in {:there 1, :helloo 1}
=> true

Why?

In certain situations there is a need to provide user feedback for miss-typed map keys. This is true for tool configuration and possibly any external API where users are repeatedly stung by single character mishaps. spell-spec can provide valuable feedback for these situations.

This library is an evolution of the library strictly-specking, which I wrote to validate the complex configuration of figwheel.

When I originally wrote strictly-specking, I really wanted to push the limits of what could be done to provide feedback for configuration errors. As a result the code in strictly-specking is very complex and tailored to the problem domain of configuration specification for a tool like figwheel.

When used with expound, spell-spec is a good enough approach which will provide good feedback for a much broader set of use cases. I am planning on using this approach instead of strictly-specking from now on.

spell-spec is much lighter as it has no dependencies other than of clojure.spec itself.

Usage

Add spell-spec as a dependency in your project config.

For leiningen in your project.clj :dependencies add:

:dependencies [[com.bhauman/spell-spec "0.1.1"]
               ;; if you want to use expound
               [expound "0.7.0"]]

For clojure cli tools in your deps.edn :deps key add:

{:deps {com.bhauman/spell-spec {:mvn/version "0.1.1"}
        ;; if you want to use expound
        expound {:mvn/version "0.7.0"}}}

Using with Expound

spell-spec does not declare expound as a dependency and does not automatically register its expound helpers.

If you want to use the spell-spec expound integration, then after expound.alpha has been required you will need to require spell-spec.expound to register the expound helpers. You will want to do this before you validate any spell-spec defined specs.

spell-spec.alpha/keys

keys is likely the macro that you will use most often when using spell-spec.

Use spell-spec.alpha/keys the same way that you would use clojure.spec.alpha/keys keeping in mind that the spec it creates will fail for keys that are misspelled.

spell-spec.alpha/keys is a spec macro that has the same signature and behavior as clojure.spec.alpha/keys. In addition to performing the same checks that clojure.spec.alpha/keys does, it checks to see if there are unknown keys present which are also close misspellings of the specified keys.

An important aspect of this behavior is that the map is left open to other keys that are not close misspellings of the specified keys. Keeping maps open is an important pattern in Clojure which allows one to simply add behavior to a program by adding extra data to maps that flow through functions. spell-spec.alpha/keys keeps this in mind and is fairly conservative in its spelling checks.

An example of using:

(require '[clojure.spec.alpha :as s])
(require '[spell-spec.alpha :as spell])

(s/def ::name string?)
(s/def ::use-history boolean?)

(s/def ::config (spell/keys :opt-un [::name ::use-history]))

(s/valid? ::config {:name "John" :use-hisory false :countr 1})
;; => false

(s/explain ::config {:name "John" :use-hisory false :countr 1})
;; In: [:use-hisory 0] val: :use-hisory fails at: [0] predicate: (not-misspelled #{:name :use-history})
;; 	 :expound.spec.problem/type  :spell-spec.alpha/misspelled-key
;; 	 :spell-spec.alpha/misspelled-key  :use-hisory
;; 	 :spell-spec.alpha/likely-misspelling-of  :use-history

;; to use with expound must first require expound
(require '[expound.alpha :refer [expound]])

;; and then the optional spell-spec expound helpers
(require 'spell-spec.expound)

(expound ::config {:name "John" :use-hisory false :countr 1})
;; -- Misspelled map key -------------
;;
;;   {:name ..., :use-hisory ..., :counter ...}
;;               ^^^^^^^^^^^
;;
;; should be spelled
;;
;;   :use-history
;;
;; -------------------------
;; Detected 1 error

spell-spec.alpha/strict-keys

strict-keys is very similar to spell-spec.alpha/keys except that the map is closed to keys that are not specified.

strict-keys will produce two types of validation problems: one for misspelled keys and one for unknown keys.

I really debated about whether I should add strict-keys to the library as it violates the Clojure idiom of keeping maps open. However, there are some situations where this behavior is warranted. I strongly advocate for the use of spell-spec.alpha/keys over strict-keys ... don't say I didn't warn you.

Example (continuation of the example session above):

(s/def ::strict-config (spell/strict-keys :opt-un [::name ::use-history]))

(s/valid? ::strict-config {:name "John" :use-hisory false :countr 1})
;; => false

(s/explain ::strict-config {:name "John" :use-hisory false :countr 1})
;; In: [:use-hisory 0] val: :use-hisory fails at: [0] predicate: #{:name :use-history}
;;   :expound.spec.problem/type  :spell-spec.alpha/misspelled-key
;; 	 :spell-spec.alpha/misspelled-key  :use-hisory
;; 	 :spell-spec.alpha/likely-misspelling-of  :use-history
;; In: [:countr 0] val: :countr fails at: [0] predicate: #{:name :use-history}
;; 	 :expound.spec.problem/type  :spell-spec.alpha/unknown-key
;;	 :spell-spec.alpha/unknown-key  :countr

(s/expound ::strict-config {:name "John" :use-hisory false :countr 1})
;; -- Misspelled map key -------------
;;
;;   {:name ..., :countr ..., :use-hisory ...}
;;                            ^^^^^^^^^^^
;;
;; should be spelled
;;
;;   :use-history
;;
;; -- Unknown map key ----------------
;;
;;   {:name ..., :use-hisory ..., :countr ...}
;;                                ^^^^^^^
;;
;; should be one of
;;
;;   :name, :use-history
;;
;; -------------------------
;; Detected 2 errors

Warnings only

One way to keep maps completely open is to simply warn when keys are misspelled or unknown, helpful feedback is still provided but the spec doesn't fail when these anomalies are detected.

Specs defined by spell-spec.alpha/keys and spell-spec.alpha/strict-keys will issue warnings instead of failing when one binds spell-spec.alpha/*warn-only* to true around the calls that verify the specs.

One can also use the following substitutions to get warnings instead of failures:

  • use spell-spec.alpha/warn-keys for spell-spec.alpha/keys
  • use spell-spec.alpha/warn-strict-keys for spell-spec.alpha/strict-keys

Handling warnings

By default warnings are printed to clojure.core/*err*. One can control how spell-spec warnings are reported by binding spell-spec.alpha/*warning-handler* to a function of one argument.

Example (continuing):

(s/def ::warn-config (spell/warn-strict-keys :opt-un [::name ::use-history]))

(binding [spell/*warning-handler* clojure.pprint/pprint]
  (s/valid? ::warn-config {:name "John" :use-hisory false :countr 1}))
;; << prints out >>
;; {:path [0],
;;  :pred #{:name :use-history},
;;  :val :use-hisory,
;;  :via [],
;;  :in [:use-hisory 0],
;;  :expound.spec.problem/type :spell-spec.alpha/misspelled-key,
;;  :spell-spec.alpha/misspelled-key :use-hisory,
;;  :spell-spec.alpha/likely-misspelling-of :use-history,
;;  :spell-spec.alpha/warning-message
;;  "possible misspelled map key :use-hisory should probably be :use-history in {:name \"John\", :use-hisory false, :countr 1}"
;;  :spell-spec.alpha/value {:name "John", :use-hisory false, :countr 1}}
;; {:path [0],
;;  :pred #{:name :use-history},
;;  :val :countr,
;;  :via [],
;;  :in [:countr 0],
;;  :expound.spec.problem/type :spell-spec.alpha/unknown-key,
;;  :spell-spec.alpha/unknown-key :countr,
;;  :spell-spec.alpha/warning-message
;;  "unknown map key :countr in {:name \"John\", :use-hisory false, :countr 1}"
;;  :spell-spec.alpha/value {:name "John", :use-hisory false, :countr 1}}
;; => true

Changing the threshold that detects misspelling

A misspelling is detected when an unknown map key is within a certain levenshtein distance from a specified map key. If the size of this distance is too big then the number of false positives goes up.

You can override the default behavior by binding the spell-spec.alpha/*length->threshold* to a function that takes one argument, the length of the shortest keyword (of two compared keywords) and returns an integer which is the threshold for the levenshtein distance.

Example (continuing):

(s/def ::namer (spell/keys :opt-un [::name]))

;; :namee one character off from :name an thus a detected misspelling
;; with a threshold of 1
(binding [spell/*length->threshold* (fn [_] 1)]
  (s/valid? ::namer {:namee "John"}))
;; => false

;; :nameee is two characters off from :name an thus an un-detected misspelling
;; with a threshold of 1
(binding [spell/*length->threshold* (fn [_] 1)]
  (s/valid? ::namer {:nameee "John"})) 
;; => true

;; with a threshold of 2 we can detect both of the above misspellings
(binding [spell/*length->threshold* (fn [_] 2)]
  (s/valid? ::namer {:namee "John"}))
;; => false
(binding [spell/*length->threshold* (fn [_] 2)]
  (s/valid? ::namer {:nameee "John"})) 
;; => false

License

Copyright © 2018 Bruce Hauman

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

spell-spec's People

Contributors

akond avatar bhauman avatar djebbz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

spell-spec's Issues

`s/form` return unqualified symbols

(s/form (spell-spec.alpha/strict-keys :req-un [::a]))
; => (keys :req-un [:user/a])

=>

(s/form (spell-spec.alpha/strict-keys :req-un [::a]))
; => (clojure.spec.alpha/keys :req-un [:user/a])

in line 160 the call should be to s/describe* to fix this. I could do a PR if those are welcome.

Performance issue of generators with spec.alpha/merge

I've been trying to debug some performance issues with my tests and have identified spell-spec as an unexpected culprit. The issue is definitely noticeable when generating examples with spec.alpha/merge, but I have not investigated the root cause.

Test case:

(tufte/add-basic-println-handler! {})

(s/def ::foo pos-int?)
(s/def ::bar neg-int?)
(s/def ::baz string?)
(s/def ::qux keyword?)

(deftest speed-difference-test
  (let [spec1 (s/merge (s/keys :req [::foo])
                       (s/keys :req [::bar])
                       (s/keys :req [::baz])
                       (s/keys :req [::qux]))
        spec2 (s/merge (spell-spec/keys :req [::foo])
                       (spell-spec/keys :req [::bar])
                       (spell-spec/keys :req [::baz])
                       (spell-spec/keys :req [::qux]))]
    (tufte/profile
     {}
     (tufte/p :spec (doall (take 100 (gen/sample-seq (s/gen (s/spec spec1))))))
     (tufte/p :spell (doall (take 100 (gen/sample-seq (s/gen (s/spec spec2)))))))))

Result:

      pId     nCalls        Min      50% ≤      90% ≤      95% ≤      99% ≤        Max       Mean   MAD       Total   Clock

   :spell          1   909.46ms   909.46ms   909.46ms   909.46ms   909.46ms   909.46ms   909.46ms   ±0%    909.46ms     97%
    :spec          1    27.39ms    27.39ms    27.39ms    27.39ms    27.39ms    27.39ms    27.39ms   ±0%     27.39ms      3%

Tested with:

[com.bhauman/spell-spec        "0.1.1"]
[org.clojure/spec.alpha        "0.1.143"]
[org.clojure/clojure           "1.9.0"]

Problem using generators with spell-spec specs

I tried dropping in spell-spec's keys macro as a replacement for spec/keys for some maps that are user provided.

I have some tests that are exercising the functions that take or return these kinds of maps, and those test broke when using spell-spec.

Here is an example:

user=> (require '[clojure.spec.alpha :as s])
nil
user=> (require '[spell-spec.alpha :as spell])
nil
user=> (s/def ::baz string?)
:user/baz
user=> (s/def ::foo (s/keys :req-un [::baz]))
:user/foo
user=> (s/def ::bar (spell/keys :req-un [::baz]))
:user/bar
user=> (s/gen ::foo)
#clojure.test.check.generators.Generator{:gen #object[clojure.test.check.generators$such_that$fn__6411 0x4a2929a4 "clojure.test.check.generators$such_that$fn__6411@4a2929a4"]}
user=> (s/gen ::bar)
ExceptionInfo Unable to construct gen at: [0] for: clojure.lang.LazySeq@fa039170  clojure.core/ex-info (core.clj:4739)
user=> 

Giving generators explicitly works around the problem:

user=> (s/def ::bar (s/with-gen (spell/keys :req-un [::baz])
  #_=>        #(s/gen (s/keys :req-un [::baz]))))
:user/bar
user=>(s/gen ::bar)
#clojure.test.check.generators.Generator{:gen #object[clojure.test.check.generators$such_that$fn__6411 0x7132a9dc "clojure.test.check.generators$such_that$fn__6411@7132a9dc"]}
user=> 

but it quickly gets a bit verbose.

`s/merge` does not work with `spell/strict-keys`

I noticed that the clojure.spec.alpha/merge does not work as I would expect with the spell-spec.alpha/strict-keys. The following example shows the case.

(s/def ::strict-keys
  (s/merge
   (spell/strict-keys :opt-un [::foo])
   (spell/strict-keys :opt-un [::bar])))

(s/valid? ::strict-keys {:foo "foo" :bar "bar"})
;; => false

Whereas, I would expect to work similar to this

(s/def ::normal-keys
  (s/merge
   (s/keys :req-un [::foo])
   (s/keys :req-un [::bar])))

(s/valid? ::normal-keys {:foo "foo" :bar "bar"})
;; => true

The results of s/explain-data for the former are the following

#:clojure.spec.alpha{:problems
                     ({:path [0],
                       :pred #{:foo},
                       :val :bar,
                       :via [:bsq.vittle-assistant.spec/strict-keys],
                       :in [:bar 0],
                       :expound.spec.problem/type
                       :spell-spec.alpha/unknown-key,
                       :spell-spec.alpha/unknown-key :bar}
                      {:path [0],
                       :pred #{:bar},
                       :val :foo,
                       :via [:bsq.vittle-assistant.spec/strict-keys],
                       :in [:foo 0],
                       :expound.spec.problem/type
                       :spell-spec.alpha/unknown-key,
                       :spell-spec.alpha/unknown-key :foo}),
                     :spec :bsq.vittle-assistant.spec/strict-keys,
                     :value {:foo "foo", :bar "bar"}}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.