Giter VIP home page Giter VIP logo

bouncer's Introduction

bouncer Build Status Buy Me a Coffee at ko-fi.com

A validation DSL for Clojure & Clojurescript applications

Table of Contents

Motivation

Check this blog post where I explain in detail the motivation behind this library

Setup

Stable release:

Clojars Project

Development release:

[bouncer "1.0.1"]

Check out the CHANGELOG to see what's new.

Then, require the library:

(ns some.ns
  (:require [bouncer.core :as b]
            [bouncer.validators :as v]))

bouncer provides two main validation functions, validate and valid?

valid? is a convenience function built on top of validate:

(b/valid? {:name nil}
    :name v/required)

;; false

validate takes a map and one or more validation forms and returns a vector.

The first element in this vector contains a map of the error messages, whereas the second element contains the original map, augmented with the error messages.

Let's look at a few examples:

Usage

Basic validations

Below is an example where we're validating that a given map has a value for both the keys :name and :age.

(def person {:name "Leo"})

(b/validate person
    :name v/required
    :age  v/required)

;; [{:age ("age must be present")}
;;  {:name "Leo", :bouncer.core/errors {:age ("age must be present")}}]

As you can see, since age is missing, it's listed in the errors map with the appropriate error messages.

Error messages can be customized by providing a :message option - e.g: in case you need them internationalized:

(b/validate person
    :age [[v/required :message "Idade é um atributo obrigatório"]])

;; [{:age ("Idade é um atributo obrigatório")}
;;  {:name "Leo", :bouncer.core/errors {:age ("Idade é um atributo obrigatório")}}]

Note the double vector:

  • the inner one wraps a single validation where the first element is the validating function and the rest are options for that validation.
  • the outer vector simply denotes a list of validations to be applied

Below is an example of date-time validation. A clj-time formatter may optionally be supplied.

(ns some.ns
  (:require [bouncer.core :as b]
            [bouncer.validators :as v]
            [clj-time.format :as f])) ;; cljs-time for clojurescript users

(def person {:name "Jeb" :last-login "2014-10-21 18:00:00"})

(b/validate person
    :name v/required
    :last-login v/datetime (:mysql f/formatters))

;; [nil {:name "Jeb" :last-login "2014-10-21 18:00:00"}]

Validating nested maps

Nested maps can easily be validated as well, using the built-in validators:

(def person-1
    {:address
        {:street nil
         :country "Brazil"
         :postcode "invalid"
         :phone "foobar"}})

