Last active
July 28, 2016 15:20
-
-
Save featalion/dfd5743e8282e6db83282c12d7101e21 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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