Giter Club home page Giter Club logo

metis's Introduction

metis [mee'-tis]

Metis is a library for data validation in Clojure inspired by Active Record Validations.

Validations are used to ensure that the data coming from user input is valid. For example, when a user inputs their email address, it is important to ensure that the email looks like an email ([email protected]).

Requirements

  • Clojure 1.4+

Installation

:dependencies [[metis "0.3.3"]]

Usage

Defining a Validator using defvalidator

defvalidator is a macro that allows you to quickly define a validator function. There are many ways to use the defvalidator dsl to define your validation rules. Let's look at all the possible ways.

Single Attribute and Single Validator

no options
(use 'metis.core)

(defvalidator user-validator
  [:first-name :presence])
with options
(defvalidator user-validator
  [:first-name :presence {:message "Please input your first name."}])

Multiple Attributes and Single Validator

no options
(defvalidator user-validator
  [[:first-name :last-name] :presence])
with options
(defvalidator user-validator
  [[:first-name :last-name] :presence {:message "This field is required."}])

Single Attribute and Multiple Validators

no options
(defvalidator user-validator
  [:first-name [:presence :length]])
with options
(defvalidator user-validator
  [:first-name [:presence :length {:equal-to 5}]])

(defvalidator user-validator
  [:first-name [:presence {:message "gotta have it!"} :length]])

(defvalidator user-validator
  [:first-name [:presence {:message "gotta have it!"} :length {:equal-to 5}]])

Multiple Attributes and Multiple Validators

no options
(defvalidator user-validator
  [[:first-name :last-name] [:presence :length]])
with options
(defvalidator user-validator
  [[:first-name :last-name] [:presence :length {:equal-to 5}]])

(defvalidator user-validator
  [[:first-name :last-name] [:presence {:message "gotta have it!"} :length]])

(defvalidator user-validator
  [[:first-name :last-name] [:presence {:message "gotta have it!"} :length {:equal-to 5}]])

All together now

(defvalidator user-validator
  [[:address :first-name :last-name :phone-number :email] :presence]
  [:first-name :with {:validator (fn [attr] false) :message "error!"}]
  [:last-name :formatted {:pattern #"some pattern" :message "wrong formatting!"}])

(user-validator {:first-name nil :last-name "Smith" :phone-number "123456789" :email "[email protected]"}
; {:address ["is not present"], :first-name ["error!" "is not present"], :last-name ["wrong formatting!"]}

Shared Options:

These options are shared by all validators, custom or built-in.

  • :message Provide a custom message upon failure.
  • :allow-nil Allow the value to be nil. Default false.
  • :allow-blank Allow the value to be blank (i.e. empty string or empty collection). Default false.
  • :allow-absence Allow the value to be blank or nil. Same as :allow-blank true :allow-nil true. Default false.
  • :only Specifiy the contexts in which the validation should be run. Default [] (all contexts).
  • :except Specifiy the contexts in which the validation should not be run. Default [] (no contexts).
  • :if A function that takes a map and returns true if the validation should be run. Default (fn [attrs] true).
  • :if-not A function that takes map and returns true if the validation should not be run. Default (fn [attrs] false).

Defining custom validators

Even though Metis has many built-in validators, you will probably need to define your own at some point. Custom validators are defined in the same way that the built-in validators are defined, as functions.

A validator is simply a function that takes in a map and returns an error or nil. As an example, let's look at the built-in presence validator.

(defn presence [map key _]
  (when-not (present? (get map key))
    "must be present")))

As you can see, this is a very simple validator. It checks if the value is present and returns an error if it is not. This is the structure of all the validators in Metis. Every validator takes in the map, the key to be validated, and a map of options. The presence validator, however, does not take in any options, so the third option is ignored.

Lets define a custom validator that checks if every charater is an 'a'.

(defn all-a [map key options]
  (when-not (every? #(= "a" (str %)) (get map key))
    "not all a's"))

(all-a {:thing "aaa"} :thing {})
; nil

(all-a {:thing "abc"} :thing {})
; "not all a's"

(defvalidator first-name-with-only-a
  [:first-name :all-a])

(first-name-with-only-a {:first-name "aaa"})
;{}

(first-name-with-only-a {:first-name "abc"})
;{:first-name ["not all a's"]}

Composing validators

As I said before, validators are functions that accept a map, key and options. The function produced by the defvalidator macro also adheres to this interface, meaning that it can be reused in the same manner as custom validators. Let's take a look at how we can use this simple feature to validate nested maps.

(defvalidator :country
  [[:code :name] :presence])

(defvalidator :address
  [[:line-1 :line-2 :zipcode] :presence]
  [:nation :country])

(defvalidator :person
  [:address :address]
  [:first-name :presence])

(person {})
; {:address {:zipcode ["must be present"], :line-2 ["must be present"], :line-1 ["must be present"], :nation {:name ["must be present"], :code ["must be present"]}}, :first-name ["must be present"]}

(person {:first-name "Myles" :address {:zipcode "60618" :line-1 "515 W Jackson Blvd." :line-2 "Floor 5" :nation {:code 1 :name "United States"}}})
; {}

Conditional validation

Often times, the set of validations to run is not cut and dry. Consider a payment form in which the user can opt to input their credit card number or PayPal information. If they select credit card, we have to validate that the credit card number is formatted correctly. If they select PayPal, we have to validate the email address.

This can be accomplished using the :if and :if-not options. The :if option is used to specify when the validation should happen. The :if-not option is used to specify when the validation should not happen.

(defn payment-type [attrs]
  (= (:payment-type attrs) "card"))

(defvalidator :if-conditional
  [:card-number :presence {:if payment-type}])

(defvalidator :if-not-conditional
  [:card-number :presence {:if-not payment-type}])

(if-conditional {})
; {}

(if-conditional {:payment-type "card"})
; {:card-number ["must be present"]}

(if-not-conditional {})
; {:card-number ["must be present"]}

(if-not-conditional {:payment-type "card"})
; {}

Contextual validation

Often times, a set of data, say a user's profile, will have multiple forms in an application; possibly one form for creating the profile and another for updating. It can be useful to share the same validations across both of these forms, especially if there are many shared validations between them. However, there is always going to be some pesky field that is required for one form and not the other. To solve this, we can use contexts. The :only option is used to specify the contexts in which the validation should be run. The :except option is used to specify the contexts from which the validation should be excluded.

(defvalidator user-validator
  [:first-name :presence {:only :creation :message "error!"}]
  [:last-name :formatted {:pattern #"some pattern" :only [:updating :saving] :message "wrong formatting!"}]
  [:address :presence {:message "You must have an address." :except [:updating]}])

(user-validator {}) ; when no context is specified, all validations are run
; {:first-name ["error!"], :last-name ["wrong formatting!"], :address ["You must have an address."]}

(user-validator {} :creation)
; {:first-name ["error!"], :address ["You must have an address."]}

(user-validator {} :updating)
; {:last-name ["wrong formatting!"]}

(user-validator {} :saving)
; {:last-name ["wrong formatting!"], :address ["You must have an address."]}

(user-validator {} :somewhere-else)
; {:address ["You must have an address."]}

Note: the context names here are arbitrary; they can be anything.

Contributing

Clone the master branch, build, and run all the tests:

git clone [email protected]:mylesmegyesi/metis.git
cd metis
lein deps
lein spec

Make patches and submit them along with an issue (see below).

Issues

Post issues on the metis github project:

License

Copyright (C) 2012 Myles Megyesi All Rights Reserved.

Distributed under the Eclipse Public License, the same as Clojure.

metis's People

Contributors

mylesmegyesi avatar patrickgombert 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

metis's Issues

Possible bug with allow-nil

Hi,

I have the following validator:

(defvalidator :my-validator 
  [:name [:allow-nil true :length {:equal-to 4}]])

...and it will always return true even thought I expect error. For example:

> (my-validator "Daveeeee")
{}

I expected the above to return {:name ["must have length equal to 4"]}. If i remove :allow-nil true it works fine though.

Am i doing something wrong or is this a bug?

Built-in validator suggestion

Hello!

I found myself using very often a custom validator I made in order to check all the elements in a vector if they are numeric in some cases or if they match a regex pattern in some other cases.

For example, the case where I mostly use it is when I wanna validate the a vector query parameter.

e.g www.example.com?items[]=1&items[]=2

And then inside my code i take the items and use my validator, which works just fine but i thought I could propose the implementation of a build-in vector-of validator which as options will take the currently available validators and check against them if all vector elements pass the validation.

:presence considers a key not present if value is the empty set or nil

A :presence validator designed to test for the presence of a key will fail the map if the value of that key is nil or #{} (but false works).

Since the key is present, it should pass validation, even if its value is #{} or nil -- (keys {:foo nil}) and (keys {:foo #{}}) both return (:foo).

For example:

(defvalidator :has-key-foo
  [:foo :presence])
user> (has-key-foo {:foo 123})
{}
nil
user> (has-key-foo {:foo #{}})
{:foo ["must be present"]}
nil
user> (has-key-foo {:foo false})
{}
nil
user> (has-key-foo {:foo nil})
{:foo ["must be present"]}
nil

Return `nil` instead?

Looking for suggestions. I'm imagining that defvalidator could make life a bit easier if it returned nil versus {} for a successful validation. Why?

Returning {}...

(let [m (my-validator data)]
    (if (empty? m)
      ... ;; Continue with my successful validation
      m)) ;; Or, return my validation errors

Returning nil...

(if-let [m (my-validator data)]
  ... ;; Continue on
  m) ;; Return validation errors

Am I missing something obvious?

Validate a nested sequence of maps

If I have a nested sequence of maps (example below), is there a way to validate each map in that sequence? I saw in the docs how to validate nested maps, but I'm not sure if it's possible to validate nested sequences of maps without a for.

{ :dogs [{ :name "Fido"
           :breed "German Shepherd" }
         { :name "Spike"
           :breed "Bulldog" }] }

defvalidator injects (use 'metis.validators), causing collisions with existing names

I'm using Metis along with Korma. Korma includes a function called with. metis.validators does the same. Meaning that if both are used, there is a collision. Requiring and aliasing metis.core doesn't fix this, since under the hood (use 'metis.validators) is getting injected.

As a work around, I can require and alias korma.core.

I'm a wee bit of a nub, so I might be making the wrong assumption here, but wouldn't it be more appropriate to require metis.validators, and then refer to it's fully-qualified functions?

Possible bug with length validator

I have the following validator

(defvalidator my-validator 
  [:my-key :length {:is-not-greater-than 5}])

And i run the following:

(my-validator {:my-key "aaaaaa"})

Expected value is error with message "must have a length less of 5" but instead it validates just fine.

I have also tested it with :is-not-less-than, :is-not-greater-than-or-equal-to and :is-not-less-than-or-equal-to and I have noticed the same buggy behavior. The only one that worked so far is :equal-to

Validators composing

First of all, I wanna say that the author has done really good work with metis :) It really solved a lot of my issues.

I do have one question though. I have two validators which they are using the same validation rule and I am wondering if I can make the composition like this:

(defvalidator :limit-validator
  [:limit {:only-integer true}])

(defvalidator :items-validator
  [:limit :limit-validator]
  [:my-field :presence])

(defvalidator :another-validator
  [:limit :limit-validator]
  [:another-field :presence]) 

I know the above does not work, I am just trying to explain what am I trying to accomplish :)

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.