(b/validate person-1
    [:address :street]   v/required
    [:address :postcode] v/number
    [:address :phone]    [[v/matches #"^\d+$"]])


;;[{:address
;;              {:phone ("phone must match the given regex pattern"),
;;               :postcode ("postcode must be a number"),
;;               :street ("street must be present")}}
;;   {:bouncer.core/errors {:address {
;;                          :phone ("phone must match the given regex pattern"),
;;                          :postcode ("postcode must be a number"),
;;                          :street ("street must be present")}},
;;                          :address {:country "Brazil", :postcode "invalid", :street nil,
;;                          :phone "foobar"}}]

In the example above, the vector of keys is assumed to be the path in an associative structure.

Multiple validation errors

bouncer features a short circuit mechanism for multiple validations within a single field.

For instance, say you're validating a map representing a person and you expect the key :age to be required, a number and also be positive:

(b/validate {:age nil}
    :age [v/required v/number v/positive])

;; [{:age ("age must be present")} {:bouncer.core/errors {:age ("age must be present")}, :age nil}]

As you can see, only the required validator was executed. That's what I meant by the short circuit mechanism. As soon as a validation fails, it exits and returns that error, skipping further validators.

However, note this is true within a single map entry. Multiple map entries will have all its messages returned as expected:

(b/validate person-1
    [:address :street] v/required
    [:address :postcode] [v/number v/positive])

;; [{:address {:postcode ("postcode must be a number"), :street ("street must be present")}} {:bouncer.core/errors {:address {:postcode ("postcode must be a number"), :street ("street must be present")}}, :address {:country "Brazil", :postcode "invalid", :street nil, :phone "foobar"}}]

Also note that if we need multiple validations against any keyword or path, we need only provide them inside a vector, like [v/number v/positive] above.

Validating collections

Sometimes it's useful to perform simple, ad-hoc checks in collections contained within a map. For that purpose, bouncer provides every.

Its usage is similar to the validators seen so far. This time however, the value in the given key/path must be a collection (vector, list etc...)

Let's see it in action:

(def person-with-pets {:name "Leo"
                       :pets [{:name nil}
                              {:name "Gandalf"}]})

(b/validate person-with-pets
          :pets [[v/every #(not (nil? (:name %)))]])

;;[{:pets ("All items in pets must satisfy the predicate")}
;; {:name "Leo", :pets [{:name nil} {:name "Gandalf"}],
;; :bouncer.core/errors {:pets ("All items in pets must satisfy the predicate")}}]

All we need to do is provide a predicate function to every. It will be invoked for every item in the collection, making sure they all pass.

Validation threading

Note that if a map is threaded through multiple validators, bouncer will leave it's errors map untouched and simply add new validation errors to it:

(-> {:age "NaN"}
    (b/validate :name v/required)
    second
    (b/validate :age v/number)
    second
    ::b/errors)

;; {:age ("age must be a number"), :name ("name must be present")}

Pre-conditions

Validators can take a pre-condition option :pre that causes it to be executed only if the given pre-condition - a truthy function - is met.

Consider the following:

(b/valid? {:a -1 :b "X"}
           :b [[v/member #{"Y" "Z"} :pre (comp pos? :a)]])

;; true

As you can see the value of b is clearly not in the set #{"Y" "Z"}, however the whole validation passes because the v/member check states is should only be run if :a is positive.

Let's now make it fail:

(b/valid? {:a 1 :b "X"}
           :b [[v/member #{"Y" "Z"} :pre (comp pos? :a)]])

;; false

Validator sets

If you find yourself repeating a set of validators over and over, chances are you will want to group and compose them somehow. Validator sets are simply plain Clojure maps:

;; first we define the set of validators we want to use
(def address-validations
  {:postcode [v/required v/number]
   :street    v/required
   :country   v/required})

;;just something to validate
(def person {:address {
                :postcode ""
                :country "Brazil"}})

;;now we compose the validators
(b/validate person
            :name    v/required
            :address address-validations)

;;[{:address
;;    {:postcode ("postcode must be a number" "postcode must be present"),
;;     :street ("street must be present")},
;;     :name ("name must be present")}
;;
;; {:bouncer.core/errors {:address {:postcode ("postcode must be a number" "postcode must be present"),
;;  :street ("street must be present")}, :name ("name must be present")},
;;  :address {:country "Brazil", :postcode ""}}]

You can also compose validator sets together and use them as top level validations:

(def address-validator
  {:postcode v/required})

(def person-validator
  {:name v/required
   :age [v/required v/number]
   :address address-validator})

(b/validate {}
			person-validator)

;;[{:address {:postcode ("postcode must be present")}, :age ("age must be present"), :name ("name must be present")} {:bouncer.core/errors {:address {:postcode ("postcode must be present")}, :age ("age must be present"), :name ("name must be present")}}]

Customization Support

Custom validations using plain functions

Using your own functions as validators is simple:

(defn young? [age]
    (< age 25))

(b/validate {:age 29}
            :age [[young? :message "Too old!"]])

;; [{:age ("Too old!")}
;;  {:bouncer.core/errors {:age ("Too old!")}, :age 29}]

Writing validators

As shown above, validators as just functions. The downside is that by using a function bouncer will default to a validation message that might not make sense in a given scenario:

(b/validate {:age 29}
               :age young?)

;; [{:age ("Custom validation failed for age")}
;; {:bouncer.core/errors {:age ("Custom validation failed for age")}, :age 29}]

You could of course use the message keyword as in previous examples but if you reuse the validation in several places, you'd need a lot of copying and pasting.

Another way - and the preferred one - to provide custom validations is to use the macro defvalidator in the bouncer.validators namespace.

The advantage of this approach is that it attaches the needed metadata for bouncer to know which message to use.

As an example, here's a simplified version of the bouncer.validators/number validator:

(use '[bouncer.validators :only [defvalidator]])

(defvalidator my-number-validator
  {:default-message-format "%s must be a number"}
  [maybe-a-number]
  (number? maybe-a-number))

defvalidator takes your validator name, an optional map of options and the body of your predicate function.

Options is a map of key/value pairs where:

  • :default-message-format - to be used when clients of this validator don't provide one
  • :optional - a boolean indicating if this validator should only trigger for keys that have a value different than nil. Defaults to true.

That's all syntactic sugar for:

(def my-number-validator
  (with-meta (fn my-number-validator
               ([maybe-a-number]
                  (number? maybe-a-number)))
    {:default-message-format "%s must be a number", :optional false}))

Using it is then straightforward:

(b/validate {:postcode "NaN"}
          :postcode my-number-validator)


;; [{:postcode ("postcode must be a number")}
;;  {:bouncer.core/errors {:postcode ("postcode must be a number")}, :postcode "NaN"}]

As you'd expect, the message can be customized as well:

(b/validate {:postcode "NaN"}
          :postcode [[my-number-validator :message "must be a number"]])

Validators and arbitrary number of arguments

Your validators aren't limited to a single argument though.

Since v0.2.2, defvalidator takes an arbitrary number of arguments. The only thing you need to be aware of is that the value being validated will always be the first argument you list - this applies if you're using plain functions too. Let's see an example with the member validator:

(defvalidator member
  [value coll]
  (some #{value} coll))

Yup, it's that simple. Let's use it:

(def kid {:age 10})

(b/validate kid
            :age [[member (range 5)]])

In the example above, the validator will be called with 10 - that's the value the key :age holds - and (0 1 2 3 4) - which is the result of (range 5) and will be fed as the second argument to the validator.

Internationalization and customised error messages

In some cases the default behaviour might not be enough.

Perhaps you'd like to customise your error messages with the value that has been provided.

Or maybe you need access to the options that were given to a specific validator.

And what if you need to know which validator generated a specific error message?

Since 0.3.1-beta1, this is possible. validate takes as an optional first argument a function - called a message-fn - that is applied to a map containing validation metadata that allows you to customise error messages in any way you like - or even return other data structures instead.

This map has the following keys:

  • :path - where in the map has the error ocurred?
  • :value - what was the value at the time of the validation?
  • :args - which arguments - if any - were passed to the validator?
  • :metadata - what is the metadata associated with this validation?
  • :message - what - if any - is the message passed to this validator instance?

Let's see how this works in practice. For the first example, we'll simply use identity as our message-fn:

(def person {:name "Leo" :age "NaN"})

(b/validate identity
            person
            :name v/required
            :age  v/number)

As you can see we simply supply identity as the first argument. This is what gets returned:

[{:age
  ({:path [:age],
    :value "NaN",
    :args nil,
    :metadata
    {:optional true,
     :default-message-format "%s must be a number",
     :validator :bouncer.validators/number},
    :message nil})}
 {:age "NaN",
  :name "Leo",
  :bouncer.core/errors
  {:age
   ({:path [:age],
     :value "NaN",
     :args nil,
     :metadata
     {:optional true,
      :default-message-format "%s must be a number",
      :validator :bouncer.validators/number},
     :message nil})}}]

Contrast this with what the function would have returned by default:

[{:age ("age must be a number")}
 {:age "NaN",
  :name "Leo",
  :bouncer.core/errors {:age ("age must be a number")}}]

This opens a number of possibilities around customising errors messages. Let's create a simple message-fn to illustrate:

(defn custom-message-fn [{:keys [path value metadata]}]
  (format "'%s' in field %s should be a %s" value path (:validator metadata)))

(b/validate custom-message-fn
            person
            :name v/required
            :age  v/number)

This time we get a much more informative message:

[{:age ("'NaN' in field [:age] should be a :bouncer.validators/number")}
 {:age "NaN",
  :name "Leo",
  :bouncer.core/errors
  {:age
   ("NaN in field [:age] should be a :bouncer.validators/number")}}]

Hats off to @dm3 for this pull request.

Built-in validations

I didn't spend a whole lot of time on bouncer so it only ships with the validations I've needed myself. At the moment they live in the validators namespace:

  • bouncer.validators/required

  • bouncer.validators/number

  • bouncer.validators/integer

  • bouncer.validators/string

  • bouncer.validators/boolean

  • bouncer.validators/email

  • bouncer.validators/positive

  • bouncer.validators/in-range

  • bouncer.validators/member

  • bouncer.validators/max-count

  • bouncer.validators/min-count

  • bouncer.validators/matches (for matching regular expressions)

  • bouncer.validators/every (for ad-hoc validation of collections. All items must match the provided predicate)

  • bouncer.validators/datetime (uses clj-time formatters)

Contributing

Pull requests of bug fixes and new validators are most welcome.

Note that if you wish your validator to be merged and considered built-in you must implement it using the macro defvalidator shown above.

Feedback to both this library and this guide is welcome.

Running the tests

Bouncer is assumed to work with Clojure 1.4 and up, as well as ClojureScript.

There is a leiningen alias that makes it easy to run the tests against multiple Clojure versions:

λ lein all-tests

It'll run all tests against Clojure 1.4, 1.5 and 1.6, as well as Clojurescript - make sure all tests pass before submitting a pull request.

TODO

  • Add more validators (help is appreciated here)
  • Docs are getting a bit messy. Fix that.

License

Copyright © 2012-2019 Leonardo Borges

Distributed under the MIT License.

bouncer's People

Contributors

caioguedes avatar edgibbs avatar fdiotalevi avatar ghoseb avatar jebberjeb avatar mishadoff avatar mrijk avatar silasdavis avatar theleoborges avatar yogthos 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bouncer's Issues

Release a new version

Hi,

I like the change with all validators being optional by default. Could you please release a new version so that we can use it?

Thanks!

There should be a way to avoid validation for optional keys

(b/validate contact-details                                                                                                      
                           "email" [[v/required :message "Email is required"]]                                                                      
                           "phone_number"  [[v/matches  #"^\d{10}$" :message "Phone number should be a numeric value of 10 digit."]])

Now email is required and phone number is optional but in case phone number is present I need to verify its pattern.

Now with the regular flow, if I don't add required to the phone number field then it checks the nil value to match with the pattern.

There should be a way in the DSL to avoid a validator if the data is not present otherwise one has to write custom validators with extra check and can't re-use existing in built validators.

Suggestion: if a validator's name starts with a special character then it can be considered optional ?

Version ranges found for:

lein deps
WARNING!!! version ranges found for:
[bouncer "0.3.2-SNAPSHOT"] -> [com.keminglabs/cljx "0.4.0"] -> [org.clojars.trptcolin/sjacket "0.1.0.6"] -> [org.clojure/clojure "[1.3.0,)"]
Consider using [bouncer "0.3.2-20141118.091354-2" :exclusions [org.clojure/clojure]].
[bouncer "0.3.2-SNAPSHOT"] -> [com.keminglabs/cljx "0.4.0"] -> [org.clojars.trptcolin/sjacket "0.1.0.6"] -> [net.cgrand/regex "1.1.0"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [bouncer "0.3.2-20141118.091354-2" :exclusions [org.clojure/clojure]].
[bouncer "0.3.2-SNAPSHOT"] -> [com.keminglabs/cljx "0.4.0"] -> [org.clojars.trptcolin/sjacket "0.1.0.6"] -> [net.cgrand/parsley "0.9.1"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [bouncer "0.3.2-20141118.091354-2" :exclusions [org.clojure/clojure]].
[bouncer "0.3.2-SNAPSHOT"] -> [com.keminglabs/cljx "0.4.0"] -> [org.clojars.trptcolin/sjacket "0.1.0.6"] -> [net.cgrand/parsley "0.9.1"] -> [net.cgrand/regex "1.1.0"] -> [org.clojure/clojure "[1.2.0,)"]
Consider using [bouncer "0.3.2-20141118.091354-2" :exclusions [org.clojure/clojure]].

Date related validators ?

Hey Leonardo,
I have been using custom validators for a lot of date related checks, but I feel something like date is so common that you could add a few inbuilt validators to bouncer itself that do checks like date format for strings, or compare dates. something that works well with clj-time aka Joda Utils.

Multi-field validation

Currently if I want to validate that :from is less than :to in a map I have to write a custom validator on the following lines:

(def subject {:from 20 :to 10})

(defn less-than [max]
  (fn [value]
    (< value max)))

(b/validate subject
            :from [[v/required (less-than (:to subject))]]
            :to   [[v/required]])

I hadn't implemented this feature so far as I didn't come up with an API I was happy with. After some thought I have some ideas that would allow the previous example to be written as:

(b/validate subject
            :from [[v/required]]
            :to   [[v/required]]
            [:from :to] [[< :message "From should be less than to"]])

This would be flexible enough to allow for other usages. For example, password confirmation could be validated as such:

(b/validate subject
            :password     [[v/required]]
            :confirmation [[v/required :pre (comp not nil? :password)]]
            [:password :confirmation] [[= :message "Passwords should match."]])

Validator functions used in multi-field validations need an arity equal to the length of keywords in the vector preceding the validators.

How does this API look to you?

:pre functions do not get nested values

When nesting validation sets, functions supplied as :pre get the entire data structure passed to b/validate, not just the nested structure that they would normally get. This basically breaks their functionality.

Here's a more descriptive example:

(def billing-data
  {:country v/required
   :eu-vat-number [[v/required
                    :pre (comp countries/eu-country-codes :country)
                    :message "is required for EU countries"]]})

(def signup-data
  {:currency v/required
   :billing-data billing-data
   :account-data account-data
   ;; [...]
   })

In this example, :eu-vat-number validation will work if a billing-data map is being validated, but will not work as expected if signup-data is validated. The :pre function will get the entire signup-data map, which does not contain a :country key, therefore the :eu-vat-number validation will never run.

feature: remove pairs that validation do not handle

Hi @atlassian, I think it would be great to add feature that remove pairs that validation do not handle. eg:

(def person {:name "Leo" :age 15})

(b/validate person
    :name v/required
    :age  v/required)

this can pass, but

(def person {:name "Leo" :age 15 :user_id 1})

(b/validate person
    :name v/required
    :age  v/required)

will return error, or just return

 [nil  {:name "Leo", :bouncer.core/errors {:age ("age must be present")}}
 ;; add a map that only validation pass,eg:
 ;; {:name "Leo" :age 15 }]

As usual scene after validation , we just merge the map to some origin map from database. Right now ,we have to check every pair in map to ensure no extra unsafe pairs.

Please consider this feature, thanks !

Problem with some use case

Hi Leonardo
I just started using the library and I am really liking. Its very clean and fits like a glove. I had a problem with a few cases and just wanted to know what I am doing wrong.

(b/validate {} :name [v/required v/number])
; works

(def myvalidators [v/required v/number])
(b/validate {} :name myvalidators)
; gives error. here I am trying to create a list of validators for re-use

(defn myvalfunc [data & validators](when-let
[errors %28first %28apply b/validate data validators%29%29]
%28throw %28Exception.))))

(myvalfunc {} :name [v/required v/number])
; gives error - here I am trying to create a function which would remove some boiler plate code for me

Any idea what I could be doing wrong here.

Thanks
Anand

Add assertion functionality for pre/post conditions.

What would you think about adding functionality like:

(ns example
  (:require [clojure.pprint :refer [pprint]]
            [clojure.string :refer [join]]
            [bouncer.core :refer [validate]]
            [bouncer.validators :refer [required number]]))

(defn assert-valid
  "Throws an AssertionError if m is not valid with a validation error
  as its message."
  [m & forms]
  (when *assert*
    (let [errors (first (apply validate m forms))]
      (when (seq errors)
        (->> errors
             (reduce-kv (fn [acc k error]
                          (assoc acc k {:value (get m k) :error error}))
                        {})
             pprint
             with-out-str
             (vector "Validation failed with the following errors:")
             (join \newline)
             AssertionError.
             throw)))))

(assert-valid {:foo "hi" :bar 1}
  :foo [number]
  :bar [number]
  :baz [required])
;; AssertionError Validation failed with the following errors:
;; {:foo {:value "hi", :error ("foo must be a number")},
;;  :baz {:value nil, :error ("baz must be present")}}
;;   example/assert-valid (example.clj:20)

This would be useful in :pre and :post conditions instead of valid?, since valid? gives no visibility into how/why it returned false.

Return a seq/list of validation errors from a validation function

Is it possible to have a validator function that returns a list of its errors/error messages at all?

Currently, I'm calling (b/validate) with a fairly deep data structure applying several validation functions against it - functions such as "model doesn't contain duplicate entries", it looks as tho the validation function can only return a boolean "yes I passed, no I failed", and the metadata can provide a default message.

Is there a way to return a custom message from the validation execution, such as "no, I failed with these specific instances..." ( either as a single string, or a list of strings )?

Cheers

namespace-qualify meta keys

For the next major release, I suggest to change the :validator, :default-message-format and :optional meta keys to their namespace-qualified counterparts :bouncer.validators/validator, :bouncer.validators/default-message-format and :bouncer.validators/optional.

Given that any library can add stuff to metas, this seems safer.

Messages customized based on input

I'd like to propose the possibility of looking at a new method of grabbing error messages where the metadata for the function is created dynamically, and therefore can take parameters. This would allow for more useful error messages, e.g. "%s must be at least 5 long" as opposed to "it should be longer"

To do this, I imagine the statically defined would need to be replaced with functions.

bouncer.validators/member does not allow me to pass it a symbol referring to the collection

I can't do this:

(def valid-things #{:a :b :c})

(bouncer.validators/defvalidatorset my-validator
  :field1 (bouncer.validators/member valid-things))

(bouncer.core/validate {:field1 :e}  myvalidator)
;; ClassCastException clojure.lang.Symbol cannot be cast to clojure.lang.Var  clojure.core/var-get (core.clj:3844)

and I'd like to. :) I'd also like to be able to use a symbol in bouncer.validators/custom and as arguments to 'format' as the :message value for validators.

provide optional validators

I often find myself wanting to validate attributes of maps only if they are present, for example when validating the body of a PATCH HTTP request.

I thought I could achieve that by omitting required in validation maps, but then realized that this would not work for anything but number,matchesandpositive`.

Which leads me to 2 potential issues:

  1. The behaviour of :optional and required may be a bit confusing at the moment. The fact that required exists in the API would let you think that the other validators are optional by default, as meant to be composed with required; this is made worse by the fact that for example number is optional but integer is not (is there a reason for this I don't see?). I would suggest to question the current design to make all provided validators :optional (which would be a breaking change of course, but could be considered for 1.0) or to document explicitly in the README which of the default validators are :optional.
  2. Back to my initial problem: I believe optional validators are needed. I ended up doing something like this in my app code:
  (ns my.own.validators
    (:require [bouncer.validators :as v]))

  (def string?? (vary-meta v/string assoc
                           :optional true
                           :validator ::string??))
  (def boolean?? (vary-meta v/string assoc
                           :optional true
                           :validator ::boolean??))
  ;; etc.

What do you think?

Cannot validate nested map before validating keys in the nested map

The following validation fails:

(b/validate {:kundeid "612345678"}
:kundeid v/required
:info v/required
[:info :klient-bruger] v/required)
=> ClassCastException clojure.lang.PersistentList cannot be cast to clojure.lang.Associative clojure.lang.RT.assoc (RT.java:702)

The following validation succeeds:

(b/validate {:kundeid "612345678"}
:kundeid v/required
[:info :klient-bruger] v/required
:info v/required)

Seems like a bug or at least unexpected behaviour.

Composable validation for nested sequences?

I have been running into cases in my project using bouncer where I have a map to be validated that contains keys which map to sequences of further maps. Your person-with-pets example is a good one:

(def person-with-pets {:name "Leo"
                       :pets [{:name nil}
                              {:name "Gandalf"}]})

(b/validate person-with-pets
          :pets [[v/every #(not (nil? (:name %)))]])

When validating with every you are checking for the presence the name key on the nested maps ad-hoc, but this could very well be done with a separate validation function specific to pets. The issue with that is every simply expects a truth-y value, and will be unaware if the call was to another validation routine that returns its own set of validation errors for a specific map in the sequence.

Of course, one workaround to this would be to validate the pets individually and separately from the person validation, but often it makes sense to keep these validations in one place (maybe!).

Any thoughts on this? I would be willing to work on a PR if this seems like a good addition and got a little nudge in the right direction.

Problem with using validatorsets as parameters

I must have misunderstood something, but I get an error when compiling the code below. What I am trying to achieve is to abstract away validation with a function. My custom validation function takes a map and a validator (i.e defined with def b-validators/defvalidatorset).

(ns x.y.z.validation
    (:require  
        [bouncer.core :as b-core]
        [bouncer.validators :as b-validators]))

(defn validate-me
    [request validator ok-function]
    (let [result (b-core/validate request validator)
           valid? (b-core/valid? result )]
          (if valid?
              (ok-function request)
              {:status "failed to validate"}))

This fails to compile with the following error:

CompilerException java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to clojure.lang.Var, compiling:(/Users/odinholestandal/git/kodemaker/oiiku/oiiku-messages-app/src/oiiku_messages_app/controllers/validation.clj:16:16) 

What am I doing wrong?

Password confirmation

I'm a bit confused. Is there a solution to password and password confirmation as discussed in #21 ?

If I try the examples in #21

(defn less-than [k]
  (fn [value subject]
    (< value (k subject))))
(core/valid? {:from 20 :to 10}
             :from [v/required (less-than :to)])

;; false

Results in
ArityException Wrong number of args (1) passed to: resources/less-than/fn--17635 clojure.lang.AFn.throwArity (AFn.java:429)

(defn same-as [k]
  (fn [value subject] 
    (= value (k subject))))

(core/valid? {:password 123 :confirmation 123}
             :password [v/required (same-as :confirmation)])

Similarly,

ArityException Wrong number of args (1) passed to: resources/same-as/fn--17711 clojure.lang.AFn.throwArity (AFn.java:429)

Am I doing something wrong?

Email validation error in domain part

In bouncer/src/bouncer/validators.cljx the email validation uses the following regex:

#"^[^@]+@[^@\\.]+[\\.].+"

I'm interested in that escaped backslash near the end. Why is it there? It allows the following email address to validate:

a@s\coom

I've checked this using an RPC822 validator here and it failed. I think a \ in the domain part is not part of RPC822 spec. Though the "/" is valid I think. The following email address passes that site's check:

hello@gmail/com

So yeah, is the escaped backslash supposed to be an escaped forward slash? What's the reasoning behind the backslash being allowed in the domain part?

Cheers

No way to validate a combination of fields.

For example: I have to check a "from" key should be less than "to" key for a range value.
I have to write a custom validator and pass the to field from the dictionary as the second parameter for the validator that gets applied to the older field.

Sometimes we have checks like if key A is present then key B should be present.

Can there be support at DSL level to specify such rules ?

Personal error for every item in vector

Hello,

Is it possible to get personal error for every item in vector? Something like this:

[{:contact {:phones [nil "Incorrect phone" nil]}}
 {:contact {:phones ["123-4567" :wrong "456-7890"]},
  :bouncer.core/errors {:contact {:phones [nil "Incorrect phone" nil]}}}]

Thank you!

Validators aren't applied if defined after applying a common set

According to the docs, you can define maps of validations that can be composed with other validations to factor out common sets. I can't get this to work. At the repl, if I do:

(require '[bouncer.core :as b])
(require '[bouncer.validators :as bv])

(def common-validations 
  {:field1 [[bv/required :message "Nothing given for field one."]]})

(b/validate {:field1 "Present" :field2 2}
            common-validations
            :field2 [[bv/boolean :message "Field 2 not a boolean"]])

(b/validate {:field1 "Present" :field2 2}  
            common-validations
            :field2 [[bv/boolean :message "Field 2 not a boolean"]]) ;; shouldn't validate :field2

Instead of a validation error, the last invocation above returns [nil {:field1 "Present", :field2 2}] - e.g. the second field 2 boolean test isn't applied. If I drop the common-validations part, it runs as expected:

(b/validate {:field1 "Present" :field2 2}
            :field2 [[bv/boolean :message "Field 2 not a boolean"]])

returns

[{:field2 ("Field 2 not a boolean")}
 {:field1 "Present", :field2 2, :bouncer.core/errors {:field2 ("Field 2 not a boolean")}}]

So the second validation is only applied if a common set isn't included in the validate invocation.

Validating heterogeneous list of items

I have a use case which I don't think is supported currently.

I have a list of actions like this:

[{:action "create" :product 123 :option "blah"}
{:action "delete" :id 321}
{:action "update" :id 456 :another-option "blah blah"}]

The point is that there is a number of possible item types (actions) in the list, each with its own fields that need to validated separately.

So when the action is "create" it should validate product and option and if product is not present it should give the error message "product is required" or "product invalid" if that is the case.

Optimally I would be able to create an "action" validator which then delegates to e.g. a "create" validator which can return the appropriate error messages. I think allowing a validator to return (or throw) an error would solve this.

Couldn't load library

Hello, I've come across some trouble when I'm trying to require bouncer into my project. I'm using leiningen, and currently trying to build a web service using compojure. I had no problem before when firing up the service, but when I tried to require bouncer, leiningen always throws a file not found exception, saying it could not locate bouncer/validator__init.class or bouncer/validator.clj on classpath.

I'm sorry, I'm very new to clojure and its ecosystem, is there something that I miss here? I follow the readme of the project, yet I still can't figure out what's wrong. I notice though that you're using a .cljc file extension instead a .clj file? I tried to understand what it is, but I can't seem to figure out what it is and how could I set up leiningen to look for that file extension as well.

I know this is a silly question, and would probably better be questioned somewhere else. But I've tried to ask in leiningen channel and they said there's nothing to be done in leiningen when dealing with that sort of files. And now I'm left stranded here. 😅

Coercion for fields

Are there any plans to add coercion for fields? For example, the following fails:

(validate {:id "1"} {:id [v/positive v/number]})

Something like this could work:

(validate {:id "1"} {[:id :number] [v/positive v/number]})

Why are these different?

(bouncer/validate
 {:date "2016-01-01"}
 :date validators/datetime (:mysql fmt/formatters))
Output ->
[nil {:date "2016-01-01"}]

(bouncer/validate
 {:date "2016-01-01"} 
 :date [[validators/datetime (:mysql fmt/formatters)]])
Output->
[{:date ("date must be a valid date")}
 {:date "2016-01-01" :bouncer.core/errors {:date ("date must be a valid date")}}]

From the documentation, I would've expected them to be equivalent, yet they aren't. What's going on?

Is it still alive?

Is Bouncer still maintained? I've few features that I'd like to see here, but I don't know if I should create a PR or a fork. Thank for answering.

v/datetime validates strings which f/parse doesn't

For example

(require 
    '[bouncer.core       :as b]
    '[bouncer.validators :as v]
    '[clj-time.format    :as f])

(def date-format (:date f/formatters)) ; => "2016-02-16"
(def bad-date  {:date "2000"})
(def good-date {:date "2000-10-11"})

(f/parse date-format (:date bad-date))  ; => Exception: Invalid format: "2000" is too short
(f/parse date-format (:date good-date)) ; => "2000-10-11T00:00:00.000Z"
(b/valid? bad-date  :date v/datetime date-format) ; => true
(b/valid? good-date :date v/datetime date-format) ; => true

Is it the expected behavior?

Unexpected `matches` behavior

I came across a surprising matches validator behavior: it marks a value as valid when only part of the value satisfies given regex:

(b/valid? {:name "ab"} {:name [v/required v/string [v/matches #"\w{1,2}"]]})
;;=> true

(b/valid? {:name "abc"} {:name [v/required v/string [v/matches #"\w{1,2}"]]})
;; should be false
;;=> true 

(re-matches #"\w{1,2}" "abc")
;;=> nil

;; even worse if we expect only digits
(b/valid? {:name "abc123def"} {:name [v/required v/string [v/matches #"\d+"]]})
;;=> true

Is this an expected behavior?

v/matches implies v/required?

I'm not sure if this is intended behaviour but

(bouncer/validate {}
                  :id [v/string [v/matches #"a*"]])

evaluates to

[{:id ("id must be a string")} {:bouncer.core/errors {:id ("id must be a string")}}]

I would have expected the validation to pass since :id is not v/required.

If this is expected how do I create a validator where an optional string value must match a regex only if the key exists?

Edit After some digging it's actually v/string that makes the key-value non-optional. This was a surprise for me

Change default custom validator message

It would be useful if it was possible to change :default-message-format to something more user friendly without having to provide a message in every validator. Possibly via a root binding.

Errors in a list, and short circuiting

The readme makes a point of short circuiting. If a key has more than one validator, only the error message from the first failing validator is returned. Considering this, why are error-strings returned as lists?

Example:

(s/validate {:age "NaN"}
                 :age [v/required v/number v/positive])
=> {:age (":age is not a number")} ;; why is error message in a list?

Revalidating a map that failed validation does not remove :bouncer.core/errors keyval and therefore always fails validation

(-> (bouncer-core/validate {:x "thing"} :x bouncer.validators/number)
     second 
     (assoc :x 6) 
    (bouncer.core/validate :x bouncer-vali/number))
[{:x ("x must be a number")} {:bouncer.core/errors {:x ("x must be a number")}, :x 6}]

I expect that the bounder.core/errors keyval be removed immediately before validation is performed and the second validation attempt to succeed. Currently, it fails with the incorrect error (the previous error).

3 level composition broken

It seems that for 3 levels the composition of defvalidatorset is broken:

(defvalidatorset lowest
  :a [v/required] 
  )

(defvalidatorset middle
  :b lowest 
  )

(defvalidatorset upper
  :c middle
  )

(:bouncer.core/errors (second (bouncer.core/validate {:c {:b {:a 1}}} vsphere.validations/upper))); => {[:c :b] {:a ("a must be present")}}

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.