Liking cljdoc? Tell your friends :D

tada

tada is a clojure(script) library that helps you compose web-applications out of declarative data-driven parts.

tada consists of two things:

  • A declarative, data-driven syntax for describing the necessary pieces that make up an applications domain model. This includes:

    • entities
    • events
    • constraints
    • data-views
    • permissions
  • A collection of community-curated conversion utilities to help you turn these declerative descriptions into an actual running system

status

This library is pre-alpha, and at this stage, is meant mostly for something to discuss around.

There's not even an official release yet. (To hack on it, clone it, lein-install it and use checkouts.)

The "API"/syntax is likely to change a lot, but once it stabilises, we'll cut an alpha release.

So far, only events have been somewhat implemented.

tada.events

example

(ns bank.core
  (:require
     [tada.events.core :as tada]
     [spec-tools.data-spec :as ds]
     [clojure.spec.alpha :as s]))

;; say we're modeling a bank...
;; and we already have some specs:
;; (here, we're using ds-spec)

(s/def :bank/currency #{:CAD :USD})

(s/def :bank/user
  (ds/spec
    {:name :bank/user
     :spec {:bank.user/id uuid?
            :bank.user/name string?}}))

(s/def :bank/account
  (ds/spec
    {:name :bank/account
     :spec {:bank.account/id uuid?
            :bank.account/balance integer?
            :bank.account/currency :bank/currency
            :bank.account/owners [:bank.user/id]}}))


;; and some database functions:

(defn get-account [account-id] ...)

(defn transfer! [from-account-id to-account-id amount] ...)

(defn deposit! [account-id amount])

(defn user-exists? [user-id] ...)

(defn account-exists? [account-id] ...)

(defn user-owns-account? [user-id account-id] ...)

(defn get-account [account-id] ...)


;; we then define some events for our app:

(def events
  [{:id :deposit!

    :params {:user-id :bank.user/id
             :account-id :bank.account/id
             :amount integer?
             :currency :bank/currency}

    :conditions
    (fn [{:keys [user-id account-id amount currency]}]
       [[(user-exists? user-id) :forbidden "User with this id does not exist"]
        [(account-exists? account-id) :not-found "Account with this id does not exist"]
        [(user-owns-account? user-id account-id) :forbidden "User does not own this account"]
        [(= currency (:currency (get-account account-id))) :incorrect "Deposit currency must match account"]])

    :effect
    (fn [{:keys [account-id amount]}]
       (deposit! account-id amount))

    :return
    (fn [{:keys [account-id]}]
      (get-account account-id))}

   {:id :transfer!

    :params {:user-id :bank.user/id
             :from-account-id :bank.account/id
             :to-account-id :bank.account/id
             :amount (and integer? pos?)}

    :conditions
    (fn [{:keys [user-id from-account-id to-account-id amount]}]
       [[(user-exists? user-id) :forbidden "User with this id does not exist"]
        [(account-exists? to-account-id) :not-found "Account with this id does not exist"]
        [(user-owns-account? user-id from-account-id) :forbidden "User does not own this account"]
        [(account-exists? from-account-id) :incorrect "Account with this id does not exist"]
        [(>= (:balance (get-account from-account-id)) amount) :conflict "Insufficient funds in account"]
        [(= (:currency (get-account from-account-id))
            (:currency (get-account to-account-id))) :conflict "Currency of accounts must match"]])

    :effect
    (fn [{:keys [from-account-id to-account-id amount]}]
      (transfer! from-account-id to-account-id amount))}])


;; register our events

(tada/register! events)

;; and then we can dispatch them with do!

;; when we call with everything correct, it runs the effect
(tada/do! :deposit! {:user-id #uuid "..."
                     :account-id #uuid "..."
                     :amount 100
                     :currency :CAD})

;; if called with invalid arguments, an error is raised
(tada/do! :deposit! {:user-id #uuid "..."
                     :account-id #uuid "..."
                     :currency :CAD})
;; => error: "Missing amount"

;; if conditions aren't met, also raises an error
(tada/do! :transfer! {:user-id #uuid "..."
                      :from-account-id #uuid "..."
                      :to-account-id #uuid "..."
                      :amount 100})
;; => error: "Insufficient Funds in Account"

event manipulation

Given a set of events, tada has a number of utilities to generate useful functions (or other artefacts) to pass off to other systems.

events -> ring-handlers

tada.events.ring can be used to generate ring-handlers from events. These handlers convert the anomaly in the events to the appropriate status code. Below, we're using reitit to route these events:

(require '[tada.events.core])
(require '[tada.events.ring])
(require '[reitit.ring])

(def app
  (reitit.ring/ring-handler
    (reitit.ring/router
      ["/api"
        ["/transfer"
          {:post {:handler (tada.events.ring/route :add-event!)}}]
        ["/deposit"
          {:post {:handler (tada.events.ring/route :deposit!)}}]])))

;; plus some middleware to insert the authenticated user-id into params

Can you improve this documentation? These fine people already did:
Rafal Dittwald & James N. V. Cash
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close