{"id":22641050,"url":"https://github.com/alexanderkiel/phrase","last_synced_at":"2025-04-04T06:09:53.832Z","repository":{"id":57713942,"uuid":"103511684","full_name":"alexanderkiel/phrase","owner":"alexanderkiel","description":"Clojure(Script) library for phrasing spec problems.","archived":false,"fork":false,"pushed_at":"2020-04-29T17:41:43.000Z","size":51,"stargazers_count":288,"open_issues_count":10,"forks_count":8,"subscribers_count":13,"default_branch":"master","last_synced_at":"2024-04-23T21:37:08.613Z","etag":null,"topics":["clojure","clojurescript","form-validation","human-computer-interaction","spec"],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexanderkiel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-09-14T09:11:10.000Z","updated_at":"2024-02-12T09:42:25.000Z","dependencies_parsed_at":"2022-08-25T12:51:29.542Z","dependency_job_id":null,"html_url":"https://github.com/alexanderkiel/phrase","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderkiel%2Fphrase","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderkiel%2Fphrase/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderkiel%2Fphrase/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexanderkiel%2Fphrase/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexanderkiel","download_url":"https://codeload.github.com/alexanderkiel/phrase/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247128753,"owners_count":20888235,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","clojurescript","form-validation","human-computer-interaction","spec"],"created_at":"2024-12-09T04:17:07.469Z","updated_at":"2025-04-04T06:09:53.798Z","avatar_url":"https://github.com/alexanderkiel.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Phrase\n\n[![Build Status](https://travis-ci.org/alexanderkiel/phrase.svg?branch=master)](https://travis-ci.org/alexanderkiel/phrase)\n[![CircleCI](https://circleci.com/gh/alexanderkiel/phrase.svg?style=shield)](https://circleci.com/gh/alexanderkiel/phrase)\n[![Dependencies Status](https://versions.deps.co/alexanderkiel/phrase/status.svg)](https://versions.deps.co/alexanderkiel/phrase)\n[![Downloads](https://versions.deps.co/alexanderkiel/phrase/downloads.svg)](https://versions.deps.co/alexanderkiel/phrase)\n[![cljdoc](https://cljdoc.xyz/badge/phrase)](https://cljdoc.xyz/d/phrase/phrase/CURRENT)\n\nClojure(Script) library for phrasing [spec][2] problems. Phrasing refers to converting to human readable messages.\n\nThis library can be used in various scenarios but its primary focus is on form validation. I talked about [Form Validation with Clojure Spec][1] in Feb 2017 and Phrase is the library based on this talk.\n\nThe 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][3] does. The target audience for generated messages are end-users of an application not developers.\n\n## Install\n\nTo install, just add the following to your project dependencies:\n\n```clojure\n[phrase \"0.3-alpha4\"]\n```\n\n## Usage\n\nAssuming you like to validate passwords which have to be strings with at least 8 chars, a spec would be:\n\n```clojure\n(require '[clojure.spec.alpha :as s])\n\n(s/def ::password\n  #(\u003c= 8 (count %)))\n```\n\nexecuting\n\n```clojure\n(s/explain-data ::password \"1234\")\n```\n\nwill return one problem:\n\n```clojure\n{:path [],\n :pred (clojure.core/fn [%] (clojure.core/\u003c= 8 (clojure.core/count %))),\n :val \"\",\n :via [:user/password],\n :in []}\n```\n\nPhrase 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.\n\nThe 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:\n\n```clojure\n(require '[phrase.alpha :refer [defphraser]])\n\n(defphraser #(\u003c= min-length (count %))\n  [_ _ min-length]\n  (str \"Please use at least \" min-length \" chars.\"))\n``` \n\nthe following code:\n\n```clojure\n(require '[phrase.alpha :refer [phrase-first]])\n\n(phrase-first {} ::password \"1234\")\n```\n\nreturns the desired message:\n\n```clojure\n\"Please use at least 8 chars.\"\n```\n\n### The defphraser macro\n\nIn its minimal form, the defphraser macro takes a predicate and an argument vector of two arguments, a context and the problem:\n\n```clojure\n(defphraser int?\n  [context problem]\n  \"Please enter an integer.\")\n``` \n\nThe 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.\n\nIn 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`:\n\n```clojure\n(defphraser #(\u003c= min-length (count %))\n  [_ _ min-length]\n  (str \"Please use at least \" min-length \" chars.\"))\n``` \n\nIn case the predicated used in a spec is `#(\u003c= 8 (count %))`, `min-length` resolves to 8.\n\nCombined with the invalid value from the problem, we can build quite advanced messages:\n\n```clojure\n(s/def ::password\n  #(\u003c= 8 (count %) 256))\n  \n(defphraser #(\u003c= min-length (count %) max-length)\n  [_ {:keys [val]} min-length max-length]\n  (let [[a1 a2 a3] (if (\u003c (count val) min-length)\n                     [\"less\" \"minimum\" min-length]\n                     [\"more\" \"maximum\" max-length])]\n    (str \"You entered \" (count val) \" chars which is \" a1 \" than the \" a2 \" length of \" a3 \" chars.\")))\n           \n(phrase-first {} ::password \"1234\")\n;;=\u003e \"You entered 4 chars which is less than the minimum length of 8 chars.\"\n\n(phrase-first {} ::password (apply str (repeat 257 \"x\"))) \n;;=\u003e \"You entered 257 chars which is more than the maximum length of 256 chars.\"          \n``` \n\nBesides 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:\n\n```clojure\n(s/def ::year\n  pos-int?)\n\n(defphraser pos-int?\n  [_ _]\n  \"Please enter a positive integer.\")\n\n(defphraser pos-int?\n  {:via [::year]}\n  [_ _]\n  \"The year has to be a positive integer.\")\n\n(phrase-first {} ::year \"1942\")\n;;=\u003e \"The year has to be a positive integer.\"\n```\n\nWithout 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.\n\n### Default Phraser\n\nIt'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.\n\n```clojure\n(defphraser :default\n  [_ _]\n  \"Invalid value!\")\n```\n\nYou can remove the default phraser by calling `(remove-default!)`.\n\n### More Complex Example\n\nIf 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.\n\nIn 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.\n\n```clojure\n(s/def ::password\n  (s/and #(\u003c= 8 (count %) 256)\n         #(re-find #\"\\d\" %)\n         #(re-find #\"[a-z]\" %)\n         #(re-find #\"[A-Z]\" %)))\n\n(defphraser #(\u003c= lo (count %) up)\n  [_ {:keys [val]} lo up]\n  (str \"Length has to be between \" lo \" and \" up \" but was \" (count val) \".\"))\n\n;; Because Phrase replaces every concrete value like the regex, we can't match\n;; on it. Instead, we define only one phraser for `re-find` and use a case to \n;; build the message.\n(defphraser #(re-find re %)\n  [_ _ re]\n  (str \"Has to contain at least one \"\n       (case (str/replace (str re) #\"/\" \"\")\n         \"\\\\d\" \"number\"\n         \"[a-z]\" \"lowercase letter\"\n         \"[A-Z]\" \"uppercase letter\")\n       \".\"))\n\n(phrase-first {} ::password \"a\")\n;;=\u003e \"Length has to be between 8 and 256 but was 1.\"\n\n(phrase-first {} ::password \"aaaaaaaa\")\n;;=\u003e \"Has to contain at least one number.\"\n\n(phrase-first {} ::password \"AAAAAAA1\")\n;;=\u003e \"Has to contain at least one lowercase letter.\"\n\n(phrase-first {} ::password \"aaaaaaa1\")\n;;=\u003e \"Has to contain at least one uppercase letter.\"\n\n(s/valid? ::password \"aaaaaaA1\")\n;;=\u003e true\n```\n\n### Further Examples\n\nYou can find further examples [here][8].\n\n### Phrasing Problems\n\nThe 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. \n\n### Kinds of Messages\n\nPhrase doesn't assume anything about messages. Messages can be strings or other things like [hiccup][4]-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.\n\n## API Docs\n\nYou can view the API Docs at [cljdoc][7] for v0.3-alpha4.\n\n## Related Work\n\n* [Expound][3] - aims to generate more readable messages as `s/explain`. The audience are developers not end-users.\n\n## Complete Example in ClojureScript using Planck\n\nFirst install [Planck][5] 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:\n\n```sh\nboot -d phrase:0.3-alpha4\n```\n\nAfter that, start Planck with Phrase as dependency:\n\n```sh\nplanck -D phrase:0.3-alpha4\n```\n\nAfter that, you can paste the following into the Planck REPL:\n\n```clojure\n(require '[clojure.spec.alpha :as s])\n(require '[phrase.alpha :refer [defphraser phrase-first]])\n\n(s/def ::password\n  #(\u003c= 8 (count %)))\n  \n(defphraser #(\u003c= min-length (count %))\n  [_ _ min-length]\n  (str \"Please use at least \" min-length \" chars.\"))\n  \n(phrase-first {} ::password \"1234\")\n```\n\nThe output should be:\n\n```clojure\nnil\nnil\n:cljs.user/password\n#object[cljs.core.MultiFn]\n\"Please use at least 8 chars.\"\n```\n\n## License\n\nCopyright © 2017 Alexander Kiel\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n\n[1]: \u003chttps://www.slideshare.net/alexanderkiel/form-validation-with-clojure-spec\u003e\n[2]: \u003chttps://clojure.org/about/spec\u003e\n[3]: \u003chttps://github.com/bhb/expound\u003e\n[4]: \u003chttps://github.com/weavejester/hiccup\u003e\n[5]: \u003chttp://planck-repl.org\u003e\n[6]: \u003chttps://github.com/alexanderkiel/phrase/issues/10\u003e\n[7]: \u003chttps://cljdoc.xyz/d/phrase/phrase/0.3-alpha4/api/phrase.alpha\u003e\n[8]: \u003chttps://cljdoc.xyz/d/phrase/phrase/0.3-alpha4/doc/examples\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexanderkiel%2Fphrase","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexanderkiel%2Fphrase","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexanderkiel%2Fphrase/lists"}