08: visiting functions

You now have most of the pieces you need to generate and insert fixture data into a test database. Now you just need to... actually insert the data! To insert data you must visit each ent in the ent db with a visiting function, and that's what you'll learn to do in this section and the next.

Earlier I wrote,

Visiting ent nodes is kind of like mapping: when you call map on a seq, you apply a mapping function to each element, creating a new seq from the mapping function's return values. When you visit ents, you apply a visiting function to each ent. The visiting function's return value is stored as an attribute on the ent (remember that ents are implemented as graph nodes, and nodes can have attributes).

You've actually already seen a visiting function at work. In the last couple sections you learned how to use spec to generate a value for each ent. The generated value was stored under the :spec-gen attribute; that's because the sg/ent-db-spec-gen you called actually applies a visiting function to the ent db it generates. Let's create our own visiting function so you can see how this works:

(ns reifyhealth.specmonstah-tutorial.08
  (:require [reifyhealth.specmonstah.core :as sm]))

(def schema
  {:user  {:prefix :u}
   :topic {:prefix    :t
           :relations {:owner-id [:user :id]}}
   :post  {:prefix    :p
           :relations {:topic-id [:topic :id]}}})

(defn announce
  [db {:keys [ent-name]}]
  (str "announcing... " ent-name "!"))

(defn ex-01
  []
  (-> (sm/add-ents {:schema schema} {:post [[1]]})
      (sm/visit-ents :announce announce)
      (get-in [:data :attrs])))

(ex-01)
;; =>
{:post  {:type :ent-type},
 :p0    {:type                 :ent,
         :index                0,
         :ent-type             :post,
         :query-term           [1],
         :loom.attr/edge-attrs {:t0 {:relation-attrs #{:topic-id}}},
         :announce             "announcing... :p0!"},
 :topic {:type :ent-type},
 :t0    {:type                 :ent,
         :index                0,
         :ent-type             :topic,
         :query-term           [:_],
         :loom.attr/edge-attrs {:u0 {:relation-attrs #{:owner-id}}},
         :announce             "announcing... :t0!"},
 :user  {:type :ent-type},
 :u0    {:type       :ent,
         :index      0,
         :ent-type   :user,
         :query-term [:_],
         :announce   "announcing... :u0!"}}

(ex-01) creates an ent db, applies a visiting function, and then looks up the :attrs key in the graph associated with the ent db's :data key. The :attrs key is where loom stores each node's attributes. We can see that each ent (:u0, :t0, :tl0) has an attribute with the key :announce and the value of "announcing... :ent-name!". Let's walk through this. You call the function ex-01, whose body is:

(-> (sm/add-ents {:schema schema} {:todo [[1]]})
    (sm/visit-ents :announce announce)
    (get-in [:data :attrs]))

sm/add-ents builds the ent db and passes it to sm/visit-ents. sm/visit-ents takes three arguments: the ent db, a visit key (:announce), and a visiting function. Then, internally, sm/visits-ents iterates overs each ent in the ent db, passing the ent's name to the visiting function along with the db and visit key, using the return value to assign an attribute to the ent.

So in the above example, the ents are :u0, :t0, and :p0, and sm/visit-ents iterates over them in that order. For :u0, it passes the following arguments to the visiting function announce:

  1. the ent db

  2. a map that includes the key :ent-name whose value is :u0

The function announce uses the :ent-name passed in the second argument to return the string "announcing... :u0!", and that gets associated with the :announce key under the ent's attributes.

So, it's great and all that the announce function returns its little string. But how would we use visiting functions to actually insert records into a database? The answer is that the map passed as the second argument to visiting functions contains a lot more information than just the :ent-name of the current ent. It also includes the :ent-type (:user, :topic, :post) and, when you're using the clojure.spec visiting function, the :spec-gen value. The next section will show you how to combine those values to insert data in a database.

Before you continue to the next section, though, try implementing your own visiting function. Can you write a visiting function that calls swap on atom, inserting a vector of [:ent-type :ent-name] for every ent?

Last updated