Skip to content

alexanderkiel/phrase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Phrase

Build Status CircleCI Dependencies Status Downloads cljdoc

Clojure(Script) library for phrasing spec problems. Phrasing refers to converting to human readable messages.

This library can be used in various scenarios but its primary focus is on form validation. I talked about Form Validation with Clojure Spec in Feb 2017 and Phrase is the library based on this talk.

The main idea of this library is to dispatch on spec problems and let you generate human readable messages for individual and whole classes of problems. Phrase doesn't try to generically generate messages for all problems like Expound does. The target audience for generated messages are end-users of an application not developers.

Install

To install, just add the following to your project dependencies:

[phrase "0.3-alpha4"]

Usage

Assuming you like to validate passwords which have to be strings with at least 8 chars, a spec would be:

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

(s/def ::password
  #(<= 8 (count %)))

executing

(s/explain-data ::password "1234")

will return one problem:

{:path [],
 :pred (clojure.core/fn [%] (clojure.core/<= 8 (clojure.core/count %))),
 :val "",
 :via [:user/password],
 :in []}

Phrase helps you to convert such problem maps into messages for your end-users which you define. Phrase doesn't generate messages in a generic way.

The main discriminator in the problem map is the predicate. Phrase provides a way to dispatch on that predicate in a quite advanced way. It allows to substitute concrete values with symbols which bind to that values. In our case we would like to dispatch on all predicates which require a minimum string length regardless of the concrete boundary. In Phrase you can define a phraser:

(require '[phrase.alpha :refer [defphraser]])

(defphraser #(<= min-length (count %))
  [_ _ min-length]
  (str "Please use at least " min-length " chars."))

the following code:

(require '[phrase.alpha :refer [phrase-first]])

(phrase-first {} ::password "1234")

returns the desired message:

"Please use at least 8 chars."

The defphraser macro

In its minimal form, the defphraser macro takes a predicate and an argument vector of two arguments, a context and the problem:

(defphraser int?
  [context problem]
  "Please enter an integer.")

The context is the same as given to phrase-first it can be used to generate I18N messages. The problem is the spec problem which can be used to retrieve the invalid value for example.

In addition to the minimal form, the argument vector can contain one or more trailing arguments which can be used in the predicate to capture concrete values. In the example before, we captured min-length:

(defphraser #(<= min-length (count %))
  [_ _ min-length]
  (str "Please use at least " min-length " chars."))

In case the predicated used in a spec is #(<= 8 (count %)), min-length resolves to 8.

Combined with the invalid value from the problem, we can build quite advanced messages:

(s/def ::password
  #(<= 8 (count %) 256))
  
(defphraser #(<= min-length (count %) max-length)
  [_ {:keys [val]} min-length max-length]
  (let [[a1 a2 a3] (if (< (count val) min-length)
                     ["less" "minimum" min-length]
                     ["more" "maximum" max-length])]
    (str "You entered " (count val) " chars which is " a1 " than the " a2 " length of " a3 " chars.")))
           
(phrase-first {} ::password "1234")
;;=> "You entered 4 chars which is less than the minimum length of 8 chars."

(phrase-first {} ::password (apply str (repeat 257 "x"))) 
;;=> "You entered 257 chars which is more than the maximum length of 256 chars."          

Besides dispatching on the predicate, we can additionally dispatch on :via of the problem. In :via spec encodes a path of spec names (keywords) in which the predicate is located. Consider the following:

(s/def ::year
  pos-int?)

(defphraser pos-int?
  [_ _]
  "Please enter a positive integer.")

(defphraser pos-int?
  {:via [::year]}
  [_ _]
  "The year has to be a positive integer.")

(phrase-first {} ::year "1942")
;;=> "The year has to be a positive integer."

Without the additional phraser with the :via specifier, the message "Please enter a positive integer." would be returned. By defining a phraser with a :via specifier of [::year], the more specific message "The year has to be a positive integer." is returned.

Default Phraser

It's certainly useful to have a default phraser which is used whenever no matching phraser is found. You can define a default phraser using the keyword :default instead of a predicate.

(defphraser :default
  [_ _]
  "Invalid valu