Skip to content
Brenton Ashworth edited this page May 27, 2013 · 20 revisions

Making a Counter

In this step, we will create a counter. The application will be able to receive messages which will cause the counter to increments. This simple exercise will introduce us to the following concepts:

  • transform functions
  • the dataflow definition
  • the input queue
  • messages
  • the data renderer
  • testing behavior code

Starting the dev tools

Before we make any changes, let's start the application so that we can see the changes as we make them.

cd tutorial-client
lein repl

Once you have a running REPL, run the following:

(use 'dev)
(run)

and then open a browser and navigate to http://localhost:3000. In the tools menu, in the lower left corner of your browser (hover the mouse to make the tools menu appear), click on the Data UI link. You will see

:greeting
"Hello World"

What is this? The default application has a very simple data model which is the value:

{:greeting "Hello World!"}

The data renderer is displaying this value by making a heading for each top level key and then printing the value of each key. We are going to make some changes which will show how our data model arrived in this state. In the next part of the tutorial we will look a bit closer at why the data is rendered in this way.

Creating the counter

We need to accomplish a few things in order to make a compelling counter:

  1. We need to define a function which can increment a value
  2. We need to have some place to store the counter value
  3. We need to know when the value has changed so that we can do something about it

The behavior of our application is defined in the file tutorial-client/app/src/tutorial_client/behavior.clj. This is where we will make our changes. It is important to note that this project layout is not required. As an application grows, you will not want to have all of your application logic in this one file. You may use normal techniques for organizing your code as the project grows.

Let's start by focusing on goal 1 above. There is a function named set-value-transform at the top of this file. Delete it. Write the following function.

(defn inc-transform [old-value _]
  ((fnil inc 0) old-value))

In Pedestal, this is called a transform function. It takes two arguments: the old value and the input message and it returns a new value. This function will simply increment the old-value. If you are new to Clojure, the fnil function is used here to wrap inc in a function which, when passed nil, will replace it with 0 before calling inc.

We have our function, what do we do with it?

Pedestal applications are described in a map. This map allows us to describe many things about how our application works. We will start very small and configure our single transform function.

Update the exmaple-app in the behavior namespace to look like this:

(def example-app
  {:version 2
   :transform [[:inc [:my-counter] inc-transform]]})

Our map has two keys: :version and :transform. :version is used to indicate the version of this description.

:transform contains a vector of vectors which define which transform function will be called when a message is received. Each vector looks like this:

[:inc [:my-counter] inc-transform]

where the first element is the type, the second element is the topic and the final element is the function to call.

When a message is received, the first matching transform function will be called. Transform functions define all of the inputs that our application can receive. A message to increment this counter will look like this:

{msg/type :inc msg/topic [:my-counter]}

In Pedestal, every message has a type and a topic. Here the type is :inc and the :topic is [:my-counter]. When this message is received it will be routed to the first transform function which matches the type and topic.

Remember that goal 2 above was to have some place to store the counter value. That place is defined in the message as [:my-counter]. We can think of this a path to the location in the data model where the function will do its work. The transform function will receive the old value at this path as input and the value returned by the function will become the new value at this path.

What if we wanted to have lots of counters and not just one. We could change the description above to be:

(def example-app
  {:version 2
   :transform [[:inc [:*] inc-transform]]})

Using a wildcard instead of a keyword. This means that any :inc message type will be handled by this function and the actual path int he data model which will be updated is determined by the message. Note that this will only match any path with one element. To match any path we could use:

[:inc [:**] inc-transform]

We now have a function, a place to store the counter value and way to make the counter increment. What about goal number 3? How will we know when the counter has changed so that we can do something about this. This is handled for us by the application at the moment. In the next section we will see how to customize this behavior but for now we will go with the default behavior.

If we make the changes above and refresh the browser window, we will see nothing. We have defined behavior but now we need to send an appropriate message to actually see it work.

Sending a message

In the file tutorial-client/app/src/tutorial_client/start.cljs you will find the create-app function which is used to create and start a new application. Notice that there is a call to p/put-message. This function call is adding a message to the input queue to be fed into the out application.

Change this line to send an :inc message as shown below.

(p/put-message (:input app) {msg/type :inc msg/topic [:my-counter]})

Now refresh the browser and you should see that the value under :my-counter is now 1. In the next section we will add a way to increment the counter form the user interface.

Testing

Before we move on to the next section, it will be helpful to discuss testing. At this point we have some behavior and it is easy enough to test it by refreshing the browser. As an application grows this will be harder to do. It would be good to have some automated tests in place.

In the file tutorial-client/test/tutorial_client/test/behavior.clj there are some tests which worked with application which we started with but are now broken. Delete these tests and we will write some new ones.

Transform functions are pure functions and are therefore very easy to test. We may create some tests for our transform function like the one shown below.

(deftest test-inc-transform
  (is (= (inc-transform nil {msg/type :inc msg/topic [:my-counter]})
         1))
  (is (= (inc-transform 0 {msg/type :inc msg/topic [:my-counter]})
         1))
  (is (= (inc-transform 1 {msg/type :inc msg/topic [:my-counter]})
         2))
  (is (= (inc-transform 1 nil)
         2)))

Notice that this particular transform function does not depend on the input message. We may also want to the application in general. Before we write this test, let's make a helper function which will extract the data model from an application object.

(defn- data-model [app]
  (-> app :state deref :data-model))

Everything that we need to know about an app is contained in the app object. It is educational to explore its contents.

The test below shows how to create an app and then send it one or more messages. Once the messages are processed we can check the state of the data model. The run-sync! function will ensure that all of the messages have been processed before returning.

(deftest test-app-state
  (let [app (app/build example-app)]
    (is (test/run-sync! app [{msg/type :inc msg/topic [:my-counter]}]
                        :begin :default))
    (is (= (data-model app)
           {:my-counter 1})))
  (let [app (app/build example-app)]
    (is (test/run-sync! app [{msg/type :inc msg/topic [:my-counter]}
                             {msg/type :inc msg/topic [:my-counter]}
                             {msg/type :inc msg/topic [:my-counter]}]
                        :begin :default))
    (is (= (data-model app)
           {:my-counter 3}))))

Once we have tests in place we can run them from the command line by running

lein test

or

lein difftest

The second command will show diffs when a test fails.

There are more advanced techniques which can be used to test our applications but for now this is good enough. Notice that even though this is behavior that will run in the browser, we are testing it as plain old Clojure code.

This works because the tutorial-client.behavior namespace is marked as :shared and uses only Clojure code that can be compiled to ClojureScript.

(ns ^:shared tutorial-client.behavior
    (:require [clojure.string :as string]
              [io.pedestal.app.messages :as msg]))

The tag for this step is step2.

Clone this wiki locally