• Stars
    star
    289
  • Rank 143,419 (Top 3 %)
  • Language
    Clojure
  • License
    Eclipse Public Li...
  • Created about 7 years ago
  • Updated over 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Clojure(Script) library for phrasing spec problems.

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.

More Repositories

1

datomic-free

Dockerfile for the Database Datomic Free Edition
Shell
64
star
2

async-error

A Clojure(Script) library which provides core.async error handling utilities.
Clojure
25
star
3

datomic-spec

Specs for Datomic
Clojure
24
star
4

material-comp

Components for Material UI especially Form Validation
Clojure
8
star
5

clojure-berlin-2016

A simple service showcasing continuous deployment on Kubernetes.
Clojure
6
star
6

hap-spec

The Hypermedia Application Protocol (HAP) is a domain generic, self-documenting, hypermedia type.
6
star
7

elm-mdc-alpha

Material Components for the Web for Elm
Elm
5
star
8

glassfish-elasticsearch

An Agent which transfers GlassFish log files such as server.log into elasticsearch.
Clojure
4
star
9

lens

Lens is a tool for online analytical data processing in medical studies.
3
star
10

takelist

I took it and will pay later.
Clojure
2
star
11

lens-warehouse

The data warehouse component of the Lens system.
Clojure
2
star
12

ring-hap

Ring middleware for Hypermedia Application Protocol.
Clojure
2
star
13

spec-coerce

Coercion for Clojure Spec
Clojure
2
star
14

prom-metrics

Clojure Wrappers for the Prometheus Java Client.
Clojure
2
star
15

gba-bridgehead-compose

Docker Compose File for GBA Bridgehead
2
star
16

hap-todo

Example ToDo Service demonstrating HAP.
Clojure
2
star
17

junit-util

Junit Util contains common assertions which extend the junit assertions and are helpful in many projects.
Java
1
star
18

samply.broker.ui.material

Prototype of a Samply Broker UI which uses Material Design
Elm
1
star
19

samply.broker.search.store

Service storing Searches
Clojure
1
star
20

shortid

Generates short random base62 encoded identifiers.
Clojure
1
star
21

kata-bank-ocr

Clojure
1
star
22

spec-bnf

Generate BNFs from Clojure Spec.
Clojure
1
star
23

lens-workbook

A workbook storage service for Lens.
Clojure
1
star
24

RestApiTest

RestApiTest is a DSL based test framework for integration tests of a HTTP based REST API with additional modules for test fixture setups and asserts in SQL.
1
star
25

tmf-cql

TMF CQL Schulung
jq
1
star
26

hap-browser

Generic Hypermedia Application Protocol (HAP) UI.
Clojure
1
star
27

mk-config-drive

Simple dockerized Script to create Cloud Config Drives
Shell
1
star
28

idea-haskell

IDEA Plugin for Haskell
Java
1
star
29

samply.connector.docker

Docker Image for Samply Connector
Shell
1
star
30

pull

Pull like in Datomic but over maps of maps.
Clojure
1
star
31

lens-app

Lens is a tool for online analytical data processing in medical studies
Clojure
1
star
32

fhir2csv

Extract Tabular Data from FHIR Resources
JSONiq
1
star