Giter VIP home page Giter VIP logo

phrase's Introduction

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 value!")

You can remove the default phraser by calling (remove-default!).

More Complex Example

If you like to validate more than one thing, for example correct length and various regexes, I suggest that you build a spec using s/and as opposed to building a big, complex predicate which would be difficult to match.

In this example, I require a password to have the right length and contain at least one number, one lowercase letter and one uppercase letter. For each requirement, I have a separate predicate.

(s/def ::password
  (s/and #(<= 8 (count %) 256)
         #(re-find #"\d" %)
         #(re-find #"[a-z]" %)
         #(re-find #"[A-Z]" %)))

(defphraser #(<= lo (count %) up)
  [_ {:keys [val]} lo up]
  (str "Length has to be between " lo " and " up " but was " (count val) "."))

;; Because Phrase replaces every concrete value like the regex, we can't match
;; on it. Instead, we define only one phraser for `re-find` and use a case to 
;; build the message.
(defphraser #(re-find re %)
  [_ _ re]
  (str "Has to contain at least one "
       (case (str/replace (str re) #"/" "")
         "\\d" "number"
         "[a-z]" "lowercase letter"
         "[A-Z]" "uppercase letter")
       "."))

(phrase-first {} ::password "a")
;;=> "Length has to be between 8 and 256 but was 1."

(phrase-first {} ::password "aaaaaaaa")
;;=> "Has to contain at least one number."

(phrase-first {} ::password "AAAAAAA1")
;;=> "Has to contain at least one lowercase letter."

(phrase-first {} ::password "aaaaaaa1")
;;=> "Has to contain at least one uppercase letter."

(s/valid? ::password "aaaaaaA1")
;;=> true

Further Examples

You can find further examples here.

Phrasing Problems

The main function to phrase problems is phrase. It takes the problem directly. There is a helper function called phrase-first which does the whole thing. It calls s/explain-data on the value using the supplied spec and phrases the first problem, if there is any. However, you have to use phrase directly if you like to phrase more than one problem. The library doesn't contain a phrase-all function because it doesn't know how to concatenate messages.

Kinds of Messages

Phrase doesn't assume anything about messages. Messages can be strings or other things like hiccup-style data structures which can be converted into HTML later. Everything is supported. Just return it from the defphraser macro. Phrase does nothing with it.

API Docs

You can view the API Docs at cljdoc for v0.3-alpha4.

Related Work

  • Expound - aims to generate more readable messages as s/explain. The audience are developers not end-users.

Complete Example in ClojureScript using Planck

First install Planck if you haven't already. Planck can use libraries which are already downloaded into your local Maven repository. A quick way to download the Phrase Jar is to use boot:

boot -d phrase:0.3-alpha4

After that, start Planck with Phrase as dependency:

planck -D phrase:0.3-alpha4

After that, you can paste the following into the Planck REPL:

(require '[clojure.spec.alpha :as s])
(require '[phrase.alpha :refer [defphraser phrase-first]])

(s/def ::password
  #(<= 8 (count %)))
  
(defphraser #(<= min-length (count %))
  [_ _ min-length]
  (str "Please use at least " min-length " chars."))
  
(phrase-first {} ::password "1234")

The output should be:

nil
nil
:cljs.user/password
#object[cljs.core.MultiFn]
"Please use at least 8 chars."

License

Copyright © 2017 Alexander Kiel

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

phrase's People

Contributors

alexanderkiel avatar bfontaine avatar leblowl avatar tirkarthi 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

phrase's Issues

Binding to clojure symbols fails

Neat library!

However, seeing some rather bizarre behaviour in the repl:

(s/def ::example #(< 8 (count %)))

(p/defphraser #(< min (count %))
  [_ _ min]
  min)

(p/phrase-first {} ::example "abcdef")
=> nil

(p/defphraser #(< m (count %))
  [_ _ m]
  m)

(p/phrase-first {} ::example "abcdef")
=> 8

(p/defphraser #(< min (count %))
  [_ _ min]
  min)

(p/phrase-first {} ::example "abcdef")
=> 8

Clojure "1.9.0-beta4"
Clojurescript "1.9.946"

Phrasing (s/int-in min max)

Here is a simple example using spec's int-in

(defphraser :default
  [_ problem]
  problem)

(defphraser (s/int-in min max)
  [_ _ min max]
  (str "must be between " min " and " max))

(s/def ::total-widgets (s/int-in 1 32))

(phrase/phrase-first {} ::total-widgets 100)

This hits the default handler, and the problem map has the pred value

  :pred ( clojure.core/fn [ % ] ( clojure.spec.alpha/int-in-range? 1 32 % ) )

What's the proper way to phrase this?

Dispatch on the Longest Suffix Match of the :via Path

Consider the following specs and phraser:

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

(s/def ::date
  (s/keys :req [::year]))

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

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

While (phrase-first {} ::year -1) returns The year has to be a positive integer., (phrase-first {} ::date {::year -1}) returns Please enter a positive integer..

If you run

(-> (s/explain-data ::date {::year -1})
      ::s/problems
      first
      :via)

it returns [::date ::year]. So the :via path includes the ::date spec. We could write a phraser with this :via path, but it shouldn't be necessary. Instead phrase should dispatch on the longest suffix match.

Fail to match coll-of min-count predicate

Hi,
Thanks a lot for this library!

I have defined the following spec:

(s/def ::colors (s/coll-of string? :kind vector? :min-count 1))

And the following phrasers:

(phrase/defphraser :default
                   [_ problem]
                   problem)

(phrase/defphraser #(clojure.core/<= min-count (clojure.core/count %) max-count)
                   [_ {:keys [val]} min-count max-count]
                   (str min-count))

When running phrase-first with the following input:

(phrase/phrase-first {} ::colors [])

I expect the 2nd phraser to be matched, but actually the default phraser is matched:

{:path [],
 :pred (clojure.core/<= 1 (clojure.core/count %) Integer/MAX_VALUE),
 :val [],
 :via [:my-project.schema/colors],
 :in [],
 :phrase.alpha/mappings {:x0 1},
 :phrase.alpha/via ()}

Any ideas what am I missing?
Thanks!

Fail to match distinct? predicate

Is there any way to define message for distinct? by phrase.

My simple example as follows.

;; (:require [clojure.spec.alpha :as s]
;;           [phrase.alpha :as p])

;; define s/def as `:distinct true`
(s/def ::distincted (s/coll-of integer? :distinct true))

;; s/explain-data return :pred as `distinct?`
(->> (::s/problems (s/explain-data ::distincted [1 1]))
     first
     :pred)
;; => distinct?

;; Use defphraser
(p/defphraser distinct? [_ _] "Should be distincted")

;; But phrase does not be invoked
(p/phrase-first {} ::distincted [1 1])
;; => nil

I expected p/phrase-first return Should be distincted but nil.
Any idea? Thanks!

Update ClojureScript to Support Java 9

The current version of ClojureScript (1.9.946) depends on javax.xml.bind.DatatypeConverter which is no longer available by default in Java 9. CLJS-2377 solves this problem but is not released yet.

Should we have a phrase-all function?

Hi,
Not sure if i understood all correctly but i don't think that there is a method to get all the errors ?
like :

(defn phrase-all
  [context spec x]
  (some->> (s/explain-data spec x)
           ::s/problems
           (map #(phrase context %))))

Do i miss something ?
Thanks,

defphrase not matching set parameter

This library was exactly what I needed. I am having trouble with the defphrase for the following predicate however.

(defn contains-only?
  [m ks]
  (-> m keys set (set/difference ks) empty?))

Put together the following code to show the problem.

(ns phraser-test
  (:require [phrase.alpha :refer [defphraser] :as phraser]
            [clojure.set :as set]
            [clojure.spec.alpha :as s]))

(defn contains-only?
  [m ks]
  (-> m keys set (set/difference ks) empty?))

(defphraser #(contains-only? % ks)
  [_ _ ks]
  (str ks " are the only allowed keys."))

(s/def ::person #(contains-only? % #{:name :age :gender}))

(defn test []
  (phraser/phrase-first {} ::person {:name "dave" :age 42 :gender :male :hair :brown}))

Despite several attempts I cannot get the phraser dispatching to match. Any help would be appreciated.

Simple or not doable???

I cannot find a way to make a simple 'or' spec work:

(s/def ::id-stg
  (s/and
   string?
   #(re-matches #"^[0-9]+$" %)))

(defphraser string?
  {:via [::id-stg]} [_ _]
  "Id must be string")

(defphraser #(re-find re %)
  [_ _ re]
  (str "Id must "
       (case  (str/replace #"/" "" (str re))
         "^[0-9]+$" "contain only digits")))

(phrase-first {} ::id-stg 1) ; works - returns "Id must be string"
(phrase-first {} ::id-stg "a") ; returns nil

That's just one set of trial and error attempts.

This lib looks like it could be on track to doing 'The Right Thing'. But it is extremely opaque. When / how to use specifiers, context, problem etc to get a 'proper' dispatch is completely guess ware. If you could at least give an example with an 'or spec' and a s/keys spec (for maps) maybe one could stumble towards success.

Confusing difference between s/explain and phrase on success

Hello, thanks for phrase!

I spent hours trying to debug a nil from phrase-first until I realized that there is a behavior difference that I find confusing. Consider the following:

(s/def ::simple int?)

(phrase/defphraser int? [_ _] "Expected an integer")

(deftest simplest-possible-spec-for-phrase
  (is (= "val: 1.0 fails spec: :grape.phrase-test/simple predicate: int?\n"
         (s/explain-str ::simple 1.0)))
  (is (= "Expected an integer"
         (phrase/phrase-first {} ::simple 1.0))))

(deftest difference-between-explain-and-phrase-when-success
  (is (= "Success!\n"
         (s/explain-str ::simple 1)))
  (is (= nil
         (phrase/phrase-first {} ::simple 1))))

First test shows that phrase is for end-user and s/explain is for developers, everything is as expected :-)

Second test shows that, when the spec is actually valid, s/explain returns string "Success!" while phrase returns nil. Although I read the docstring for phrase-first: "Returns nil if x is valid or no phraser was found." I was still confused for hours :-)

In my opinion, there is a big difference between a valid spec and no phraser found. Is there a reason for always returning nil or would it make sense to conform (pun intended) with explain here and return "Success!" ?

Is there any way to remove/clear phrases?

Thanks for the great library, I really like the design of the user interface (defphraser specifically). I stumbled upon an odd issue that may not be common, but it caused me to scratch my head for a few moments, so I'd like to share in case anyone else runs into it. When you define a phraser with dispatch on the :via keyword, and then delete it and re-define the same phraser without dispatch (within the same REPL session), the dispatched version will shadow the non-dispatched version - which is what you would expect, but for REPL development it may be confusing since your current phraser is not actually working. I am not sure how to clear the phrase storage besides a REPL reset.

Binding pred

Hello guys!

There is a reason about this:

(def enum #{"foo" "bar"})

(defphraser enum [_ _ _] (str "Error")) ;; not work when there is an error

(defphraser #{"foo" "bar"} [_ _ _] (str "Error")) ;; work when there is an error

Thanks!

Confused about phrasing a `coll-of`

Hello, I am missing how to phrase a coll-of. Consider the following:

(ns grape.phrase-test
  (:require [clojure.test :refer [deftest is are testing]]
            [clojure.spec.alpha :as s]
            [phrase.alpha :as phrase]))

(s/def ::just-int int?)

(phrase/defphraser int? [_ _] "Expected an integer (1)")

; this passes
(deftest simplest-possible-spec-for-phrase
  (is (= "val: 1.0 fails spec: :grape.phrase-test/just-int predicate: int?\n"
         (s/explain-str ::just-int 1.0)))
  (is (= "Expected an integer (1)"
         (phrase/phrase-first {} ::just-int 1.0))))

(s/def ::coll-of-ints (s/coll-of int?))

(phrase/defphraser coll? [_ _] "Expected a collection (2)")

(deftest coll-of-ints-fail-pred-int
  ; this passes
  (is (= "In: [0] val: :a fails spec: :grape.phrase-test/coll-of-ints predicate: int?\n"
         (s/explain-str ::coll-of-ints [:a])))
  ; this fails with `nil`
  (is (= "Expected an integer (1)" (phrase/phrase-first {} ::coll-of-ints [:a]))))

; this passes
(deftest coll-of-ints-fail-pred-coll
  (is (= "val: :a fails spec: :grape.phrase-test/coll-of-ints predicate: coll?\n"
         (s/explain-str ::coll-of-ints :a)))
  (is (= "Expected a collection (2)" (phrase/phrase-first {} ::coll-of-ints :a))))

Tests simplest-possible-spec-for-phrase and coll-of-ints-fail-pred-coll pass as expected. On the other hand, test coll-of-ints-fail-pred-int fails, phrase-first returns nil instead of "Expected an integer (1)". What am I missing ?

Please define phrase-all like so:

(defn phrase-all [context spec val]
  (some->> (s/explain-data spec val)
           ::s/problems
           (mapv (partial phrase.alpha/phrase context))))

Capture Binding Symbols Should Shadow Global Symbols in Predicates

The following phraser doesn't work because key is a function in clojure.core and most namespaces import it:

(defphraser #(contains? % key)
  [_ _ key]
  (format "Missing %s." (name key)))

The intended behaviour is to use key as symbol to capture the actual key of the contains? predicate. The problem is that the current symbol resolution tries to resolve all symbols first and only leave the symbol alone if it fails. The solution is to not resolve capture binding symbols. The result is that key in the predicate is actually in scope of the phraser and not in global scope.

Add required keys phraser example to README?

I think it might be helpful to add the example phraser in issue #8 to the README. The example shows how to generate a phrase for a required spec key that is missing. I feel this is a very common use-case. Any thoughts? Thanks

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.