Skip to content

Instantly share code, notes, and snippets.

@featalion
Last active July 28, 2016 15:20
Show Gist options
  • Select an option

  • Save featalion/dfd5743e8282e6db83282c12d7101e21 to your computer and use it in GitHub Desktop.

Select an option

Save featalion/dfd5743e8282e6db83282c12d7101e21 to your computer and use it in GitHub Desktop.
(ns spec-test
(:require [clojure.string :refer [lower-case blank?]]
[clojure.spec :as s]
[buddy.hashers :as hsh]
[compojure.core :refer [defroutes POST]]
[korma.core :as dbc]
[taoensso.timbre :as log]
[postgre.models :refer [users]]))
;;; Utils
(defn password-valid?
"Checks, that password:
- is at least 8 characters long
- contains one or more uppercase characters
- contains one or more lowercase characters
- contains one or more digits
- contains one or more special characters: ! @ # $ % ^ & * _ -
returns true if password is valid, false otherwise."
[password]
(some?
(re-matches #"(?x)
(?=.*\d)
(?=.*[!@\#$%^&*_-])
(?=.*[A-Z])
(?=.*[a-z])
.{8,}"
password)))
(defn email-valid?
"Validates email address. Returns true if email is valid, false otherwise."
[email]
(some?
(re-matches #"(?xi)
[a-z0-9!\#$%&'*+/=?^_`{|}~-]+
(?:\.[a-z0-9!\#$%&'*+/=?^_`{|}~-]+)*
@
(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+
[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
email)))
(defn- spec-req-key-not-found [[pred _ k :as predicate]]
(when (= 'contains? pred)
{k (str (name k) " is not set")}))
(defn spec-errors-data->error-messages
"Reduces clojure.spec errors data, i.e. received by clojure.spec/explain
function, into map of errors. errors-map must be map of maps, which structure
is the next:
{ spec-error-via-leaf -> { field-error -> error-message } }
Returns (probably empty) map of reduced errors."
[errors-data errors-map]
(reduce (fn [acc {:keys [via pred] :as problem}]
(let [err-leaf (last via)]
(if-let [err (get errors-map err-leaf
(spec-req-key-not-found pred))]
(merge acc err)
acc)))
{}
(get errors-data :clojure.spec/problems)))
(defn spec-errors-data->response
"Converts clojure.spec errors data, i.e. received by clojure.spec/explain
function, into Ring response map. Returns response map with status HTTP 400
and body. If errors map is not empty, it assigns the map to :errors key
of the body. Otherwise, body is set to \"Bad request\""
[errors-data errors-map]
(let [errors (spec-errors-data->error-messages errors-data errors-map)
response {:status 400}]
(if (seq errors)
(assoc-in response [:body :errors] errors)
(assoc response :body "Bad request"))))
(defn spec-errors->response
"Converts clojure.spec errors data into Ring response map.
See spec-errors-data->response for more information."
[spec data errors-map]
(-> spec
(s/explain-data data)
(spec-errors-data->response errors-map)))
;;; DB entity fns
(defn create-entity!
"Conforms data according to spec. If data is valid, inserts data into DB
as model, otherwise, returns HTTP 400 with appropriate error.
Returns HTTP 201 \"Created\" when DB insert is OK. Otherwise, HTTP 500."
[model data spec errors-map]
(let [entity (s/conform spec data)]
(if-not (= :clojure.spec/invalid entity)
(try
(let [current-time (now)
entity (assoc entity
:created_at current-time
:updated_at current-time)]
(dbc/insert model (dbc/value entity))
{:status 201, :body "Created"}
(catch Throwable ex
(log/error ex)
{:status 500, :body "Internal server error"}))
(spec-errors->response spec data errors-map))))
;;; User model
(defn- conform-email [email]
(if (and (string? email) (email-valid? email))
(lower-case email)
:clojure.spec/invalid))
(defn- ->user [data]
(let [user (select-keys data [:email :first_name :last_name])]
(if-let [pwd (get data :password)]
(assoc user :encrypted_password (hsh/derive pwd))
user)))
(def errors
{::email {:email "Email is not valid"}
::password {:password "Password is weak"}
::password-confirmed {:password_confirmation
"Password and its confirmation does not match"}})
(s/def ::email (s/conformer conform-email))
(s/def ::first_name string?)
(s/def ::last_name string?)
(s/def ::password (s/and string? password-valid?))
(s/def ::password_confirmation string?)
(s/def ::encrypted_password (s/and string? (complement blank?)))
(s/def ::password-confirmed
(fn [entity]
(if-let [pwd (get entity :password)]
(= pwd (get entity :password_confirmation))
true)))
(s/def ::convert-user (s/conformer ->user))
(s/def ::new-user
(s/and
(s/keys :req-un [::email ::password ::password_confirmation]
:opt-un [::first_name ::last_name])
::password-confirmed
::convert-user))
(defn create-user! [data]
(create-entity! users data ::new-user errors))
;;; Handlers
(defn signup! [{:keys [params] :as request}]
(create-user! params))
;;; API routes
(defroutes api-routes
(POST "/signup" [] signup!))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment