All Projects → rafd → Tada

rafd / Tada

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

Programming Languages

clojure
4091 projects
clojurescript
191 projects
declarative
70 projects

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 alpha, and at this stage, is meant mostly for something to discuss around.

It's being used in prod, but the "API"/syntax is likely to change a lot.

So far, only events have been 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
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].