Deriving Values

Some new requirements for our app have come in:

  • We want to anonymize admins even further. Their name should be of the form "Admin-X" where 'X' is the admin users id

  • Each post should contain the name of its author

(both of these are bad ideas, but office politics forces you to accept them)

Function 'directives' (see Directives 101) won't really help us here since they don't have access to the entity being built. We need a new directive, derive:

(ns fab.tutorial
  (:require [fabrikk.alpha.core :as fab]))

(defn admin-email []
  (str "admin-" (rand-int 10000) "@example.com"))

(def user
  (fab/->factory
   ::user
   {:template {:id (fab/sequence)
               :name "John Smith"
               :email "john@example.org"
               :role "user"
               :verified true}
    :traits {:admin {:name (fab/derive :id (partial str "Admin-"))
                     :email admin-email
                     :role "admin"}
             :unverified {:verified false}}}))

(def post
  (fab/->factory
   ::post
   {:template {:id random-uuid
               :title "This one weird trick"
               :content "Some content goes here...."
               :author (fab/one ::user)
               :author-name (fab/derive [:author] :name}}))

derive allows us to, well, derive values from either keys on the same entity or from any dependent entities created using one (or other similar directives). Its first argument can be a key or a path - i.e. it identifies the entity or key to be derived from - the second is a function that's used to transform the source value into the derived value.

We'll go into more detail on paths later, for now it's enough to know that a path is a sequence of one or more keywords. Our path in the post factory is [:author] which can be translated to 'derive a value from the entity referenced through the author key', and we're using :name as the transform function to get the author's name.

Let's see this in action on the user entity first:

(fab/build user {:traits [:admin]})
;; => {:id 1, :name "Admin-1", :email "admin-8575@example.com", :role "admin", :verified true}
(fab/build user {:with {:id 100}
                 :traits [:admin]})
;; => {:id 100, :name "Admin-100", :email "admin-8902@example.com", :role "admin", :verified true}
(fab/build user {:with {:name "Omega-Admin"}
                 :traits [:admin]})
;; => {:id 2, :name "Omega-Admin", :email "admin-7767@example.com", :role "admin", :verified true}

The derived value responds to changes in the value of the underlying key - if we specify the ID it gets reflected in the user's name. If we specify the name as a string the derive has no effect.

Now for the post factory:

(fab/build post)
;; => {:author 3,
;;     :id #uuid "233c7e97-f7cf-413b-89e3-69b782c9a4d8",
;;     :title "This one weird trick",
;;     :content "Some content goes here....",
;;     :author-name "John Smith"}

(let [jimmy (fab/build user {:with {:name "Jimmy Murphy"}})]
  (fab/build post {:with {:author jimmy}}))
;; => {:author 4,
;;     :id #uuid "564653ee-5e06-40bd-a5a0-8cf8b0032172",
;;     :title "This one weird trick",
;;     :content "Some content goes here....",
;;     :author-name "Jimmy Murphy"}

As before: if we change the name of the author user, we change the value of the author-name key. Now let's try:

(fab/build post {:with {:author-name (fab/derive :author str)
                        :author-email (fab/derive [:author] :email)}})
;; => {:author 5,
;;     :id #uuid "781f0ca7-6399-46de-b056-3232cae71a98",
;;     :title "This one weird trick",
;;     :content "Some content goes here....",
;;     :author-name "5",
;;     :author-email "john@example.org"}

There are a few things to unpack here:

  • The derive directive works happily in a :with option

  • If we derive from the keyword :author instead of a path [:author], we derive from the value of that key on the post entity we build - in this case the id of the author entity

  • We can derive multiple values from the same dependent entity

Last updated